fix(skill-quality): name the sibling skill when a cross-skill citation misses - #2179
Conversation
…n misses Check 5 resolves every bare `context/…`-shaped path against the CITING skill's own directory. A skill citing a sibling skill's supporting file therefore failed with "no such file under the skill dir" while the file plainly existed one directory over, and the message pointed at the citing skill — so the author's natural next move was to look for the file where it could never be. That misdirection cost two round trips in a single PR (#2161), once in each direction. The bare form really is wrong, so this stays a FAIL; only the diagnosis changes. When the unresolved path resolves under a sibling skill of the same skills root, the finding names that sibling and the citation form that works. Which form is layout-dependent, so the message follows check 1's existing plugin detection: `${CLAUDE_PLUGIN_ROOT}/skills/<sibling>/…` in a plugin-shaped root, `../<sibling>/…` outside one, where that variable is undefined and advertising it would be wrong guidance. Extraction is deliberately untouched — prose and inline-code refs stay in scope, since narrowing to link targets would drop coverage the check is presumed to have. PLUGIN-PHILOSOPHY.md gains the rule where the anchoring rules already live, so an author meets the form before the gate teaches it to them. Closes #2173 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @kyle-sexton's task in 1m 25s —— View job Security review complete
This PR only touches a local, static shell-based lint script for skill-authoring quality ( Notes from what I checked (not findings, just what was verified as safe):
Per instructions, I deferred to zizmor's static lane for any workflow/supply-chain/permissions concerns — none of those surfaces are touched by this PR anyway (no Branch: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e60197baa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 4m 10s —— View job Review complete
Scope note: this repo has What the change doesCheck 5 in CorrectnessTraced the new branch (
One inline suggestion posted (maintainability, not a correctness bug): the plugin-shaped-root test ( Docs / changelog / version
What I could not independently verifyThis session's sandbox blocked executing No blocking issues found. This is a solid, well-scoped fix with good test coverage for the plugin / non-plugin / no-sibling-hosts cases. |
|
Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
…in detection Review findings on the check-5 message. The sibling existence hit is evidence, not proof. Check 5 deliberately extracts prose and inline-code refs, so a generic path (`scripts/run.sh`) can collide with an unrelated same-named file under a sibling. The first wording asserted the cross-skill reading and dropped the hand-verify caveat, which would misdirect exactly the illustrative-example case the caveat exists for. The finding now keeps the original message verbatim and APPENDS the sibling evidence conditionally, so both readings — real cross-skill citation, coincidental name collision — stay live and the author still gets the anchored form to paste. The plugin-shaped-root test was also restated verbatim at two call sites (check 1's bare-alias warning, check 5's suggested form). One layout convention asserted twice drifts the moment it changes, so it is hoisted to a single IS_PLUGIN_SKILL computed once after the skill dir resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FdBsorqGjCU4Mg5nrmgz4m
|
Claude finished @kyle-sexton's task in 2m 32s —— View job Review complete
Scope note: per This is the follow-up commit — both round-1 findings are addressedCommit
Correctness (traced by hand)
No correctness issues found in this diff. Docs ( Not independently verifiedI ran no commands in this session beyond reading the diff and source by hand ( |
|
Claude finished @kyle-sexton's task in 1m 28s —— View job Security review complete
No security issues found. This PR only touches a local, static shell-based lint script for skill-authoring quality ( Verified independently on the current head (3f58e20), which is one commit ahead of what the earlier automated pass on this PR reviewed:
No Branch: |
…eferred gate calls (#2186) No linked issue ## Summary Three deferred judgment calls, all made here. **The uncited doc pages were never dispositioned, and the set is larger than recorded.** PR #2177 worked from an index of 112 core `/docs/en/` pages with ~33 uncited. Re-derived today, the index lists **135** core pages and this repo cites **78**, leaving **57** unevaluated. #2177 also demonstrated the miss rate of dismissing by eye: it took six pages that had been written off as irrelevant and found every one load-bearing enough to warrant a verdict. So all 57 get a recorded disposition here rather than a second eyeball pass. The finding that mattered is in the **cross-platform contract**. It reads one axis — the operating system — and names `feature-availability` as its canonical input. That page carries two axes, model provider and subscription plan, and scopes itself to what runs locally: "The Claude Code CLI and everything that runs locally work on every provider." The **host surface** a consumer runs in was therefore never read at all, by either the contract or its input. It has to be, because a host can withhold the plugin system itself rather than one capability, and where no plugin loads there is no portable path for one to owe. **`check-skill.sh` check 5** and **check 12's 4-skill warning floor** were both left open as "a separate call". Both are decided, at their own sites, with the reasoning recorded so neither is re-litigated from a false premise. ## Fix ### 1. Uncited-page disposition (57 pages) One stated relevance test, applied to all 57 so the dismissals are auditable rather than tacit: > **Relevant** if the page describes a surface a plugin author can **declare, invoke, or must > accommodate.** Otherwise **not relevant.** **Split: 1 adopt / 0 defer / 3 decline / 4 relevant-as-evidence / 49 not relevant.** Verdicts land in `docs/PLUGIN-PHILOSOPHY.md` under Native-first → **Recorded gate runs**, in the [upstream-drift](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/upstream-drift/README.md) four-part shape (claim, basis, as-of date, recheck trigger), following #2177's form. | Page | Disposition | |---|---| | `platforms` | **Relevant → ADOPT, as a citation.** The canonical host-surface index; the axis `feature-availability` does not carry. Lands doctrine. | | `github-enterprise-server` | **Relevant → DECLINE.** A real plugin-distribution surface ("Plugin marketplaces \| ✅ Supported") — declines on need, not subject: nothing here documents a GHES-hosted mirror or fork. | | `ultrareview` | **Relevant → DECLINE.** Fails gate 1: every run is human-confirmed and metered, so no skill can reach it. | | `chrome` | **Relevant → DECLINE.** Ships as the built-in `claude-in-chrome` skill; nothing to declare. Recorded because it only *looked* cited — see the dead-link note below. | | `desktop`, `vs-code`, `mobile`, `desktop-wsl` | **Relevant → read in full as the evidence base for the `platforms` row**; no separate verdict, because they are one finding seen from four pages rather than four surfaces. Quoted verbatim in that row. | | `jetbrains` | Not relevant — false friend: "plugin" there is the JetBrains IDE plugin, a different sense. Ranked 2nd by `plugin`-keyword density and is the one page where that signal is pure noise. | | `desktop-quickstart`, `desktop-linux`, `desktop-ios-simulator`, `web-quickstart`, `troubleshoot-install` | Not relevant — install and first-run recipes; nothing declarable. | | `slack`, `claude-tag` | Not relevant on their own — delegation front ends indexed by `platforms`, which is the adopted citation; `slack` is additionally being retired for Team/Enterprise. | | `devcontainer` | Not relevant — container recipe; its only `marketplace` hit is a VS Code extension link. | | `gitlab-ci-cd`, `github-actions-cloud-providers` | Not relevant — CI recipes and provider IAM routing; zero plugin or skill surface (`gitlab-ci-cd`: 0 keyword hits). | | `amazon-bedrock`, `google-vertex-ai`, `microsoft-foundry`, `claude-platform-on-aws` | Not relevant — provider auth/IAM config; the plugin-facing consequence is the availability matrix, already adopted as `feature-availability`. | | `gateways`, `llm-gateway`, `llm-gateway-connect`, `llm-gateway-protocol`, `llm-gateway-rollout` | Not relevant — org request-routing plane between the client and a provider; no plugin declares or observes it. | | `claude-apps-gateway`, `claude-apps-gateway-config`, `claude-apps-gateway-deploy`, `claude-apps-gateway-on-aws`, `claude-apps-gateway-on-gcp`, `claude-apps-gateway-spend-limits` | Not relevant — deploying and operating Anthropic's gateway product; `gateway.yaml`, Kubernetes, spend caps. | | `self-hosted-environments`, `self-hosted-environments-quickstart`, `self-hosted-environments-configuration`, `self-hosted-environments-deploy`, `self-hosted-environments-identity`, `self-hosted-environments-reference`, `self-hosted-environments-testing` | Not relevant — standing up and operating cloud-session runners on org infrastructure. | | `admin-setup`, `authentication`, `legal-and-compliance`, `third-party-integrations` | Not relevant — enterprise deployment, identity, and policy plane; no surface a plugin declares or observes. | | `analytics` | Not relevant — but **fetched, not assumed**, because per-skill or per-plugin cost attribution would have bound instruction economy. It has none: attribution is PR-level only. (The per-skill/per-plugin usage breakdown is a consumer-side `/usage` dialog, not an authoring input.) | | `network-config` | Not relevant, and the third clause of the test is why rather than the family label: proxy, custom CA, and mTLS are **transport configured on the client**, so a skill making a network call either succeeds or sees an ordinary failure — there is nothing to declare or degrade. Its two plugin-adjacent lines are egress allowlist entries a network admin sets, not a plugin (`downloads.claude.ai` for "Plugin executable downloads"; `storage.googleapis.com` for "plugin metadata shown in `/plugin`"). | | `corporate-launcher` | Not relevant, checked against the page rather than dismissed as admin tooling: `CLAUDE_CODE_PROCESS_WRAPPER` wraps "every process Claude Code launches **from its own binary** — the background service, every session it hosts in agent view, and Claude Code's relaunches after an update". A plugin's `${CLAUDE_PLUGIN_ROOT}/bin/` invocation is a Bash-tool subprocess, not a Claude Code self-spawn, so the `bin/` stance is unaffected and owes no change. | | `champion-kit`, `communications-kit` | Not relevant — internal-advocacy and rollout-comms collateral. | | `accessibility`, `keybindings`, `terminal-config`, `voice-dictation`, `fullscreen`, `fast-mode` | Not relevant — consumer client settings; no plugin declares or must accommodate them. | | `prompt-library` | Not relevant — copy-paste prompts for users, not an authoring surface. | **Doctrine added — one paragraph, plus four table rows.** The cross-platform contract gains the host axis, citing `platforms` and restating none of its facts. The three verbatim host facts (Desktop-in-WSL sessions lack "connectors and plugins"; `/plugin` "[doesn't] work from the app" on mobile; Desktop's Cowork tab sources plugins "not from the CLI's `~/.claude` directory") live in the gate-run row, where they carry a recheck trigger — not in the contract, which states only the rule they establish. **A dead citation, deliberately not fixed.** Every doc URL this repo cites was checked live — all 81 slugs plus the 4 subpath citations (`agent-sdk/overview`, `agent-sdk/agent-loop`, `agent-sdk/plugins`, `whats-new/2026-w32`). **84 of 85 return 200.** One does not: `code.claude.com/docs/en/browser` now 404s (`chrome` is the live page). Its sole occurrence is `plugins/playbooks/skills/boris/vendor/SKILL.md:938` — a **verbatim upstream baseline kept for drift detection**, which the plugin README says to treat as untrusted and which `/playbooks:update` owns. Hand-editing it would corrupt the vendor SHA it exists to compare. Recorded in the `chrome` row with that path as its recheck trigger instead. ### 2. `check-skill.sh` check 5 — KEEP the extractor as-is (decided, recorded at the site) Two premises are usually offered for narrowing to markdown-link targets. Both are false, and the comment now says so, because the premise is what keeps the question alive: 1. **"It matches bare paths in prose."** It does not, and never did. Both generators are delimited — backtick-wrapped, or a `](…)` link target — and both are scoped to the `INTERNAL_DIRS` allowlist. Naked prose cannot match. (#2179's own summary and CHANGELOG entry describe it as extracting "prose and inline-code refs"; the in-script wording is corrected here to match what the greps do.) 2. **"The backtick branch is redundant."** Measured over the 196-skill corpus rather than argued: | Measure | Count | |---|---| | Backtick-form refs, all SKILL.md | 282 | | Link-form refs, all SKILL.md | 475 | | **Unique backtick-form refs with no link form anywhere in the same file** | **122** | | …spread across | **39 skills** | | …of those 122, resolving to a real file today | **122 (100%)** | Narrowing would drop 122 real, currently-resolving supporting-file references across 39 skills. The link branch being the larger share is not the question; the overlap is, and 122 refs sit outside it. The false-positive risk that motivated the proposal is real but **latent, not observed** — zero on the current corpus. It is handled by message wording (every failure carries `hand-verify the line before fixing, may be an illustrative example`) rather than by deleting coverage of 39 skills. Reopen only if a false positive is actually observed. ### 3. Check 12's 4-skill warning floor — INTENTIONAL, no dmi carve-out (all 4 confirmed) #2181's reasoning holds, and upstream states the premise more strongly than #2181 did. The skills doc's frontmatter-behavior table gives, for `disable-model-invocation: true`: **"Description not in context, full skill loads when you invoke"** — so trigger phrasing on such a skill cannot route anything, at all. `user-invocable` defaults to `true` (confirmed on the same page, not assumed), so `github:setup` omitting it is slash-command-only, exactly its declared contract. The load-bearing half of #2181's argument is the *stranded-phrase* test, which is an empirical claim about the current tree, so each was re-checked against the tree rather than against #2181's prose: | Skill | Verdict | Confirmed against the tree | |---|---|---| | `discipline:wait-what` | **Right to leave** | Its description *is* the instruction; the trigger is noticing you have stopped following. No sibling needed — by construction the model cannot detect it. | | `firecrawl:update` | **Right to leave** | Maintainer-only. Sibling `firecrawl:firecrawl` **verified** to carry the consumer phrases (`'scrape this page'`, `'crawl this site'`, `'WebFetch is blocked'`, …). Nothing stranded. | | `playbooks:update` | **Right to leave** | Maintainer-only. Sibling `playbooks:boris` **verified** to carry `'how does Boris use Claude Code'`, `'Claude Code workflow tips'`, `'optimize my CLAUDE.md'`, … Nothing stranded. | | `github:setup` | **Right to leave** — the weakest of the four as originally argued, and it holds | #2181 argued from intent ("user-invoked only"). Checked instead for a stranded phrase: model-invocable siblings `github:advise` and `github:audit` carry the plugin's consumer-facing routing, including `'help me set up Y'`. `setup` covers plugin *prerequisites* (gh auth, writing `.claude/github/`), which is a deliberate slash command, not a routing target. | **No carve-out is added**, and that is the recorded call. Exempting dmi-true from check 12 would suppress a warning that is doing no harm while hiding the `kindle-dedrm` failure mode #2181 itself surfaced — a phrase reachable only from a skill the model can never match. The floor stays; the exemptions stay documented at the check-12 site. ## Verification **Method.** Every page was fetched with `curl -sL …/<slug>.md` — the raw markdown, not WebFetch. That removes the summarizer and the truncation window from the loop entirely, so the METHOD RULE holds trivially: every upstream sentence quoted in this PR and in the doctrine is verbatim from a complete page, and a genuine "the page never states X" is a checkable claim rather than a routine false negative. Byte counts confirm no truncation (e.g. `desktop.md` 96,288 bytes, `vs-code.md` 49,764). No page was asked to confirm a sentence from this repo. **The uncited set was re-derived, not inherited.** The grep was also re-run with **no `--include` filters** to be sure no citation lives in a file type the filter misses — identical result, 81 slugs, so 57 uncited is the real number. **Every cited URL was checked live**: 84 of 85 (81 slugs + 4 subpath citations) return 200; the single 404 is the vendored `browser` link described above. **The gate-1 check that decided the headline adopt** was run against the page rather than assumed: `feature-availability`'s section headings are *Availability by model provider*, *Availability by subscription plan*, and *Model availability* — no host-surface axis — and its only feature table header row is `| Feature | Pro | Max | Team | Enterprise |`. Had it carried a host axis, `platforms` would have been a redundant second index and this would be a decline instead. Gates run the CI way, against the **committed** tree, base-ref form: | Gate | Result | |---|---| | `bash scripts/check-contract-slice-prune.sh --check-diff origin/main` | pass — leaves no path under `docs/topics/` | | `bash scripts/check-changelog-parity.sh --check-bump origin/main` | pass | | `bash scripts/check-changed-skills.sh origin/main` | pass — no changed skills | | `bash scripts/check-skill-portability.sh origin/main` | pass — no skill files in scope | | `bash scripts/check-shell-portability.sh origin/main` | pass — no unexcused GNU-only constructs | | `npx --yes markdownlint-cli2` over all 3 changed `.md` | **0 errors** | | `shellcheck` + `shfmt -i 2 -d` on `check-skill.sh` | clean | | `bash -n check-skill.sh` | clean | | Line endings | all 5 changed files `i/lf w/lf` | Because the change to `check-skill.sh` is comments only, `check-changed-skills.sh` exercises nothing — so the script was run directly to prove it still parses and behaves: - `check-skill.sh measure` → `PASS — 0 errors, 0 warning(s)`, `all 10 base-ref trigger phrase(s) preserved`. - `check-skill.sh wait-what` → `PASS — 0 errors, 2 warning(s)`, one of which is verbatim `description has no 'Use when:' trigger phrasing` — confirming the documented floor still fires as described rather than being silently suppressed. - `check-skill.test.sh` runs to completion in CI (`plugin-gate`); locally on Windows/Git Bash it is impractically slow, per the coverage note #2179 recorded. Nothing here is behavioral. `plugins/skill-quality` → **0.15.2** with a matching `## [0.15.2]` entry. The `docs/` changes are docs-only and owe no plugin bump; the `upstream-drift` **Adopters** registry already carries a row for the gate-run table (added in #2177), and these rows join that table rather than create a new adopter, so that convention needs no version change. `docs/OFFICIAL-DOCS.md` gains the four newly load-bearing pages, per the rule its own warning states and the precedent #2177's review set: a needed page that is not listed must be added. No `docs/topics/<slug>/` directory was created — the durable outcome is doctrine text, as the Contract-tier prune rule requires. ## Review rounds Three threads, all real, all answered and resolved. Each found a defect in the *basis* of a row rather than in its verdict, which is the failure mode a decision record most needs caught: a verdict outlives the reasoning nobody re-reads. - **The GHES row's premise was overstated and its trigger fired on arrival** (`chatgpt-codex-connector`). It claimed "every plugin README ships the github.com shorthand". Re-derived from the tree: 54 of 65 carry the literal string, 9 carry no install block, `dometrain` points at another github.com marketplace, and `plugins/github/README.md` — deliberately marketplace-agnostic — uses the `<marketplace-owner>/<marketplace-repo>` placeholder. All are still `owner/repo` shorthand, so the trigger now names the form that actually signals a non-github.com host, a **full git URL**, of which the tree has none. The verdict stays Decline, but the review surfaced a real finding that had been waved through and is now recorded in the row: a consumer redistributing the `github` plugin from a GHES-hosted marketplace would follow that README and have the shorthand silently resolve to github.com instead of their own instance. - **"Platform" was doing two jobs** (`chatgpt-codex-connector`). The existing `feature-availability` row and `docs/OFFICIAL-DOCS.md` both described that page as covering "platform, provider, and plan", while this change rests on the host axis being absent from it. Both senses of the word in one table would let a future audit read the host axis as already covered and retire the new row as redundant. The page's own sense is the **provider** platform — its axis headings are *Availability by model provider* and *Availability by subscription plan* — and both sites now say so explicitly. - **The `platforms` row claimed four evidence pages and quoted three** (`claude`). Correctly diagnosed as a missing fact rather than an overstated page: `vs-code` does carry a host-axis fact, and the most directly plugin-relevant of the four — its CLI-vs-extension table gives `Commands and skills` as `All` for the CLI against `Subset (type / to see available)` for the extension, so a skill this fleet ships may not be reachable there. It is now quoted in the row. All gates and `markdownlint-cli2` re-run clean over the changed files after these edits. CI is green, including `plugin-gate` — which runs `check-skill.test.sh`, the only executable proof that the check-5 comment insertions changed no behavior. ## Related - #2177 — established the Recorded-gate-runs table and its four-part row form; this run extends it and corrects its page census (112 → 135 core pages, ~33 → 57 uncited) - #2179 — deferred the check-5 extractor question as "a separate call"; decided here - #2181 — swept 196 skills for `Use when:` phrasing and left 4 with stated reasoning; all 4 re-reviewed and confirmed here - #2169 — the gate doc-currency audit these findings trace back to --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #2173
Summary
check-skill.shcheck 5 resolves every barecontext/…-shaped path against the citing skill'sown directory. A skill citing a sibling skill's supporting file therefore failed as a broken
internal ref — "no such file under the skill dir" — while the file plainly existed one directory
over, and the message pointed at the citing skill. The author's natural next move was to look for
the file where it could never be. That misdirection cost two round trips in a single PR (#2161),
once in each direction.
This implements issue options (2) message and (1) docs. Option (3) (narrowing extraction to
link targets) is deliberately not implemented: prose and inline-code refs are presumed-intended
coverage, and dropping them is a separate call.
Fix
Sibling-aware diagnosis (check 5). When the unresolved path also resolves under a sibling
skill of the same skills root, the finding names that sibling and the citation form that works.
Still a FAIL — the bare form really does resolve against the citing skill, so it is wrong
regardless of where the file lives; only the diagnosis gains detail.
The sibling hit is evidence, not proof. Because this check deliberately extracts prose and
inline-code refs, a generic path (
scripts/run.sh) can collide with an unrelated same-namedsibling file. The original message — including its
hand-verify the line before fixing, may be an illustrative exampleinstruction — is kept verbatim, and the sibling evidence is appendedconditionally, so both readings stay live:
Layout-aware suggestion. Which form is correct depends on the skills root, and the script's
resolution ladder deliberately bakes no layout. A plugin-shaped root gets
${CLAUDE_PLUGIN_ROOT}/skills/<sibling>/<path>; outside a plugin that variable is undefined, sothe message names the layout-free
../<sibling>/<path>instead of advertising a variable theconsumer does not have. The plugin-shaped test itself is hoisted to a single
IS_PLUGIN_SKILLcomputed once after the skill dir resolves — check 1's bare-alias branch and check 5's suggested
form now read one boolean instead of restating the same layout convention twice.
No false positives. A path no sibling hosts keeps the original message unchanged. Sibling
enumeration reuses check 3's
"$SKILLS_ROOT"/*/SKILL.mdglob (sorted, so a path present undermore than one sibling names the first deterministically) and skips the citing skill.
The two grep generators feeding the loop are untouched — extraction behavior is unchanged.
Docs (
docs/PLUGIN-PHILOSOPHY.md). Two sentences in the existing Configuration ownership andscope section, directly parallel to the
${CLAUDE_PROJECT_DIR}project-path rule already thereand to the section's own
Bundled plugin code and assets | ${CLAUDE_PLUGIN_ROOT}row. No newsection.
plugins/skill-quality/README.md's check-5 bullet and the script's own header list areupdated to match.
Verification
plugins/skill-quality/scripts/check-skill.test.sh— three new cases, all confirmed passing inCI (
plugin-gate→PASS: plugins/skill-quality/scripts/check-skill.test.sh):${CLAUDE_PLUGIN_ROOT}form, andstill carries
hand-verify the line— the caveat cannot be silently dropped again.../<sibling>/<path>form and not theplugin-root one.
sibling skilltext (false-positive guard).
while a genuinely absent ref in the same
SKILL.mdstill reports the unmodifiedbroken skill-internal ref: context/absent-file.md (…).check-shell-portability.sh,check-skill-portability.sh,check-changelog-parity.sh --check-bump,check-changed-skills.sh,check-contract-slice-prune.sh --check-diff— all pass.markdownlint-cli2clean on the changedmarkdown.
shellcheckclean andshfmt -i 2clean on both changed shell files.(where it is impractically slow); local Windows confirmation is limited to targeted smoke tests of
the two new message branches. Nothing in the change is platform-sensitive — pure bash string ops
and
-e/-ftests, nodate/readlink/GNU-only constructs — andshell-portability-lintpasses.plugins/skill-qualitybumped to0.15.1with a matching## [0.15.1]CHANGELOG entry (patch isconsistent with this plugin's history:
0.12.1shipped a whole new check as a patch).Related