Skip to content

feat(scripts): gate the de-slopped surfaces against em-dash regression - #3344

Merged
kyle-sexton merged 4 commits into
mainfrom
feat/2891-em-dash-purged-paths-gate
Aug 26, 2026
Merged

feat(scripts): gate the de-slopped surfaces against em-dash regression#3344
kyle-sexton merged 4 commits into
mainfrom
feat/2891-em-dash-purged-paths-gate

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

No linked issue

Summary

The de-slop campaign (#2891) has rewritten prose surface by surface for a week, and each shard is paid for by hand: a mechanical em-dash split changes meaning often enough that every shard so far has needed a rationale-withheld reviewer to catch clauses the automated passes waved through. Nothing then stopped the next contributor from reintroducing one, because no lane enforced the policy. This adds the ratchet, so that what the campaign has already cleaned stays clean.

scripts/check-purged-em-dashes.sh enforces zero em dashes on the paths declared in scripts/em-dash-purged-paths.txt, and runs unconditionally in the hygiene job.

Enforcement is scoped to an allowlist rather than the corpus, and that is the design rather than a concession. Roughly 29,600 em-dash prose lines across 1,074 tracked markdown files remain, so a repo-wide check could only have been merged switched off. Declaring what is already clean inverts the problem: adoption breaks nothing, because every listed path passes the moment it is added, and enforcement grows with the campaign instead of waiting for it. It is the same positive-list shape, and the same fail-safe direction, as scripts/docs-only-paths.txt.

The allowlist currently covers ten plugins. Sixty-nine have had instruction surfaces rewritten by campaign shards, so the gate defends a fraction of what has been cleaned; the backfill and what blocks it are tracked in #3342.

Fix

  • scripts/check-purged-em-dashes.sh drives plugins/ai-slop/skills/audit/scripts/detect.sh rather than grepping, so fenced code blocks, inline code spans, and ignore-marked regions keep carrying the character as data. The tracked .claude/ai-slop.json is not modified; the gate copies it into a throwaway layer for its own run. That layer strips every switch that can quiet rule-em-dash on a declared path: the disabled_rules entry, em_dash_allowed_paths, and rule_allowed_paths["rule-em-dash"]. A path on the allowlist declares the surface purged, and a per-rule exemption on that path is the opposite claim; honoured, it would let the gate report the surface checked and clean while no finding on it was ever possible, because such a file is still opened and still counted as scanned. excluded_paths and every threshold are left standing, including the vendor, catalog and eval-fixture paths that hold em dashes deliberately: those files are never opened at all, so they stay visible as excluded-glob declines that the verdict names and fails on. A declared file that genuinely carries the character as data still has the detector's in-file exemptions, and failing those it belongs off the allowlist with the reason. HOME is emptied so a contributor's user-global config cannot move the verdict.
  • Because that detector exits 0 on every audit path, the gate believes a run only when the rule-em-dash summary is present, reports disabled=0, and accounts for exactly the files handed to it. Any other shape is exit 2 rather than a false green, and a stale allowlist entry matching no tracked file fails for the same reason. A declared path the tracked config puts out of enforcement through excluded_paths fails too, at exit 1 with a message distinct from a findings failure: the detector never read it, so declaring it enforces nothing, and a green exit is how that gets mistaken for coverage.
  • scripts/em-dash-purged-paths.txt seeds the declaration. Every entry meets a stricter standard than the gate enforces, containing no em dash anywhere rather than none in prose, so the whole list can be audited by expanding it with --list and grepping the named files.
  • .github/workflows/ci.yml runs the self-test first and ungated, so a broken gate cannot mask a regression, then the gate itself, whose outcome reaches the ci-status aggregate through aggregate-hygiene-results.sh. The step is deliberately not gated on run_full: it reads markdown prose, which is exactly what a docs-only diff changes.

Verification

An independent fresh-context reviewer audited the change with the authoring rationale withheld and returned two defects, both fixed in a1b64959c with regression cases that fail against the previous code: --list ran the whole detector rather than stopping at expansion, contradicting the allowlist header's own audit instructions; and the coverage assertion added two overlapping detector totals, so an allowlisted file carrying an ai-slop-ignore-file marker failed the run at exit 2 for using an exemption the allowlist promises.

  • Seeded a real em dash into a covered path (plugins/naming/README.md): exit 1, naming file and line. Reverted: exit 0.
  • scripts/check-purged-em-dashes.test.sh: PASS=21 FAIL=0. The suite drives the real detector rather than a stub, plants a violation and requires exit 1, and separately plants em dashes in a code fence and an ignore-marked line and requires silence.
  • scripts/check-purged-em-dashes.sh: 21 declared paths, 30 files scanned, exit 0. No declared path overlaps the tracked config's excluded_paths, so the new failing verdict does not fire on this repository today.
  • scripts/check-purged-em-dashes.sh --list: 55 seconds, down from roughly nine minutes.
  • git ls-tree HEAD confirms both scripts tracked 100755; the workflow invokes the gate as a bare path.
  • shellcheck -x, shfmt -d -i 2, and actionlint clean.
  • check-changelog-parity.sh --check-bump origin/main, check-silent-skips.sh, check-discriminating-test-skips.sh, check-orphaned-fixtures.sh, and check-docs-only-gate.sh --check all pass. No plugin version changes, so no changelog entry is due.

Related

Refs #2891. This shard does not close it. The campaign's second checkbox, carrying the purge through the remaining corpus, is nowhere near done, and its fourth, tightening .claude/ai-slop.json once the purge completes, is explicitly gated on that. A gate over already-cleaned paths is the ratchet the campaign was missing, not its completion.

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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


Code review complete

  • Gather context (PR body, changed files, CLAUDE.md/REVIEW.md)
  • Review scripts/check-purged-em-dashes.sh
  • Review scripts/check-purged-em-dashes.test.sh
  • Review scripts/em-dash-purged-paths.txt
  • Review .github/workflows/ci.yml diff
  • Post findings

This lane covers correctness/maintainability only (claude-security-review.yml exists in this repo, so security findings are out of scope here per REVIEW.md).

Findings

Posted one inline finding on scripts/check-purged-em-dashes.sh (lines 213-248, permalink):

Coverage-assertion gap: em_dash_allowed_paths isn't watched the way excluded_paths is. The gate's "believe the run only if it proves it happened" section explicitly detects and warns when a declared-purged path is excluded from all scanning via the tracked config's excluded_paths (the exact defect class fixed for excluded_paths in a1b64959c). The sibling exemption em_dash_allowed_paths, which detect.sh also honors for rule-em-dash specifically, has no equivalent signal: detect.sh skips the rule for a matching file with no per-file Declined: row (only a silent bump to the rule's aggregate declined= count), so the gate's scanned/excluded/handled arithmetic can't see it and the coverage check passes. Currently unreachable (the tracked .claude/ai-slop.json carries no em_dash_allowed_paths entries today), but if one is ever added that overlaps an allowlisted path, that surface would report clean while the one rule this gate exists for is silently skipped on it — no warning, unlike the excluded_paths case. Full detail and a suggested fix direction are in the inline comment.

Other things checked, no issues found

  • check-purged-em-dashes.sh's coverage arithmetic (scanned + excluded == EXPECTED) is otherwise sound, including the fix in a1b64959c for the double-counted ai-slop-ignore-file-marked file case — traced through detect.sh's TOTAL_FILES/DECLINED_FILES bookkeeping and it holds.
  • CLAUDE_PROJECT_DIR="$TMP/root" correctly redirects detect.sh's config cascade onto the throwaway layer while the relative file paths passed via --paths-file still resolve against the real repo root (script never cds away from $ROOT).
  • .github/workflows/ci.yml: the new self-test + gate steps mirror the existing hook_wiring pattern exactly (ungated self-test, continue-on-error gate step with id, wired into aggregate-hygiene-results.sh via CHECK_RESULTS), no id collisions, and the docs-only-diff report step's not-applicable message was updated to include the new gate.
  • File modes: confirmed via git diff (new file mode 100755) for both new scripts.
  • scripts/em-dash-purged-paths.txt glob entries use git's :(glob) pathspec magic correctly (won't cross directory separators).
  • Test suite (check-purged-em-dashes.test.sh) assertion count (17 ok call sites) matches the PR's claimed PASS=17 FAIL=0, and its scenarios exercise the meaningful edge cases (seeded violation, code-fence/ignore-marker data, whole-file marker interaction, stale/empty/missing allowlist, --list stopping before the detector, unknown args).

Not independently verified: this environment blocked execution of check-purged-em-dashes.test.sh / check-purged-em-dashes.sh (shell scripts required interactive approval unavailable here), and I did not run shellcheck/shfmt/actionlint. The PR body's test output and lint results are author-claimed, not independently confirmed by this review — everything above was verified by static reading of the diff and the (unchanged) detect.sh it drives.

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 7s —— View job


Security review

  • Gather context (PR diff, changed files, CLAUDE.md rules)
  • Run skip-gate check
  • Review scripts/check-purged-em-dashes.sh
  • Review .github/workflows/ci.yml changes
  • Review scripts/em-dash-purged-paths.txt and test script

No security issues found in this PR.

Scope reviewed: the new CI gate (scripts/check-purged-em-dashes.sh), its self-test (scripts/check-purged-em-dashes.test.sh), the seed allowlist (scripts/em-dash-purged-paths.txt), and the .github/workflows/ci.yml wiring that invokes them, per a1b6495.

What I checked for and didn't find:

  • Trust boundary / secret exposure: the job trigger is plain pull_request (not pull_request_target) with workflow-level permissions: contents: read (ci.yml#L6-L9), unchanged by this PR. The new steps run fixed script paths with no ${{ }} expression interpolated into a run: shell body, so there's no script-injection surface via the github context even though this lane executes on fork PRs.
  • Command/argument injection: check-purged-em-dashes.sh passes allowlist entries to git ls-files -- ":(glob)$glob" as a pathspec argument (not through a shell), and hands detect.sh a --paths-file rather than filenames on the command line. Pathspec magic (:(glob)) is only recognized at the very start of a spec, so an entry can't smuggle a second magic signature.
  • Config/environment isolation: the throwaway detector config is built by jq from the tracked .claude/ai-slop.json into a temp dir rather than mutating the tracked file, and HOME is pointed at an empty temp directory before invoking detect.sh (check-purged-em-dashes.sh#L246-L265), so a contributor's user-global config can't influence the verdict, and this doesn't touch the org's tracked exclusion policy.
  • Fail-open risk: the gate is explicitly fail-closed (exit 2) on every prerequisite it can't verify, an empty/stale allowlist, a missing coverage summary, or a mismatched file count, rather than defaulting to a silent pass. --list stops before invoking the detector at all, matching the audit-only contract its own header documents.
  • Path traversal: file discovery is scoped through git ls-files against the repo's own tracked index, so allowlist entries can't reach outside the checkout.

Nothing here touches authn/authz, secrets, or code execution paths beyond a read-only lint gate over tracked markdown, and the design is consistent with the same-shaped scripts/docs-only-paths.txt gate already in the codebase. No inline comments filed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a1b64959cf

ℹ️ 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".

Comment thread scripts/check-purged-em-dashes.sh Outdated
Comment thread scripts/check-purged-em-dashes.sh Outdated
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Last security-reviewed head: af46aa6c327b868235160e21269457a10140f075. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

Comment thread scripts/check-purged-em-dashes.sh
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

kyle-sexton and others added 4 commits August 26, 2026 02:37
The de-slop campaign purges prose surface by surface, and each landed shard
is paid for by hand: a mechanical em-dash split changes meaning often enough
that every shard so far has needed a rationale-withheld reviewer to catch
clauses the automated passes waved through. Nothing then stopped the next
contributor from reintroducing one, because no lane enforced the policy. A
one-time purge with no gate is a purge that silently rots.

Add scripts/check-purged-em-dashes.sh, which enforces zero em dashes on the
paths declared in scripts/em-dash-purged-paths.txt, and wire it into the ci
hygiene job so its outcome reaches the ci-status aggregate.

Enforcement is scoped to an allowlist rather than the corpus. 29,649 em-dash
prose lines across 1,074 tracked markdown files remain, so a repo-wide check
could only have been merged switched off. Listing what is already clean
inverts that: adoption breaks nothing, because every listed path passes the
moment it is added, and enforcement grows with the campaign instead of
waiting for it. This is the same allowlist-not-blocklist argument, and the
same fail-safe direction, that scripts/docs-only-paths.txt already makes.

The tracked .claude/ai-slop.json is untouched. Re-enabling rule-em-dash
corpus-wide there is a separate decision the campaign has gated on the purge
finishing, so the gate instead copies that config into a throwaway layer for
its own detector run and removes only rule-em-dash from disabled_rules. Every
exclusion the tracked file carries, including the vendor, catalog and
eval-fixture paths that hold em dashes as data, keeps applying unchanged.

The detector is driven rather than reimplemented: a bare grep would fire
inside code fences, inline code spans and ignore-marked regions, which
legitimately carry the character. Because that detector exits 0 on every
audit path, the gate believes a run only when the rule-em-dash summary is
present, reports disabled=0, and accounts for exactly the files handed to it;
any other shape is exit 2 rather than a false green. A stale allowlist entry
matching no tracked file fails for the same reason.

Refs #2891

Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
…right

The gate shipped mode 100644 while the workflow invokes it as a bare path,
`scripts/check-purged-em-dashes.sh`, exactly as it invokes every sibling
scanner. Every one of those siblings is tracked 100755. The hygiene job would
therefore have failed on contact with a permission error, and the repository's
own unconditional exec-bit scanner would have reported the same defect a second
time. Both the gate and its test script are now tracked executable.

Add plugins/playwright to the allowlist. Its README and its skills were purged
in #3320 and contain no em dash anywhere, so the entry meets the stricter
standard the allowlist header declares for a seed entry: the whole list stays
auditable by expanding it with --list and grepping the named files for the
character. The plugin was clean when the list was seeded and was simply not
carried over.

Restructure the spaced double hyphen in the workflow comment into a sentence
break. The gate's own failure message tells a contributor not to reach for a
hyphen when an em dash comes out, and a comment introducing that gate should
not model the substitution it warns against.

Refs #2891

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gate

A fresh-context reviewer audited the gate with the authoring rationale withheld
and returned two defects that every automated pass had waved through. Both are
fixed here, each with a regression case that fails against the previous code.

--list ran the whole detector. The mode changed only what the expansion loop
printed; control then fell through to the detector invocation and the check
verdict, so a listing cost the same minutes as a full check and returned the
check's exit code. That contradicted the allowlist header, which sends a reader
to --list precisely to audit the declaration WITHOUT running the detector. It
now stops after expansion, still failing on a stale entry, and the real
allowlist lists in under a minute instead of nine.

The coverage assertion double-counted a declined file. The gate proved the
detector had accounted for exactly the files handed to it by adding the two
totals in the detector's summary, but those totals overlap: a file the detector
opens and then declines on an in-file marker is counted under BOTH "files
scanned" and "files declined", while a file excluded by a config glob is never
opened and appears only under the latter. An allowlisted path carrying an
ai-slop-ignore-file marker therefore failed the run at exit 2, naming a
coverage problem that did not exist, for using an exemption the allowlist
explicitly promises. Unique files handled is now the scanned count plus the
excluded-glob declines alone, read from the decline rows the detector already
labels with a cause.

Two smaller corrections come with them. A declared path that the tracked
detector config excludes is now named in the output instead of being folded
into the clean count: such a path sits inside the allowlist and outside
enforcement at once, which is the one way this gate can report green over a
surface it reads nothing on. And the clean verdict now reports files scanned
rather than files declared, so the number means what it says.

The workflow comment called an ungated docs-only diff a fail-closed hole when
the hazard is the opposite, and the two enumerations of the unconditional
whole-repo scanners had not been told about the fourth one.

Refs #2891

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…orced

Two switches in the tracked config could leave a declared path green without
being judged, and the gate reported neither as a failure.

`em_dash_allowed_paths` and `rule_allowed_paths["rule-em-dash"]` exempt a file
from this one rule. The derived config copied both through, and the exemption
is invisible in the run it produces: detect.sh still OPENS such a file, so it
lands in the scanned count, the coverage assertion still balances, and the rule
summary still reports `disabled=0`. Every signal the gate reads says the
surface was checked and clean while no finding on it was ever possible. The
derivation now strips both, alongside the `disabled_rules` entry it already
stripped, because a path on the allowlist declares the surface purged and a
per-rule exemption on that path is the opposite claim. A declared file that
genuinely carries the character as data still has the detector's in-file
exemptions, and failing those it belongs off the allowlist with the reason.

`excluded_paths` is the one exclusion left standing, since its files are never
opened and so remain visible as excluded-glob declines. The gate named them and
then fell through to the clean path, printing "no em dashes" and exiting 0. A
declaration the detector never reads is not coverage, and a green exit is how
it gets mistaken for some, so that count is now a nonzero verdict with a
message that distinguishes it from a clean run.

Four cases cover it, each pointed at the seeded violation, which is the only
fixture where suppression is observable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N2RpNNVGx3wMp8oFeFVBSA
@kyle-sexton
kyle-sexton force-pushed the feat/2891-em-dash-purged-paths-gate branch from a1b6495 to af46aa6 Compare August 26, 2026 06:50
@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 11s —— View job


Code review complete

  • Gather context (PR body, comments, existing review findings)
  • Load /review:code-review skill instructions
  • Review changed files against origin/main diff
  • Post inline findings (if any)
  • Post summary comment

This lane covers correctness/maintainability only (claude-security-review.yml exists in this repo, so security findings are out of scope here per REVIEW.md).

Summary

This is a re-review after new commits (84c224a, af46aa6) landed in response to the two prior findings on this PR (codex's excluded_paths fallthrough, and my own em_dash_allowed_paths/rule_allowed_paths gap). I re-derived the diff independently rather than trusting the PR body's account of the fix, and traced both through plugins/ai-slop/skills/audit/scripts/detect.sh (unchanged by this PR) to confirm the fix is structurally correct, not just textually plausible:

  • excluded_paths fallthrough (codex): scripts/check-purged-em-dashes.sh:271-283 now sets verdict=1 when excluded > 0 and threads it through both the empty- and non-empty-findings branches, so a declared path the tracked config excludes now fails the gate (exit 1) instead of falling through to the clean-count path. Confirmed via the new cfg-excluded.json fixture case in check-purged-em-dashes.test.sh:314-321.
  • em_dash_allowed_paths / rule_allowed_paths["rule-em-dash"] gap (my prior finding): the throwaway config derivation at scripts/check-purged-em-dashes.sh:221-223 now strips both keys (del(.em_dash_allowed_paths) | .rule_allowed_paths |= (... | del(."rule-em-dash"))). I checked detect.sh:273 to confirm em_dash_allowed_paths is only a legacy alias that gets folded into rule-em-dash's rule_allowed_paths entry, so stripping both closes the loophole completely rather than leaving a second spelling open. Confirmed via the new cfg-em-dash-allowed.json / cfg-rule-allowed.json fixture cases (check-purged-em-dashes.test.sh:296-311), each pointed at the seeded violation so suppression would be observable if the strip regressed.

I also independently checked, beyond re-verifying the two fixes:

  • detect.sh's config cascade (CFG_LAYERS, detect.sh:171-174) includes a third layer, .claude/ai-slop.local.json, that the gate's throwaway-config build doesn't explicitly account for. This isn't a hole: REPO_ROOT inside the detector run resolves from CLAUDE_PROJECT_DIR, which the gate points at its own empty $TMP/root, so a real .claude/ai-slop.local.json in the actual checkout (if one existed) is never read by this gate's invocation regardless. Same isolation mechanism as the HOME redirection, just for a layer the PR's own comments don't mention — worth being aware of but not a defect.
  • .github/workflows/ci.yml's new steps (ci.yml#L287-L299) against scripts/check-docs-only-gate.sh's own structural contract (its "THE FEED MIRRORS A REAL GATE" check, check-docs-only-gate.sh:591-615): the new purged_em_dashes step carries no if: and its aggregator feed entry is the raw steps.purged_em_dashes.outcome}} with no success-override, so it's correctly classified as an ungated whole-repo scanner and doesn't trip that gate's pairing check. Consistent with the PR body's claim that check-docs-only-gate.sh --check passes.
  • Re-derived the coverage arithmetic (scanned + excluded == EXPECTED, check-purged-em-dashes.sh#L249-L267) from detect.sh's scanned/declined counter semantics rather than trusting the comment's claim that they overlap — holds.

No new findings. Both fixes are correct, minimal, and each has a fixture that fails against the pre-fix code (verified by reading the fixture/assertion pairs, not by executing them).

Not independently verified: this sandboxed environment blocks running shell scripts (bash scripts/check-purged-em-dashes.test.sh, check-docs-only-gate.sh --check, shellcheck/shfmt/actionlint) without interactive approval unavailable here — same limitation as the review on the prior head. Everything above was verified by static reading of the diff and the (unchanged) detect.sh/check-docs-only-gate.sh it interacts with, not by execution.
(branch)

@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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


Security review

  • Gather context (PR diff, prior review comment, CLAUDE.md rules)
  • Apply skip gate (diff since last-reviewed head)
  • Review new commits for security-relevant surface
  • Post findings

Scope reviewed: commits landed since the last security-reviewed head (a1b64959c), specifically 84c224a and af46aa6c327b868235160e21269457a10140f075 (current HEAD), which close the two exemption-suppression gaps flagged by the Codex review and the earlier Claude code-review comment on this PR — af46aa6.

No security issues found.

What changed: scripts/check-purged-em-dashes.sh's throwaway-config derivation now also strips em_dash_allowed_paths and rule_allowed_paths["rule-em-dash"] (in addition to the already-stripped disabled_rules entry) before invoking the detector, and the excluded_paths coverage check now fails the gate (verdict=1) instead of merely warning and falling through to exit 0 (check-purged-em-dashes.sh#L268-L295). This is a correctness/enforcement fix to a read-only CI lint gate — it closes ways the gate could pass without actually checking a declared surface, which tightens the gate rather than loosening it.

What I checked for and didn't find:

  • New injection surface: the added jq filter (del(.em_dash_allowed_paths) | .rule_allowed_paths |= (... | del(."rule-em-dash"))) operates on the already-isolated throwaway config copy under $TMP/root/.claude/, built from the tracked .claude/ai-slop.json with no interpolation of PR-controlled or externally-sourced strings into the jq program itself.
  • Trust boundary / exec changes: no new process invocation, no new file writes outside the existing $TMP sandbox, no change to what detect.sh is handed as input paths.
  • Fail-open regression: the direction of both fixes is fail-closed (an unenforced/exempted declared path now produces exit 1 instead of a silent exit 0), consistent with this gate's existing fail-closed design reviewed previously.
  • Workflow/permissions changes: .github/workflows/ci.yml is untouched by these commits; no change to trigger type, permissions:, or secret exposure since the prior review.
  • Test-only changes: the corresponding check-purged-em-dashes.test.sh diff adds four fixture configs and cases exercising exactly these two suppression paths (em_dash_allowed_paths, rule_allowed_paths, and the excluded_paths fail-instead-of-warn behavior) — no security relevance, but confirms the fix is exercised.

Nothing in this incremental diff touches authn/authz, secrets, or code execution paths beyond the same read-only lint gate reviewed previously. No inline comments filed.

@kyle-sexton
kyle-sexton merged commit 20b929a into main Aug 26, 2026
55 checks passed
@kyle-sexton
kyle-sexton deleted the feat/2891-em-dash-purged-paths-gate branch August 26, 2026 07:03
kyle-sexton pushed a commit that referenced this pull request Aug 26, 2026
main landed three commits after this branch was cut, and one of them collided:
ai-slop released 0.4.1 for the Windows jq carriage-return fix while this branch
had already used 0.4.1 for its table-of-contents entry.

This conflict is why CI had gone quiet rather than red. GitHub schedules
pull_request workflows against the computed merge commit, and with a conflict it
cannot compute one, so ci and pr-title were never created for this head while the
pull_request_target workflows (do-not-merge, pr-issue-linkage) kept reporting
green against the base. Three green checks on a PR that should run a dozen is the
tell; a missing run reads exactly like a slow one.

Same resolution rule as the earlier collision on this branch: main's entry is
released and keeps its number, this branch's entry is unreleased and renumbers
above it, and the manifest ends at the highest. So the table-of-contents entry
becomes 0.4.2, main's 0.4.1 stays where it is with its body verbatim, and the
manifest reads 0.4.2.

One thing in main's other commit needed checking rather than assuming. #3344
added scripts/check-purged-em-dashes.sh, a ratchet enforcing zero em dashes on
an allowlist of already-purged surfaces, and that allowlist covers
plugins/implementation/skills/*/SKILL.md and plugins/overengineering/skills/*/SKILL.md,
both of which this branch edits. The table-of-contents pass copies heading text
verbatim into its rows, em dashes included, so the two changes could have
collided by construction. They do not: the gate passes, 21 declared paths and 30
files scanned.

Verified on the merged tree: no conflict markers anywhere, changelog parity,
order and bump gates clean, the new em-dash gate clean, plugin contracts across
3023 files, and markdownlint clean at 1359 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTVH5ZRVph75kvxzUAnb6q
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.

1 participant