The finditer fix shipped in #114 without a test that can fail — a
reversion to first-match-only scanning would stay green and quietly
undercount. One assertion closes it: a two-ref line must report 2 hits.
Verified: intact script passes; mutating scan_line to first-match-only
fails with "expected 2 hits on a two-ref line, got ['scripts/check-style.js']".
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Delivers the outstanding findings from the #110 reviews, which were posted
before merge but not addressed in it:
- FILENAME can accept path segments, so plugins/avoid-ai-writing/SKILL.md
and corpus/human/essay-01.md now block — every real file under plugins/
is nested, so that REPO_DIRS entry could previously never fire (the hole)
- right boundary: patterns.json no longer blocks via its patterns.js prefix
- left guard includes _, so my_scripts/foo.js is prose
- a markdown link with an http(s) target drops text and target together —
the error message recommends exactly that form, so the gate no longer
blocks its own advice
- finditer per line: the count and the quoted matches are now honest when
one line carries two refs
- the review's precision matrix is committed as an always-on self-test
(assert on every run; CI executes this script for the drift check, so it
rides along with no workflow change). examples/.json documented as a
deliberate pass per the review's ruling.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* check-pattern-count: assert CLAUDE.md's copy of the category count on every push
CLAUDE.md's don't-restate-it sentence quotes the README bullet, and its copy
is tracked only by .ssot.yaml, which runs on release and weekly cron - so a
same-repo edit could sit stale for up to a week (how #95's 46-vs-61 lived
unnoticed). The sed pattern mirrors .ssot.yaml's regex for the copy, so the
two guards cannot read different sentences.
Controls: clean tree passes; 61->62 mutation fails with the drift message;
a reworded sentence fails with its own message pointing at both guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwz2XfwaMD6uR5ABmVaKq
* count guard: actually trigger on CLAUDE.md edits, survive a missing file, take the first match
All three findings from the adversarial review of #111:
- HIGH: plugin-skill-sync.yml's paths filter omitted CLAUDE.md, so the
guard never ran on the edits it exists to catch (PR #106: zero check
runs). Added to both triggers.
- MEDIUM: under set -euo pipefail a missing CLAUDE.md killed the script
with sed's raw error and exit 2 before the friendly branch. Explicit -f
check with its own message and exit 1.
- LOW: greedy sed took the LAST match on a line where a first-match regex
engine takes the first; extraction is now grep -o | head -n1 (file-order
first match) and the mirror-claim comment states the semantics honestly.
Controls, exit codes checked unpiped: clean 0; mutation 1; reworded 1;
missing file 1; same-line double-mention resolves to the first match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwz2XfwaMD6uR5ABmVaKq
---------
Co-authored-by: Claude <noreply@anthropic.com>
The anchored rewrites catch EDITS to a span they already know about. A brand
new SKILL.md section that names a repo path passes them untouched and lands
in a rule users curl into a project where none of this repo's files exist.
That is how the --style section nearly shipped pointing at
scripts/check-style.js and examples/, caught by hand in #99 rather than by
the guard.
The gate scans the generated body and exits 1, naming the match and the line
number it would occupy in the rule. Two structural choices:
Only `body` is scanned. The attribution header names
scripts/sync-cursor-rules.sh and lives in cursor_fm, so it is exempt by
construction rather than by a line-range carve-out that would drift when the
header changes.
It runs before the write, so a failing generation leaves no bad artifact on
disk to be committed by accident.
Precision is the whole difficulty, since the bare directory names collide
with ordinary prose in this document. A match therefore has to name a FILE
(`scripts/check-style.js`, `examples/<name>.json`), which is what every real
reference here looks like. An earlier revision matched bare directories and
false-positived on unmodified SKILL.md: `docs/technical-blog` is a context
pair, not a path. Three further exemptions, each a precision call: a left
word boundary, so `manuscripts/x.md` stays prose; anything inside an http(s)
URL, which is the portable form and also what this gate's own message tells
an author to switch to; and README/CHANGELOG/CONTRIBUTING/CLAUDE, which
every project has and which this skill's prose already names.
Verified with a 15-case matrix. Must-pass: manuscripts/, transcripts/,
superscripts/, examples/counterexamples, docs/technical-blog, README.md,
and two GitHub URLs whose paths contain detector/ and examples/. Must-block:
the #99 near-miss shape, `examples/<name>.json`, `detector/CATEGORIES.md`,
./scripts/self-scan.js, and the bare filenames validate.js and PROOF.md.
Removing the probe line returns exit 0 and leaves the rule unchanged.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: optional --style house-style layer (config-driven, points at Vale)
Implements the config-driven design recorded in #88, which #76 and the fork
review converged on. No bundled guides: the layer applies register/voice
directives and removes AI tells on top of whatever mechanics you enforce.
--style ./house.json applies a user-supplied JSON config (register +
mechanics) and scripts/check-style.js verifies the CHECKABLE mechanics
deterministically: quotes and latinAbbrev gate the exit code (0 clean /
1 hard / 2 tool error), headings, emDash and spellNumbersUpTo are advisory,
serialComma is never checked. The docs scope the verb accordingly rather
than claiming "mechanics are verified" flatly. examples/ ships two generic,
guide-neutral starters plus the schema, and links #88 for the licensing
rule.
A bare --style "APA" is a best-effort fallback, not a feature: SKILL.md
instructs the model to print a no-compliance-claim status line and not to
reproduce the guide's text. Those are instructions rather than checked
rules, and the docs say so instead of asserting the behaviour as fact.
--style is listed in the SKILL.md invocation line and has an explicit
composition rule with --voice/--context, since the file specifies
composition for every other axis.
README: the "House style is a different job" section said "There's no
--style input", which this makes false. The first paragraph now describes
the config input and reconciles with the heading (enforcing a PUBLISHED
guide is the different job). The Vale paragraph and the heading anchor are
unchanged.
cursor-rules: the ported rule would have told a Cursor user to run
scripts/check-style.js and read examples/, neither of which ships in the
.mdc. Two anchored spans in sync-cursor-rules.sh rewrite those the way
span 3 already handles detector/validate.js, so the port stays
self-contained. Worth noting the drift guard only covers edits to anchored
spans, not newly added SKILL.md sections that introduce repo references.
The checker went through three adversarial review rounds, and each round
found the previous round's fix had introduced a new way to hard-fail a
correct document or to miss a real one. The tests pin all of them: markdown
link titles, reference definitions (including escaped quotes in a title),
HTML attribute values, nested and tilde fences, BOM'd frontmatter, URLs
containing parentheses, parentheticals that wrap or span a code block, and
a stray "(" that must not suppress later findings, and prose containing a
comparison ("n<N ... >") that must not be mistaken for an HTML tag. Masking runs refdef ->
links -> tags, in that order, because masking a link destination first
makes an ordinary "[a](url): text" line look like a reference definition.
Link masking walks to the matching paren and scans linearly; the regex form
was quadratic (22.7s on a hostile line, now 4ms). resolveConfig decides
path-vs-name by shape so a cwd file can't shadow a bundled example, the CLI
parses by position, a config with "mechanics": null or [] exits 2 rather
than a false-green 0, and advisory findings print instead of hiding behind
--json.
Version 3.22.3 -> 3.23.0 in SKILL.md frontmatter, package.json and
plugin.json; CHANGELOG entry under a dated heading; plugin and cursor
copies regenerated.
No detector changes: catalog counts unchanged (61 / 112). 32 tests in
scripts/check-style.test.js under npm test; detector-test.yml path filters
extended to scripts/** and examples/**.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: mask indented code, tighten frontmatter open, announce the resolved mode
Review fixes applied on top of the submitted branch:
- Indented code blocks (4+ spaces / tab after a blank line, outside a list)
are masked, so pasted terminal output can't hard-fail a correct document.
Lazy continuation and list-item content stay checked; a code block nested
in a list item needs a fence. Was the largest undocumented false positive.
- A document opening with a thematic break is no longer read as frontmatter
(which silently swallowed everything to the next ---).
- The bare-name traversal regex was dead code its test never reached; a bare
name is a single path segment by construction, so the guard is now a
comment stating that argument, and a test pins dot-bearing names.
- Config runs open by naming the resolved config, mirroring the fallback's
status line, so which of the two modes ran is always explicit in output.
- Known-limitations lists gain the double-backtick span and next-line
reference-definition title gaps; examples/README.md joins the self-scan
budgets; CHANGELOG updated; cursor-rule anchors follow the SKILL.md edit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com>
The Cursor port was a hand-made copy stranded at v3.16.0 while
SKILL.md reached v3.22.3 — its own README said 'no automated sync
today, open an issue if drift becomes a problem.' It did.
scripts/sync-cursor-rules.sh now generates the .mdc from SKILL.md:
Cursor frontmatter with the version derived from the SKILL.md
frontmatter, plus the three portability rewrites a copied-out rule
needs (no repo self-reference, no CATEGORIES.md citation, manual
preservation check instead of node detector/validate.js). Each
rewrite is anchored on exact upstream text and exits non-zero if the
anchor stops matching once, so an upstream edit to a ported span
breaks CI here instead of silently shipping a wrong rule.
plugin-skill-sync workflow regenerates the rule on every SKILL.md
change and fails on drift; cursor-rules/** added to its path filters
so a hand-edit to the .mdc alone can't dodge the check.
Negative control run: mutating a ported span in SKILL.md makes the
script exit 1 with an actionable message; restoring it regenerates
byte-identically (second run idempotent).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Catch promo-surface drift from the release that causes it
The pattern count and word-table count are already policed inside this repo:
check-pattern-count.sh derives both from SKILL.md and fails CI if the README
disagrees. Nothing policed the second link — four surfaces in other repos
quote those numbers, and a release moves the number here while they sit still.
That drift cannot be caught from the other side. Their files do not change on
release day, so no pre-commit hook over there ever has a commit to fire on.
It went unnoticed for three weeks: three of the four surfaces were still on
53 pattern categories and 109 table entries.
So the check runs here, on release, with a weekly backstop:
- .ssot.yaml declares the README bullets as canonical and the four surfaces
as copies (dogfoods conorbronsdon/ssot-check)
- promo-drift.yml clones the surfaces as siblings and runs the check
- promo-drift-report.py turns the JSON into a readable issue body
A surface that cannot be read is reported as NOT CHECKED and does not fail
the job — conorbronsdon-site is private, and failing red every week over a
missing token trains people to ignore the check. It is always printed. Set
SITE_REPO_TOKEN to bring it into coverage; no code change needed.
Verified against real clones of all four surfaces: in-sync exits 0, a stale
number exits 1 with file:line, an unreadable surface exits 0 and says so, and
a reworded canonical bullet is a hard failure rather than a silent pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YA7143ow5qvWZghbeQKiUd
* Drop the site from the manifest; the check now needs no credential
conorbronsdon.com/builds floors both counts instead of quoting them, so there
is nothing left to keep in sync there. That removes the only private surface,
which removes the only reason this workflow wanted a PAT.
Every tracked surface is now a public repo and the job runs on its own
GITHUB_TOKEN. The not-checked path stays — a surface repo can still be
renamed or fail to clone, and that must read as a coverage gap, not a pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YA7143ow5qvWZghbeQKiUd
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds a preservation validator, a self-scan with PROOF.md, over-correction guardrails, a false-positive intake, and a two-class control corpus with the project's first FPR, TPR, and AUC.
Splits Tier 1 into 1A frequency markers and 1B clarity edits, and fixes two detector defects the measurement found (#67, #69).
Findings and open decisions: #70, #71, #72, #73.
The README advertised a "109-entry word replacement table". The real count is 111 (59 Tier 1 + 40 Tier 2 + 12 Tier 3). Stale since 176b5a0 because nothing checked it.
Extends check-pattern-count.sh to derive the word count from the Tier 1/2/3 tables and assert it against the README bullet. Already wired into CI via plugin-skill-sync.yml.
Headers are detected positionally (the row before the |---|---| separator) rather than by label: Tier 1/2 use "| Replace | With |" while Tier 3 uses "| Word | What to do |", so label-matching silently counted two headers as data — which is how the first pass of this PR arrived at 112. Separator matching tolerates alignment colons. A per-tier non-zero assertion fails with a distinct "fix the counter, not the README" message when a heading stops matching, so a broken counter cannot enshrine a wrong number.
Comprehensive doc review after the 3.4->3.7.2 churn. The pattern count
had rotted across files (README said 42, CLAUDE.md said 36, reality is
46 detection categories in SKILL.md).
- Fix the count to 46 and keep it in ONE user-facing place (the README
"46 pattern categories" bullet). README heading reframed to "Pattern
reference" (representative examples, not an exhaustive census), and the
table extended with the 7 missing categories (placeholders, citation
markup, UTM, gap-filling, hyphenated-pair, infomercial, low-TTR).
- De-duplicate the count: CLAUDE.md and detector/CATEGORIES.md now
reference SKILL.md + CI instead of restating their own numbers.
- Add scripts/check-pattern-count.sh + wire into the CI workflow: it
derives the count from SKILL.md's detection ### entries (excluding the
3 writer-side tests) and fails if the README number drifts. Drift is
now a red check, not silent rot.
- Fix stale detector/patterns.js version comment and the marketplace.json
description (was "detection-only mode").
No SKILL.md / version change — docs + CI only.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: distribute as a Claude Code / Cowork plugin
Package the skill as a single-plugin marketplace so it can be installed
via /plugin instead of a bare clone. This is the only path that works in
Claude Cowork, which loads skills exclusively from installed plugins
(see #12).
Root SKILL.md stays the single source of truth; the plugin's bundled
copy at plugins/avoid-ai-writing/skills/avoid-ai-writing/SKILL.md is
generated by scripts/sync-plugin-skill.sh. A CI workflow regenerates it
and fails on drift or a plugin.json/SKILL.md version mismatch.
Validated with `claude plugin validate` and a full local
add-marketplace / install / inspect / uninstall cycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: parse plugin version safely in sync script
* fix: harden plugin version parsing in sync script
* harden skill-version parsing + plugin install docs
Builds on Copilot's plugin.json JSON-parser fix:
- sync-plugin-skill.sh: read the version only from the SKILL.md
frontmatter block (not anywhere in the file), strip CR, and fail with
a clear message if it's unparseable — closes the loose/false-green
side of the version check that the JSON parser already closed for
plugin.json.
- .gitattributes: pin *.md to LF so a CRLF checkout can't forge a
version mismatch or mask skill drift.
- README: add /reload-plugins to the install steps (skills don't
activate until reload/restart) and note updates are version-gated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>