Skip to content

fix(repo): add pull_request_template scaffolding ## Related contract#490

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/462-pr-template
Jul 19, 2026
Merged

fix(repo): add pull_request_template scaffolding ## Related contract#490
kyle-sexton merged 2 commits into
mainfrom
fix/462-pr-template

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #462

Summary

PRs fail the pr-issue-linkage CI check because no .github/pull_request_template.md existed to scaffold the ## Related section and native closing keyword the check requires. Authors discovered the contract only after a red CI run. This adds the template so web/editor authors scaffold a passing body up front, and documents the contract in one place.

Fix

Add .github/pull_request_template.md scaffolding, in order, a Closes # closing-keyword placeholder plus ## Summary / ## Fix / ## Verification / ## Related. Section-level guidance lives in HTML comments. The reusable validator (melodic-software/ci-workflows pr-issue-linkage.yml, pinned at 90f1c54) is the single source of truth — the template only mirrors the exact ## Related heading and the closing-keyword contract it enforces; no validator logic is duplicated.

Scope note on accuracy: this does not retroactively fix the agent-authored PRs that failed this check. Those used gh pr create --body "...", which fully overrides .github/pull_request_template.md (cli/cli#10751), so templates never applied to them. This template helps human/web/editor authors and documents the contract; the agent path is covered separately by front-loading the contract into worker briefs, and the /source-control:pull-request create-flow half is tracked in #487.

By design, an unedited template does not pass the check: the validator strips HTML comments before validating (its explicit anti-"vacuous pass" guard), so the placeholders must be filled with real linkage before a PR goes green.

Verification

Repo-pinned gates run against the new file — all clean:

  • markdownlint-cli2 v0.23.1 (config .markdownlint-cli2.jsonc, schema pinned v0.23.0): Summary: 0 issues.
  • editorconfig-checker v3.8.0 (config .editorconfig-checker.json): exit 0, no output.
  • typos v1.44.0 (config _typos.toml): exit 0, no output.

Mirror against the validator's own regex (copied verbatim from the pinned reusable and run locally):

  • Raw template as-is → ["## Related" empty, "Missing closing keyword"] — correctly fails-until-filled (comments stripped, no #N digit), matching the check's design.
  • A filled copy (Closes #462 + ## Related bullets) → [] — clean pass.
  • Heading name: /^##\s+related$/i matches ## Related; rejects ### Related and ## Related Issues.

This PR body itself demonstrates a passing body: it carries Closes #462 and a non-empty ## Related section.

Related

🤖 Generated with Claude Code

Add .github/pull_request_template.md scaffolding the sections the
pr-issue-linkage check enforces: a native closing-keyword placeholder
(Closes #) plus ## Summary / ## Fix / ## Verification / ## Related, with
guidance in HTML comments. The validator (ci-workflows reusable) is the
SSOT; the template only mirrors its required ## Related heading and
closing-keyword contract so web/editor authors scaffold a passing body
without a red-CI round-trip. Comments are stripped by the validator, so
an unedited template correctly fails until filled — matching the check's
explicit anti-vacuous-pass design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 20s —— View job


PR Review

  • Load tools
  • Read the changed file
  • Review for correctness, security, and CLAUDE.md alignment
  • Post findings

Summary: This is a clean, well-scoped addition. The template correctly mirrors the validator contract and the anti-vacuous-pass guard works as designed. No blocking issues found.


Correctness

Contract alignment is accurate. The template's ## Related heading matches the validator's regex /^##\s+related$/i exactly. The bare Closes # (no digit) will fail the closing-keyword check after comment stripping, as confirmed in the verification section — intentional and correct.

Anti-vacuous-pass guard holds. Both failure modes (empty ## Related + missing closing keyword digit) survive comment stripping. An unedited template correctly fails; a properly filled one passes. The verification evidence in the PR body is thorough and trustworthy.

"No linked issue" escape hatch is documented. The guidance comment on the closing-keyword block describes the fallback string correctly. One gap: the ## Related section comment doesn't address what to put there when there's no linked issue. The validator still requires a non-empty ## Related even for "No linked issue" PRs — an author following the guidance comment might stop after replacing the closing keyword line and leave ## Related empty.

🟡 Nit — .github/pull_request_template.md L32–36: Consider adding a note in the ## Related comment that ## Related is still required non-empty even when using "No linked issue" (e.g. N/A or a self-reference). Without this, the "no linked issue" path is under-documented relative to the normal path.


Security

No credentials, tokens, or executable code introduced. The template is inert markdown; no attack surface.


CLAUDE.md / REVIEW.md alignment

All six blocking criteria (object-level authz, tenant scoping, secrets, injection, audit logging, atomicity) are inapplicable to a Markdown file — none triggered.

The "do not report" clause correctly suppresses CI-enforced items; markdownlint, editorconfig, and typos results are all claimed clean and not flagged here.


Verdict: Approve. The single nit above is non-blocking. The template adds clear value by surfacing the contract up-front and the design (fail-until-filled) is sound.

The pull_request_template ## Related guidance only showed the linking
case (Refs #N), leaving the "No linked issue" path under-documented: an
author could replace the closing keyword and leave ## Related empty,
which the pr-issue-linkage validator rejects (its only failure mode is
an empty section). Note that the section stays required and to use N/A
when nothing applies. Comment-only; the validator strips HTML comments,
so the template's fail-until-filled behavior is unchanged.

Addresses the claude[bot] review nit on #490.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 4s —— View job


PR Review (re-review after synchronize)

  • Load tools
  • Read the changed file and diff
  • Review for correctness, security, and CLAUDE.md alignment
  • Post findings

The synchronize pushed one new commit (73e894f) that responds directly to the nit raised in the previous review. Here's the updated assessment.


What changed

The second commit (73e894f docs(repo): document ## Related non-empty rule on no-linked-issue path) added three lines to the ## Related HTML comment:

Required non-empty even when the closing-keyword line above is `No linked issue`
— if nothing applies, put `N/A`. Use `Refs #N` to link an issue without closing
it.

These appear at L35–L38.


Correctness

Previous nit: resolved. The gap identified in the first review — the ## Related comment not explaining what to do on the "no linked issue" path — is now closed. Authors following the template guidance will see:

  1. Line 16: replace Closes # with No linked issue when no issue exists.
  2. Lines 35–36: ## Related is still required non-empty; N/A is the documented fallback.

Both failure modes (empty ## Related + missing digit in closing keyword) continue to survive comment stripping as designed. An unedited template still fails; a properly filled one passes. The contract is now fully documented in one place.

Refs #N hint is correct. The addition of Use \Refs #N` to link an issue without closing itis accurate —Refs/References` are the conventional non-closing link keywords GitHub recognizes. This is useful guidance that wasn't present before.


Security

No change to the security posture — still inert markdown, no executable code, no credentials.


CLAUDE.md alignment

The file is a repository governance artifact, not a plugin, so plugin-specific rules (repo-agnostic paths, ${CLAUDE_PLUGIN_ROOT}, versioning) don't apply. The PR correctly targets the repo and does not touch plugin manifests or marketplace config.


Verdict

Approve. The previous nit is addressed and no new issues are introduced. The template is clean, the contract is fully documented, and the anti-vacuous-pass guard remains intact.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Addressing the claude[bot] review (run 29693725085).

# Finding Classification Evidence Reacted
1 🟡 Nit (L32–36): ## Related comment doesn't note the section stays required non-empty on the "No linked issue" path; an author could fill only the closing keyword and leave ## Related empty VALID — fixed Confirmed against the pinned validator (ci-workflows pr-issue-linkage.yml@90f1c54): extractSection(body, /^##\s+related$/i) fails only when related.length === 0, so any non-whitespace content (e.g. N/A) satisfies it. The prior comment showed only the linking case (Refs #N), leaving the no-issue path under-documented. Fixed in 73e894f: the comment now states the section stays required even when the closing-keyword line is No linked issue, and to use N/A when nothing applies. Comment-only — the validator strips HTML comments, so fail-until-filled is unchanged. Repo gates re-run clean on the edited file (markdownlint-cli2, editorconfig-checker, typos). 👍

Verdict was Approve (nit non-blocking); the single nit is now closed. No thread to resolve — this is an issue-level review comment, not an inline thread.

@kyle-sexton
kyle-sexton merged commit df04210 into main Jul 19, 2026
15 checks passed
@kyle-sexton
kyle-sexton deleted the fix/462-pr-template branch July 19, 2026 17:04
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
#593)

## Summary

This repo edits its own skills daily via the autonomous pipeline, yet no
CI lane invoked the `skill-quality` checker — 120 `evals/evals.json`
went unexecuted and the 17-check static gate (`skill-quality:check`) was
invoked by nothing. This wires that checker into CI as a deterministic
skill-regression net.

Implements **stage 1 only** of #530 (the "cheap, now" deterministic
lane). Stages 2-5 (model-graded judges, frozen regression suite, pass^k
autopilot-unlock metric, clean-state isolation) remain a separate
decompose target per the ratified operator decision on the issue.

## Fix

New `skill-quality-gate` job in `.github/workflows/ci.yml`, added to the
`ci-status` required aggregate:

- **Static gate (`scripts/check-changed-skills.sh`)** — on a PR, for
each skill under `plugins/*/skills/**` the diff touches, runs
`plugins/skill-quality/scripts/check-skill.sh` with
`CHECK_SKILL_SKILLS_ROOT` set to the owning plugin's `skills/` dir and
`CHECK_SKILL_BASE_REF` set to the PR base. Because the checker's
trigger-keyword-preservation check diffs against that base, a rewrite
that silently drops a `description` trigger phrase (an auto-invocation
regression) fails the check — replacing the work lane's manual
high-blast-radius diff read with a deterministic one. Vendor subtrees
map to the owning skill; deleted skills are filtered; any FAIL (or env
error) exits non-zero. `markdownlint` (check 6) is deliberately skipped
here (`CHECK_SKILL_SKIP_MARKDOWNLINT=1`) — SKILL.md markdown is already
gated by the hygiene lane.
- **validate-evals** — the existing `check-jsonschema` composite action
schema-validates every `evals/evals.json` against the bundled
`plugins/skill-quality/reference/evals.schema.json`.
- **Self-test (`scripts/check-changed-skills.test.sh`)** — runs first so
a broken orchestrator cannot mask a real regression behind a green gate.

**No silent-skip (#445 precedent):** the job itself never skips (a
skipped required job reports success to branch protection) — only the
inner PR-diff step is event-gated (mirroring `hook-utils-sync`'s
`--check-bump`), and the self-test + eval-schema steps give push-to-main
a passing path. Missing base ref or missing checker fails closed (exit
2).

**No new reusable action:** reuses this repo's own local script plus the
already-referenced `check-jsonschema` composite action — no change to
`melodic-software/ci-workflows`.

**No plugin modified → no version bump** (consistent with CI-only PR
#490, which bumped nothing). No plugin frontmatter, triggers, hooks, or
cross-plugin contracts are touched.

## Verification

Local, in the worktree:

- **Orchestrator unit tests** — `bash
scripts/check-changed-skills.test.sh` → `PASS=8 FAIL=0` (covers
changed-skill detection, vendor-subtree mapping, dedupe, deletion
filtering, env passthrough, pass/fail aggregation, fail-closed on bad
base ref / missing checker).
- **ShellCheck** — `shellcheck --rcfile .shellcheckrc
scripts/check-changed-skills.sh scripts/check-changed-skills.test.sh` →
clean.
- **End-to-end, real `check-skill.sh` against real skills**
(`scripts/check-changed-skills.sh origin/main`):
  - Happy path (a changed skill with no regression) → `PASS`, exit 0.
- Simulated regression (dropped the `'lint my skill'` trigger from
`skill-quality/check`) → `FAIL: dropped trigger keyword(s) vs
origin/main (auto-invocation regression): 'lint my skill'`, exit 1.
- **validate-evals** — `check-jsonschema --schemafile
plugins/skill-quality/reference/evals.schema.json` over all
`plugins/*/skills/*/evals/evals.json` → 120/120 pass today (glob matches
exactly the 120 eval sets, excludes vendor).

Closes #603

## Related

- #530 — this PR is stage 1 of the staged quality-net umbrella; stages
2-5 stay a decompose target.
- #445 — mechanical-conformance CI backlog; this extends gating from
conformance to behavior and follows its silent-skip-gate anti-pattern
warning.
- #477-#480 — the rule→skill absorption lifecycle whose regression net
this begins to supply.
- #513 — the mini-SDLC verify lane that consumes these gates.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
#617) (#622)

Closes #617

## Summary

Docs-only PRs — operator precedent codifications and other
`docs/topics/` churn, a large share of current throughput per #617 — ran
the full plugin contract suite (`plugin-gate`: Node + Python installs,
every `plugins/**/*.test.sh`, manifest + catalog validation) and the
full `miro-plugin` Node build for a diff that cannot affect either. Both
lanes now short-circuit their expensive steps when the diff is confined
to a small, positive, provably-inert allowlist, reporting an honest
evaluated-and-not-applicable success.

## Fix

**Mechanism — in-job detection, never a job skip.** GitHub's docs are
explicit: *"If a workflow is skipped due to path filtering, branch
filtering or a commit message, then checks associated with that workflow
will remain in a 'Pending' state. A pull request that requires those
checks to be successful will be blocked from merging."* whereas *"If,
however, a job within a workflow is skipped due to a conditional, it
will report its status as 'Success.'"*
([troubleshooting-required-status-checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks)).
So workflow-level `paths:`/`paths-ignore:` is ruled out for required
checks. It is doubly ruled out here: the sole required check is the
`ci-status` aggregate, which fails unless every lane's `result` is
exactly `success` — a conditionally-*skipped* job's result is `skipped`,
not `success`, so even a job-level `if:` would red the aggregate. The
safe pattern is therefore the one #593/#609 already use: the job always
runs, self-tests its detector first, and gates only its inner expensive
steps, staying `success` and reporting to branch protection either way.

New files:

- **`scripts/check-docs-only.sh`** — emits `docs_only=true|false` to
`$GITHUB_OUTPUT`; `true` iff every path changed vs the base ref matches
an allowlist prefix. Fail-closed toward *running* the full suite: an
unresolvable base ref, an empty/missing allowlist, or an empty diff all
emit `false` with a stderr note and exit 0 (a code PR is not an error —
"has code" is never encoded as a non-zero exit that would red every code
PR).
- **`scripts/docs-only-paths.txt`** — the allowlist as external,
extensible **data** (same shape as #609's token list). **Allowlist, not
blocklist, by design**: a blocklist ("skip on any doc the validators
don't currently read") silently turns false-green the day a validator
starts reading a new doc; a positive list fails safe — any unlisted
path, including a brand-new code input, forces the full suite. Seeded
with `docs/topics/` only. Adding an entry is a two-part proof (inert + a
self-test case).
- **`scripts/check-docs-only.test.sh`** — the honesty proof, run
unconditionally in both wrapped jobs before anything expensive.

In `ci.yml`, `plugin-gate` and `miro-plugin` each gain: `fetch-depth:
0`, an always-run detector self-test, a PR-only detect step (`id:
scope`), an `if: steps.scope.outputs.docs_only != 'true'` guard on every
install/test step, and an explicit *"not applicable to a docs-only diff
— reporting success"* log step. On `push` the detect step doesn't run,
the output is empty, and every real step runs — main always gets the
full suite.

**Job classification** (only jobs that *cannot* be affected by a
docs-only diff are scoped; when unsure, not scoped):

| Lane | Scoped? | Rationale |
|---|---|---|
| `plugin-gate` | **Yes** | Heaviest lane. `run-plugin-tests.sh` is
hermetic (tests build their own fixture repos); `validate-plugins.sh`
reads only `README.md`, `docs/PLUGIN-ARTIFACT-PROTOCOL.md`,
`docs/CATALOG-TAXONOMY.md` (all excluded from the allowlist) +
`plugins/**` + toolchain files. A `docs/topics/`-only diff touches none
of these. |
| `miro-plugin` | **Yes** | Every step is `working-directory:
plugins/miro`; a docs-only diff provably cannot touch it. |
| `hygiene` | No | Markdownlint / typos / comment-hygiene **do** read
the changed docs — this is the lane that *should* run on a docs PR. |
| `skill-quality-gate` | No | Already self-scopes to changed skills
(empty set on a non-skill docs diff); eval-schema step is cheap. |
| `zizmor` | No | Advisory reusable workflow; scoping a reusable call
partly belongs to `ci-workflows`. |
| `hook-utils-sync`, `standards-contract-sync`,
`cross-plugin-source-drift`, `silent-skip-gate`, `runner-policy` | No
(deferred) | Provably docs-inert, but off the critical path (fast bash
tests, no heavy installs) — wrapping them adds surface for negligible
latency gain. Trigger to revisit: if any becomes install-bearing. |

**Deferred with trigger:** scoping `miro-plugin` to `plugins/miro/**`
specifically (skipping it on *any* non-miro diff, not just docs-only) is
a larger, separate optimization broader than #617's docs-only framing;
trigger: a dedicated follow-up issue for per-lane path scoping.

## Verification

In the worktree (`ubuntu`/Git Bash):

- **`bash scripts/check-docs-only.test.sh` → `PASS=16 FAIL=0`.** Cases
pin: `docs/topics/`-only (incl. new nested files) → `true`; `README.md`,
`docs/PLUGIN-ARTIFACT-PROTOCOL.md`, `docs/CATALOG-TAXONOMY.md` →
**`false`** (the grep payoff — these are consumed by `plugin-gate`); any
non-topics `docs/**`, any `plugins/**` incl. `SKILL.md`, `scripts/`,
`.github/`, toolchain lockfiles, a sibling prefix
(`docs/topics-archive/`), and mixed docs+code → `false`; unresolvable
base ref → `false` exit 0; empty allowlist → `false`; `$GITHUB_OUTPUT`
written.
- **`shellcheck --rcfile .shellcheckrc`** on both scripts → clean.
**`shfmt -d`** → no diff. **`actionlint .github/workflows/ci.yml`** →
clean.
- **Provenance of the "inert" claim:** `git grep` over
`plugins/**/*.test.sh`, `validate-plugin-contracts.mjs`,
`generate-catalog.mjs`, and the manifests confirmed
`generate-catalog.mjs --check` consumes `README.md` and
`docs/CATALOG-TAXONOMY.md` and `validate-plugin-contracts.mjs` requires
`docs/PLUGIN-ARTIFACT-PROTOCOL.md` — which is exactly why those are
excluded from the allowlist and the docs-only win is
`docs/topics/`-scoped, not "all markdown".
- **zizmor:** base ref flows via `env: BASE_REF` (no `${{ }}`
interpolation inside `run:`), matching the existing
`hook-utils-sync`/`skill-quality-gate` steps; `persist-credentials:
false` retained; no new permissions.

Honest scope note: the win is narrow by construction — it covers
`docs/topics/`-only PRs and deliberately does **not** cover
README/philosophy edits, `SKILL.md` edits (code), or plugin-`CHANGELOG`
edits (code). Narrow-but-sound is the point.

## Related

- #532 — false-green / liveness-assertion convention this honors
(evaluated-and-not-applicable success, not a silent skip).
- #593 — the merged `skill-quality-gate` whose never-skip,
self-test-first, event-gated job shape this mirrors.
- #609 — open PR also touching `ci.yml` (adds the `portability-lint`
lane + one line to the `ci-status` needs list). Different file regions
from this PR (new job block + needs line vs. the
`plugin-gate`/`miro-plugin` step bodies); composes without conflict.
Whichever merges second rebases.
- No `plugins/<name>/` directory is touched, so no version bump /
CHANGELOG entry (matching CI-only precedent #490/#593).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…y diffs (#628) (#659)

## Summary

Docs-only PRs still paid the `hygiene` job's path-scoped linter cost —
`actionlint` and four `check-jsonschema` steps — for a diff that
provably cannot affect any of them. #617 (via #622) short-circuited
`plugin-gate` and `miro-plugin`, which cut runner-minutes but not the
merge-latency bottleneck. This applies the SAME self-test-first,
fail-closed, inner-step-gated discipline to `hygiene`'s genuinely
path-scoped subset, so those runs report an honest
evaluated-and-not-applicable success instead of running.

`ShellCheck` and `exec-bit` are deliberately NOT gated: both scan the
whole repo extension- and directory-agnostically (`ShellCheck` lints
every tracked `*.sh`/`*.bash` via `git ls-files`; `exec-bit` flags every
tracked shebang file recorded `100644` via `git grep -- .`), so a shell
or shebang file added under an otherwise docs-only prefix like
`docs/topics/` is real input they must still catch. They run on every
diff alongside the docs-linting checks that read the changed docs
directly: `markdown`, `typos`, `editorconfig`, `gitleaks`,
`eol-renormalize`, `comment-hygiene`, `machine-specific-paths`.

> **Scope of the perf win (honest):** the initial revision of this PR
also gated `ShellCheck` (~53s, the dominant cost) and `exec-bit`. Bot
review (P2, thread on `ci.yml`) surfaced that both are whole-repo
scanners, so gating them opened a **fail-closed hole** — a docs-only PR
adding a mode-`100644` shebang file (or a `*.sh`) under `docs/topics/`
skipped both and the ternary mapped them to `success`, letting a file
the commit procedure requires `exec-bit` to catch merge unlinted.
Ungating them (commit `f73be27`) closes the hole but removes the
dominant ShellCheck skip, so the remaining docs-only win is limited to
`actionlint` + the four `check-jsonschema` steps. A perf-recovery
follow-up (cache/pin the ShellCheck install, which downloads a release
tarball every run) is tracked separately and lives in
`melodic-software/ci-workflows`, not this repo.

## Fix

Reuses the existing `scripts/check-docs-only.sh` detector and
`scripts/docs-only-paths.txt` allowlist from #622 unchanged — no new
detector.

In the `hygiene` job (`.github/workflows/ci.yml`):

- Added the always-run `Test the docs-only detector` self-test and a
PR-only `Detect a docs-only diff` step (`id: scope`), mirroring
`plugin-gate`/`miro-plugin`. The checkout already had `fetch-depth: 0`,
so the base ref resolves.
- Gated exactly the five path-scoped steps with `if:
steps.scope.outputs.docs_only != 'true'`: `actionlint`,
`marketplace_schema`, `plugin_schema`, `dependabot_schema`,
`workflow_schema`. `shellcheck` and `exec_bit` stay unconditional.
- The job never skips and the aggregator is never weakened. The
`hygiene` job uses `continue-on-error: true` per step plus a
`CHECK_RESULTS` feed into `scripts/aggregate-hygiene-results.sh`, which
fails closed on any non-`success` outcome (including `skipped`). A gated
step that skips would therefore red the job — so each gated entry in
`CHECK_RESULTS` maps the intentional docs-only skip to `success` in the
expression layer:

  ```yaml
actionlint=${{ steps.scope.outputs.docs_only == 'true' && 'success' ||
steps.actionlint.outcome }}
  ```

This is honest, not a swallow: the ternary condition (`docs_only ==
'true'`) is the exact inverse of each step's gate, so `success` is fed
ONLY for a step that provably did not run — no real outcome is ever
masked. The two whole-repo scanners (`shellcheck`, `exec-bit`) feed
their raw `.outcome`. The aggregator script is untouched and still fails
closed on any real `failure` (or a `skipped` it isn't told to tolerate).
This is the same evaluated-and-not-applicable success `plugin-gate`
reports with its log step, expressed here through the feed the hygiene
job already uses.
- Added an explicit `Report docs-irrelevant checks not applicable` log
step (gated on `docs_only == 'true'`) so a human reading a green run
understands why those checks show skipped.

Also extended the two non-workflow artifacts per the established
two-part-proof discipline:

- `scripts/docs-only-paths.txt` — updated the inertness-proof comment to
name only the genuinely path-scoped gated subset (`actionlint` + the
four schema checks, none of which read `docs/topics/`) and to state
explicitly that `ShellCheck` and `exec-bit` scan the whole repo and stay
unconditional (as do the docs-linters). Allowlist itself unchanged
(still `docs/topics/` only — no growth).
- `scripts/check-docs-only.test.sh` — added three cases pinning the
remaining `check-jsonschema` inputs to `false`
(`.claude-plugin/marketplace.json`,
`plugins/*/.claude-plugin/plugin.json`, `.github/dependabot.yml`); the
`actionlint` / workflow-schema inputs (`.github/workflows/ci.yml`) were
already pinned false. These fail the moment someone widens the allowlist
to cover an input a gated check reads.

## Verification

Run in the worktree (Git Bash):

- **`bash scripts/check-docs-only.test.sh` → `PASS=19 FAIL=0`.**
Confirms `docs/topics/`-only → `true` and every gated check's input
(`.github/workflows/ci.yml`, `.claude-plugin/marketplace.json`,
`plugins/*/.claude-plugin/plugin.json`, `.github/dependabot.yml`,
lockfiles) → `false`.
- **`scripts/aggregate-hygiene-results.sh --self-test` → `Hygiene result
aggregation contract passed.`** The aggregator is unchanged and still
fails closed on a named empty/`failure`/non-`success` outcome.
- **`actionlint .github/workflows/ci.yml` → clean.** Validates the five
added gate conditions and the five ternary expressions in
`CHECK_RESULTS`.
- **`shellcheck --rcfile .shellcheckrc` and `shfmt -d`** on the
edited/reused scripts → clean, no diff.

Honest scope of what this PR's own CI proves: this PR touches `.github/`
and `scripts/`, so on it `docs_only=false` and the full hygiene suite
runs — a live proof only of the run-path (nothing is wrongly skipped on
a code diff). The skip-path is not live-observable here by construction;
it is proven by the detector unit test (`docs/topics/`-only → `true`)
plus the gate/expression logic being the exact inverse of each other.
Correctness of the intentional-skip → `success` mapping rests on that
unit test and the expression, not on a fabricated observation.

**Versioning/changelog:** none. No `plugins/<name>/` directory is
touched — this is a repo-root CI-only change — so per the precedent #622
followed (matching CI-only #490/#593) there is no plugin version bump or
CHANGELOG entry.

Closes #628

## Related

- #628 — this issue.
- #617 (closed) / #622 (merged) — the established precedent this
extends: same `scripts/check-docs-only.sh` detector, same
self-test-first / fail-closed / inner-step-gated discipline, applied to
`hygiene` instead of `plugin-gate`/`miro-plugin`.
- #532 — the false-green / liveness class this must not reintroduce. A
job-level skip (or teaching the aggregator to pass on `skipped`) is
exactly that shape and is deliberately avoided: the job always runs, the
detector self-tests first, and only inner steps are gated with their
skip reported as an honest not-applicable success.

🤖 Generated with a Claude Code implementation subagent (issue #628)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

source-control/repo: PRs fail pr-issue-linkage (missing ## Related) — no template scaffolds the required sections

1 participant