--- name: skill-delete description: Remove a skill from the jsc skill set safely. Pick the skill from the Gitea canonical marketplace skill list, inventory every file referencing it, fix each affected file through decision-tree questions in parallel sub agents until guideline checks pass, delete the skill, open a PR via jsc-git pr, then deploy per references/deploy-verify.md and verify from a fresh CLI process that no on-disk leftover remains, and append the change report to wiki SKILLSET_{HASH}. Use only for removal; not for renaming (use skill-update). --- # skill-delete — delete a skill Single source of guidelines: [`../../references/guidelines.md`](../../references/guidelines.md). ## Flow 1. Run `tools/sync-domains.sh` to sync every domain repo of the Gitea canonical marketplace. Completion condition: the script exits 0 and prints one `domainpath` line per marketplace domain — exit 0 is the only code that means every repo is present and current. Exit 3 means some repos were not updated: reconcile every path named on stderr (commit or stash the dirty tree, or fix the failing pull) and rerun; when the user confirms a dirty tree is intentional local work, record that decision and continue on the local version — never read exit 3 as current. Exit 2 means a domain could not be cloned. Exit 1 means the root could not be derived, `gitea.sh` was not found, or the canonical marketplace was unreadable; when stderr says the root could not be derived, set `JSC_PLUGINS_ROOT` to the directory that holds the domain repos and rerun, because under a plugin install the script sits in the CLI's plugin cache and its built-in guess lands there instead of the domain workspace. Resolve 2 and 1 before continuing. 2. Run `tools/list-skills.sh` and present its `domain / name / description` rows to the user. The tool prints skills, not domains, so read the domain column to prove coverage. Exit 1 means the root could not be derived, the domain list was unreadable, or no skill was found — read stderr, fix the named cause (`JSC_PLUGINS_ROOT` for the root case, as in step 1) and rerun; never read it as an empty skill set. Completion condition: the script exits 0 and every domain printed by step 1 appears in at least one row; a domain with no row means its repo is missing or holds no skill — return to step 1 for that domain. 3. Let the user pick the skill to delete. Options state the impact scope: which skills reference it, and that its command stops working after deletion. Completion condition: one `{domain}/{name}` pair is confirmed for deletion. 4. Inventory every file related to the skill: run `tools/find-skill-refs.sh {domain} {name}` to list every file that references the skill name or its `/jsc-{domain}:{name}` command form, across every marketplace domain repo on this machine (covers other SKILL.md files, the domain README's 「Skills 目錄」 section, the two marketplace.json files in `plugins/meta` plus their synced copies in every domain repo, `tools/`, and the `jsc-hooks` wiring). The skill-name pattern is a bare substring match, so the list also carries other skills whose name starts with the same word plus plain prose — treat it as candidates to read, not as files that must change. Exit 0 means hits were printed; exit 1 means a clean zero-hit scan; exit 2 means a usage error, so fix the two arguments and rerun; exit 3 means the scan never ran — the root could not be derived, the domain list was unreadable, no domain repo is on this machine, or grep failed. Read stderr and fix the named cause; when it names the root, set `JSC_PLUGINS_ROOT` to the directory that holds the domain repos, because under a plugin install the script sits in the CLI's plugin cache and its built-in guess lands there. **Never read exit 3 as zero hits** — a failed scan taken as "no references" makes this skill skip files it must fix. Completion condition: the tool's file list is captured as the step 5 inventory. 5. Fix every file in the step 4 inventory. For each file: 1. Read the file and decide whether it needs a fix to keep its current behavior after the deletion. If no fix is needed, record it as no-fix-needed with the reason and **skip the rest of this loop**. Completion condition: the file carries a recorded verdict — needs-fix or no-fix-needed with a reason. 2. Ask for fix details via the `jsc-ask:ask` decision tree (call a replacement skill? move a deterministic input/output flow to `tools/`? run the detailed flow as a sub agent? drop the feature too?). If the fix touches wiki or Gitea access, confirm it reads inherited environment variables before asking the user. Every option states its impact scope. Completion condition: every question has a recorded answer. 3. Apply the confirmed fix, then check the guidelines.md audit checklist for the file — the per-file fix work MUST run as a sub agent, one sub agent per affected domain repo, and those sub agents **run in parallel**: each repo's files are independent, so serialising them only adds waiting. Each sub agent reports one line per file: the path and either the applied fix or「無需修正」with the reason. On any checklist failure, return to step 5.2. Completion condition: the fix is in the file and every checklist item passes for it. Completion condition: every file in the step 4 inventory is marked either fixed-with-a-clean-checklist or explicitly no-fix-needed with a reason — no file is left without a verdict. 6. Delete the skill directory `skills/{name}/` and remove that skill's `## {name}` section from `references/behaviors.md` — the whole section, its table included, leaving every other section untouched. Both deletions ship in this same PR: a behavior list still carrying a deleted skill fails the domain's next audit, and the extra section is exactly what `check-behaviors.sh` reports. Then run `tools/sync-skill-manifest.sh {domain-path}` directly (no sub agent needed) to sync the domain README and bump the version in all three manifests. Route each exit code: 0 — the README block and all three manifests are synced; 1 — the domain path, `skills/`, `README.md`, the `JSC-SKILLS` markers, a remaining `SKILL.md`, a manifest, or a manifest `version` field is missing, so fix the named cause on stderr and rerun; 2 — usage error, the script takes exactly one argument; any other code — the script runs under `set -e`, so treat it as an environment fault and stop, never as a successful sync. Then run `tools/check-behaviors.sh {domain-path}` and route each exit code: 0 — the remaining sections match the remaining skills; 1 — every mismatch is printed on stderr as `{檔案}:{技能名}:{說明}`, so fix each one and rerun, the deleted skill's leftover section included; 2 — usage error, the tool takes exactly one argument; 3 — nothing was checked, because `references/behaviors.md` is missing, `skills/` is missing, or no `SKILL.md` was found, so fix the named cause and rerun. **Exit 3 is never a pass.** Completion condition: the directory is gone, `references/behaviors.md` holds no `## {name}` section for the deleted skill, `tools/check-behaviors.sh {domain-path}` exits 0, the README's 「Skills 目錄」 no longer lists the skill, and all three manifests show the same new version. 7. Call `jsc-git:pr` to open a Push Request. Completion condition: a PR URL comes back and is reported with the table format in [`../../references/pr-report.md`](../../references/pr-report.md). 8. Deploy the deletion, verify it took, then report: 1. Follow [`../../references/deploy-verify.md`](../../references/deploy-verify.md) from section 1 to section 5: `tools/deploy-route.sh {domain-path}` picks the route, the deploy route or the worktree route runs, and the verification then runs in a **fresh CLI process**, never in the session that ran the deploy. That session raised the restart gate itself and still holds the old skill set, so verifying inside it either gets blocked or passes on stale behavior. On the worktree route, add that the skill stays installed and stays callable until the outstanding release PR merges. Completion condition: every completion condition in `deploy-verify.md` sections 1 to 5 holds for this domain repo. 2. Verify the deletion concretely, on top of the `deploy-verify.md` items: - `tools/list-skills.sh` prints no row carrying the deleted `{domain}/{name}`. - **Deep-delete verification.** Run `tools/verify-skill-removed.sh {domain} {name}`; it detects the installed CLIs and greps each one's skill cache and hook config. Route each exit code: exit 0 — no leftover in the locations listed on stderr; exit 1 — leftovers printed as `{file}:{line}:{content}`, so remove every one by hand and rerun; exit 2 — usage error, fix the two arguments and rerun; exit 3 — nothing was checkable, because the root could not be derived, `detect-clis.sh` was not found, no CLI was detected, or no config location exists. The script printed no leftover because it looked nowhere, so exit 3 is **never** clean: report「無處可查」with the reason from stderr, and carry that sentence into step 8.3's wiki section and the final report, so nobody later reads the deletion as verified on disk. This is the only place the deep-delete check runs — running it before the PR only scanned a deletion that had not been deployed yet, so it always came back clean and proved nothing. - Every tool and skill that step 5 fixed still finishes with its documented exit code — a fix that broke a caller shows up here, not earlier. - One minimal prompt per checkable CLI, each in its own fresh process and all in parallel, confirming `/jsc-{domain}:{name}` is gone or that the replacement path still works. On any mismatch — the deleted skill still listed, a leftover from exit 1, a fixed caller that now fails, a prompt failure, or unexpected stderr — fix the cause and rerun this step from 8.1. Completion condition: the skill is absent from the list, the verification script exits 0 or its exit 3 is reported as「無處可查」and carried into step 8.3, every fixed caller ran, every checkable CLI completed the prompt with the expected result, and every untestable CLI has a stated reason. 3. Write the change report to the wiki — this part MUST run as a sub agent. It is two pages in two repos, and they must not be mixed up. - **Content page `SKILLSET_{HASH}`.** Resolve its repo with `jsc-gitea/tools/gitea.sh wiki-repo SKILLSET`, which reads `JSC_WIKI_REPO_SKILLSET` first, then `JSC_WIKI_REPO`. `{HASH}` is `gitea.sh hash-id "{owner}/{repo}"` of the domain repo that lost the skill, used at the full 40 characters it prints. Write it through `jsc-gitea:wiki` following [`../../templates/skillset-page.md`](../../templates/skillset-page.md): **append** a section for this change — date, 「刪除」, skill name, changed files (the step 5 inventory verdicts included), PR URL, the step 8.1 route verdict and the step 8.2 verification result per item, the deep-delete verdict「無處可查」included when it applies — and keep every earlier section. - **Directory page `SKILLSET_CONTENTS`.** It lives in the CONTENTS repo, never in the SKILLSET one. `wiki-contents.sh` resolves it itself with `gitea.sh wiki-repo CONTENTS`, whose chain is `JSC_WIKI_REPO_CONTENTS` then `JSC_WIKI_REPO` and never falls back to `JSC_WIKI_REPO_SKILLSET`. Build one file holding the single row from [`../../templates/skillset-contents.md`](../../templates/skillset-contents.md), its 異動頁 cell written as `[SKILLSET_{HASH}]({url})` with the **absolute** URL from `gitea.sh wiki-url {SKILLSET repo} SKILLSET_{HASH}`. Every link on both pages takes that `[{text}]({url})` form; the double-bracket wiki-link form resolves only inside one wiki, so it is never used. Then run: `jsc-gitea/tools/wiki-contents.sh upsert SKILLSET 2 "{owner}/{repo}" {row file} templates/skillset-contents.md` The key column is `2`, the 存取庫 column, holding that repo's `{owner}/{repo}` exactly as the row file writes it, so one domain keeps exactly one row and no other domain's row moves. Never hand-edit the directory page. Write the content page first and fetch the URL only after it exists. - **Check the links before writing.** Hand every URL going onto the content page and into the directory row to `jsc-gitea/tools/link-check.sh`, and write only when it exits 0. It verifies through the Gitea API, never a web status code: a private repo answers 404 to an unauthenticated web request, so a status-code check would call a live page dead. - **Exit codes.** Route every one of them: | Call | Exit | Do | | --- | --- | --- | | `gitea.sh wiki-repo` | 2 | The page type was misspelled. Fix the argument and rerun | | | 3 | No wiki repo is configured for that type. Name the variable (`JSC_WIKI_REPO_SKILLSET` for the content page, `JSC_WIKI_REPO_CONTENTS` for the directory page) and `JSC_WIKI_REPO`, ask per the `jsc-ask:ask` rules, then rerun | | `gitea.sh hash-id` | 1 | No SHA-1 helper on this machine. Stop and report that `sha1sum` or `shasum` has to be installed, and never hand-compute the hash | | | 2 | Empty input, so the `{owner}/{repo}` was never resolved. Fix that first | | Content page read | 0 | Append into the sections already there | | | 4 | The page does not exist yet, so build it from `templates/skillset-page.md` | | | 7 or 8 | Stop and write nothing: a page rebuilt on top of an unread read loses every section already on it | | `gitea.sh wiki-url` | 4 | The content page is not there, so the write above did **not** succeed. Go back and write it, and add no directory row until the page exists | | | 5 | The API answered with no `html_url`. Stop and report it; never assemble the URL by hand from the host and the page name | | `link-check.sh` | 0 | Every link is reachable. Write the page | | | 1 | At least one link is dead. Write nothing, and report the `DEAD` lines it printed | | | 2 | No URL was passed, which is a defect here. Pass the links and rerun | | | 3 | `GITEA_HOST` is unset. Set it and rerun; never skip the check instead | | | 7 | Gitea authentication failed. Stop and report the key problem, and never read it as a dead link | | `wiki-contents.sh upsert` | 0 | The row is in place. Report the `updated` or `added` it printed | | | 1 | The write failed. Report `SKILLSET_CONTENTS` as not written, together with the row content | | | 2 | An argument was rejected. Fix it and rerun; nothing was written | | | 3 | No CONTENTS wiki repo is configured. Report `JSC_WIKI_REPO_CONTENTS` and `JSC_WIKI_REPO` as the two variables to set; the new section is on `SKILLSET_{HASH}` and stays there | | | 4 | The directory page is absent and no template was passed. Rerun with `templates/skillset-contents.md` as the fifth argument | | | 7 | The token is invalid or lacks permission, so the other domains' rows are unknown. Stop, report the token problem, and create no page | | | 8 | Some other API failure. Stop, report that status, and create no page | On any failure, hand the page name and the unwritten entry back to the user and leave this step open; never close the flow on an unwritten report. Completion condition: `SKILLSET_{HASH}` holds the new section plus all earlier sections, and `wiki-contents.sh upsert` exited 0 with this domain's row on `SKILLSET_CONTENTS` linking that page by absolute URL. 9. Report this run's outcome to the local event stream — the last step of every run, the ones that stop early included. Run: `jsc-hooks/tools/report-status.sh skill-end jsc-meta:skill-delete {status} {exit code} [detail]` Resolve `jsc-hooks` from the `domainpath` row step 1 printed for the `hooks` domain, the same way this skill resolves every other cross-plugin script. **When that script is not on this machine, skip this step in silence and close the run as normal.** A reporting path that is absent must never fail the run it reports on, and this call's own exit code never changes what this skill reports. Pick `{status}` from what the run actually did: | status | Use it when | | --- | --- | | `ok` | the skill directory and its behavior-list section are gone, the PR is open, `deploy-verify.md` sections 1 to 5 hold, `verify-skill-removed.sh` exited 0, and both wiki writes exited 0 | | `blocked` | a gate or a missing prerequisite stopped the run before any file changed — `sync-domains.sh` never reached exit 0, or no skill could be listed to pick from | | `failed` | the run broke mid-way — a leftover from `verify-skill-removed.sh` exit 1 could not be removed, or a wiki write failed again after its one retry | | `degraded` | the deletion landed with a part missing — the deep-delete check came back 「無處可查」, or the content page was written while its `SKILLSET_CONTENTS` row was not | | `aborted` | the user stopped the run, or a prerequisite turned out not to hold and this skill stopped on its own | `{exit code}` is this run's own result as a number: `0` for `ok`, non-zero otherwise. `detail` is optional, one line, at most 200 characters. The matching `skill-start` comes free from the hook, which fires when the skill loads. The deletion itself happens in the model turns after that, so no hook can see how the run ended — a `start` with no `end` reads as an abort, which is why writing the `end` is this skill's own job. Completion condition: one `skill-end` line for this run is appended to `$JSC_HOME/usage/events.jsonl`, or the script was absent and the final report says so.