feat(claude-config): body-scoped coercive-emphasis detector wired to the findings relay - #3176
Conversation
…ings-relay wiring Carries the D2 (coercive emphasis) detector end-to-end to the review:fanout apply relay, plus the producer wiring the claude-config plugin did not have. The detection half already existed: criteria.md I28 is the coercive-emphasis check, and instruction-scan.sh already marked its two families. What was missing was a route to a remediation surface, and a fence making that route safe. instruction-scan.sh --body-only skips YAML frontmatter, so no row can point at a description, when_to_use, or a trigger phrase quoted in one. Opt-in rather than default: the human-facing audit legitimately reports on frontmatter content, and what must never happen is such a row reaching an APPLY relay. An unclosed leading `---` fences the whole file (fail-safe); a mid-document `---` opens nothing. emit-findings.sh composes a conforming `type: review-findings` file. It recomputes the body-scope fence rather than trusting its input, and additionally declines any body row quoting a trigger phrase that appears in the file's own description. Both fences exist because check-skill.sh check 3 hard-FAILs a dropped trigger phrase versus the base ref, so a remediation that edits one is an auto-invocation regression. Declines are counted and reported in `## Surfaces`, never silent. Only the two I28 families are emitted; every other family instruction-scan.sh marks has no severity-crosswalk row, and the contract admits no row whose tier cannot be looked up from one. Those stay in the human report, counted as declined. Remediation is a downgrade, never a deletion: both Action cells require the directive to survive and only its volume to change. Tests: 84 scanner checks (14 new, covering the fence and its fail-safe cases) and 38 producer checks, including the negative test that feeds the writer deliberately-unfenced input and asserts no emitted row points at a description or when_to_use. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
…ng for I28 Completes the producer: the code landed in the previous commit, this is the contract half that makes it admissible. docs/conventions/detector-findings/README.md gains two crosswalk rows — rule-coercive-emphasis and rule-blanket-tool-default — both IMPORTANT. The tier is argued from severity.md's tests, first match winning: CRITICAL fails every limb because instruction prose computes nothing (what emphasis changes is how likely a trigger is to fire, a distribution over behavior rather than a determined result), and IMPORTANT's degradation-with-a-named-trigger limb then matches, the trigger being the first session in which the over-emphasized instruction fires on input its actual condition does not cover. Official guidance supplies the mechanism rather than a preference, so SUGGESTION's catch-all is never reached: the alternatives do not both work, one overtriggers. The adopter row records what is novel here — the first producer whose emitted set is a strict subset of what its scanner marks, and the first with a structural fence on which findings may reach the relay at all. context/persist-findings.md is this plugin's read of the producer contract. context/report-keying.md takes the Phase D state-key mechanics out of SKILL.md as a progressive-disclosure spoke; SKILL.md had 4 lines of headroom under the 500-line hard cap and documenting a new capability needed more than that. The moved text also restated rules the plugin-data-report-keying convention already owns, so the spoke points at that owner rather than re-stating it again. criteria.md records on I28: the two rule ids, that the remediation is a downgrade and never a deletion, that relay-bound findings are body-scoped and why, and that V1 selection is deliberately narrower than the Detect prose — whole bolded sentences and general all-caps runs are judged by the model lane but not mechanically selected, both being too common in ordinary technical prose to select without swamping the relay. Written down rather than left as a silent gap. Verification: skill-quality:check PASSes against the source checkout (0 errors; 483/500 lines, all 13 base-ref trigger phrases preserved, 3/3 script suites green). shellcheck, shfmt, markdownlint, typos, editorconfig all clean. The one remaining check-skill warning (criteria.md:1020) reproduces on the base ref. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
…rced-caps The end-to-end proof caught what inspection would not have. #3120 asked for a test proving the directive survives the remediation "byte-for-byte". Run against a real leading wrapper, that assertion fails: dropping "CRITICAL: You MUST " promotes the next word to sentence-initial position, so "...MUST resolve the item id" becomes "Resolve the item id" and exactly one byte changes. The change is forced by the edit rather than chosen, and the official source's own worked example makes it too — "CRITICAL: You MUST use this tool when..." becomes "Use this tool when...", lowercase `use` to capital `Use`. A contract demanding strict byte equality would therefore reject upstream's own demonstration of the remediation. So the contract now asserts verbatim survival APART FROM sentence-initial capitalization forced by dropping a leading wrapper; any other change to the directive's wording still means the remediation overreached. Updated in the Action cell emit-findings.sh writes, in criteria.md's I28 row, in persist-findings.md, and in the crosswalk row. New test case 9b applies each Action at its Location and asserts the result: the wrapper is gone, the frontmatter is untouched, the directive matches case-insensitively, only the first byte differs, and the remediated line is no longer a candidate. End-to-end proof (acceptance criterion 3), run not inspected: 1. detector runs body-scoped over a sample surface -> 2 candidates 2. emit-findings.sh writes to the rung-5 findings home for this branch 3. every clause of fix-pass-mode.md "Step 1" verified against the written file: .md extension, `type: review-findings`, `branch:` exact match, parseable table, no subtracting fix-pass-record 4. remediations applied; frontmatter digest unchanged, description and when_to_use byte-identical, quoted trigger phrases intact, directive verbatim modulo the one forced capital, instruction not deleted 5. re-scan returns no candidates: no stale finding survives its remediation Verification: 43 + 84 + 48 script checks green, skill-quality:check PASS (0 errors), shellcheck / shfmt / markdownlint / typos clean. Closes #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
|
Claude finished @kyle-sexton's task in 7m 4s —— View job Security review complete
Finding: body-scope fence bypass via non-canonical
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 153c808f4c
ℹ️ 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".
CI runs the suite on a detached-HEAD checkout of the PR merge ref, where `git branch --show-current` returns empty. emit-findings.sh resolves --branch before it checks whether its input is scanner output, so case 3 hit the exit-2 branch guard and never reached the exit-3 condition it was asserting. The script's behavior is correct and unchanged: `branch:` is load-bearing for the consumer — fix-pass-mode.md "Step 1" admits a candidate only when its branch: equals the current branch exactly — so with no branch to write there is nothing correct to emit, and refusing beats writing a file the relay can never match. The defect was the test asserting one condition while tripping over another. Case 3 now passes --branch explicitly so it isolates the scanner-row check. New case 3b asserts the branch-resolution path on purpose: with GIT_DIR pointed at a nonexistent path (a hermetic stand-in for detached HEAD) it exits 2 and writes no file, and an explicit --branch still succeeds and lands in the frontmatter. That path was real and untested until CI exercised it by accident. Reproduced locally by detaching HEAD before the fix (exit 2, not 3), and verified green after. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
|
Last security-reviewed head: |
|
Claude finished @kyle-sexton's task in 4m 27s —— View job Security Review —
|
… branch names Four findings from the Codex review on #3176, all verified against the code before acting and all reproduced empirically first. CRLF DEFEATED THE BODY-SCOPE FENCE (the serious one). head/awk leave a terminal CR, so a CRLF frontmatter delimiter reads as "---\r" and matched neither comparison in either layer. The block was then treated as body content and description / when_to_use rows became emittable — the fence inverted on exactly the Windows-authored files it most needs to hold for. Measured before the fix: a CRLF fixture emitted a finding pointing at its own description line, which is the auto-invocation regression the fence exists to prevent. Both the scanner and the writer now strip a terminal CR before comparing, and the excerpt is stripped too so no stray CR lands in a table cell. OUT-OF-REPO LOCATIONS REACHED THE RELAY. Phase A inventories user-level surfaces under CLAUDE_CONFIG_DIR, but Location is contractually repo-relative and the fix action fences each remediation to it. A hit in a user CLAUDE.md entered the findings file with an absolute path, where the fix pass could try to mutate a file outside the working tree. Such rows are now declined and counted as reason=outside-repo-root; they stay in the human report. CARVE-OUT DECLINES WERE SILENT. persist-findings.md told the model to drop carve-out candidates before the writer saw them, while the same document requires every decline to be counted — so ## Surfaces would have reported fewer candidates examined than were actually looked at. New --declined-carveout <n> records the count as its own line; the doc now says to pass it. YAML-INDICATOR BRANCH NAMES MISPARSED. git accepts @foo, !foo, #foo; emitted as plain scalars, #foo reads as a comment and the others as indicators, so the consumer — which admits a candidate only on an exact branch match — silently dropped every finding for such a branch. Quoting is deliberately CONDITIONAL: an ordinary branch name keeps a byte-identical plain scalar, so this cannot perturb the common path or diverge from the sibling ai-slop producer on it. Tests: 69 producer checks (was 47) and 89 scanner checks (was 84). The CRLF cases use a throwaway git repo so the new out-of-repo fence does not mask what they measure, and the pipe-escaping case moved into it for the same reason. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
…r parser Follow-up to the CRLF fix, from the automated review on #3176. Stripping a terminal CR closed the CRLF case but left the general one open. All three delimiter checks — instruction-scan.sh frontmatter_end, and emit-findings.sh fm_end and descr — matched "---" by exact equality. This repo's own authoritative extractor, skill_frontmatter::extract, matches ^---[[:space:]]*$, and that is what check-skill.sh parses frontmatter with — the hard-FAIL gate this entire fence exists to satisfy. Exact equality is STRICTER than that parser, and the mismatch runs the dangerous way: a delimiter carrying trailing whitespace or a CR is real frontmatter to the gate but invisible to the fence, so the block reads as body content and description / when_to_use rows become emittable. Because descr shares the detection, the quoted-trigger-phrase fence is disabled in the same case, and because emit-findings.sh recomputed the same exact-match logic, both layers failed on identical input rather than independently — which is the one property the defense-in-depth design was supposed to buy. All three now use the authoritative pattern, so the three readers agree instead of the fence being only as strong as the loosest one. [[:space:]] subsumes the CR case, so this replaces the strip on the delimiters; the excerpt keeps its own strip so no stray CR reaches a table cell. Tests: 72 producer checks (was 69), 92 scanner checks (was 89) — CRLF, trailing space, trailing tab, and unclosed-frontmatter variants in both layers. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
|
Claude finished @kyle-sexton's task in 2m 31s —— View job Security review in progress
|
|
Claude finished @kyle-sexton's task in 2m 14s —— View job Security Review —
|
…a fixture fixture-git-isolation-gate caught this on 794851a and bab7be7: the CRLF and out-of-repo cases build a throwaway repository (git init) to give the writer a repo_root of its own, and the suite did not clear the ambient git environment first. The gate is right and the hazard is real. `git -C <dir>` is a readability guard, not isolation: an inherited ABSOLUTE GIT_DIR overrides repository discovery outright and outranks -C, and GIT_CONFIG replaces the file `git config` reads and writes regardless of -C. Under either, the fixture identity lands in the CALLER's .git/config — shared by every worktree of the clone — and the fixture keeps none. Cleared unconditionally at the top of the suite, matching the plugin-suite precedent (ai-slop detect.test.sh and four others) rather than sourcing the repo-level harness. Case 3b's per-command `GIT_DIR=...` prefix is unaffected: it scopes to that one invocation and is set deliberately to simulate a detached HEAD, not inherited. Not added to scripts/fixture-git-isolation-baseline.txt — that file records pre-existing debt only, and this suite is new. Reproduced the failure locally against the gate before fixing, and confirmed after: "fixture git isolation: OK (87 isolated, 0 baselined)". Suites unchanged at 72 / 92 / 48 green. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
|
Claude finished @kyle-sexton's task in 3m 6s —— View job Security Review —
|
|
Claude finished @kyle-sexton's task in 4m 25s —— View job Code review —
|
|
Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
Review finding: `declined_unreadable` was the only decline path in emit-findings.sh with zero assertions — every sibling (no-crosswalk, frontmatter, quoted-trigger, out-of-repo, carve-out) had at least one. Confirmed by grep before acting: the reason string appeared only at its two emit sites. It is reachable in the documented pipeline. The scan output and the source file are read at different moments — the model lane edits the --from file to drop carve-out rows — so a source file touched in that window, or a stale scan-output file reused, leaves a row whose line number no longer exists. source_line() returns "" and the row becomes a counted decline rather than a finding with an empty excerpt. That fail-safe direction was already correct; what was missing was the assertion keeping an off-by-one in source_line's counting loop from silently changing it. New case 8b feeds a row pointing past EOF and asserts no findings row, the decline counted, and no row bearing the phantom location. Verified meaningful by mutation: deleting the branch fails exactly these three assertions. The case uses its own output variable. Written against the shared `OUT` it clobbered what cases 10 and 11 read — caught by running the whole suite rather than only the new assertions, and noted inline so the next case does not repeat it. 75 producer checks (was 72); scanner 92 and conflict 48 unchanged. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
|
Claude finished @kyle-sexton's task in 38s —— View job Security Review —
|
|
Claude finished @kyle-sexton's task in 2m 5s —— View job Code review —
|
…ng order Review nit: case 8b was labelled 8b but placed between 9b and 9c, so the case numbers stopped being a reliable index when scanning the file top to bottom. Moved the block rather than renumbering. The reviewer offered 9g as one option, but that would have been wrong for where the block actually sits — 9g implies it follows 9f, and it preceded 9c. Moving it to Case 8 is what the existing label already claims, renumbers nothing, and puts it beside the case it belongs with: 8 decides which rows are admitted, 8b decides which are declined. Pure block move; no assertion changed. 75 producer checks still pass, and the mutation check still catches exactly the same three (deleting the declined_unreadable branch), so the case is still load-bearing where it now sits. Case order is now strictly ascending end to end. Refs #3120 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF
|
Claude finished @kyle-sexton's task in 37s —— View job Security Review —
|
|
Claude finished @kyle-sexton's task in 47s —— View job Code review —
|
|
No description provided. |
Fifth #2891 de-slop shard: purge em dashes from the claude-config plugin instruction surfaces, the next-worst cluster after session-flow (#3106), planning (#3105), work-items (#3107) and source-control (#3108). Rewrote README.md and all ten SKILL.md files under /ai-slop:audit fix semantics: periods, commas, a colon before a list, or a restructured sentence. Never parentheses, en dashes, or a spaced hyphen, since each of those is the same interruption wearing a different mark. Two review rounds on the PR caught seven places where a paired em dash had become parentheses and two doubled-connective run-ons; all were restructured. Rebased across two releases that landed on main mid-review, in both cases re-applying to the newer text rather than reverting it: - 0.39.0 (#3176) restructured audit-instructions/SKILL.md, moving the Phase D state-key block to context/report-keying.md and adding --persist-findings. The flag, its Phase D paragraph, and both context/ spokes are retained. - 0.39.1 (#3178) normalized setup/SKILL.md and audit-instructions/SKILL.md to canonical fleet SSOT wording with PLUGIN-PHILOSOPHY citations. That wording and those citations are kept verbatim; only their punctuation is de-slopped. context/ files stay out of scope, matching #2891's target set and every prior shard. Frontmatter description and argument-hint values are rewritten too. No quoted auto-invocation trigger phrase contained an em dash, so no trigger changed. Verification (this repo's .claude/ai-slop.json disables rule-em-dash corpus-wide, so the detector runs against an isolated HOME and CLAUDE_PROJECT_DIR to force the rule on): - detect.sh over the 11 shard files: 0 findings, every rule clean - no en dash or spaced hyphen introduced; the four en dashes in the diff are pre-existing numeric ranges (I1-I28, I1-I5, 3-5 lanes) - check-changelog-parity.sh --check and --check-bump origin/main: pass - CHECK_SKILL_SKIP_MARKDOWNLINT=1 check-changed-skills.sh origin/main: 10 skills, 0 errors, every base-ref trigger phrase preserved - markdownlint-cli2 over the 12 changed files: 0 issues - audit-instructions/SKILL.md is 484 lines, under the 500-line cap Pre-existing and not from this diff: three audit-permission-state script suites fail identically on a clean origin/main worktree in this environment. This shard touches no script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tu5t8rYWv2kDzRcdmLE2ro

Closes #3120
Summary
Carries D2 (coercive emphasis) end-to-end to
review:fanout's apply relay, plus the findings-producer wiring theclaude-configplugin did not have.claude-config:audit-instructionsbecomes the plugin's first conformingdetector-findingsproducer.Scope note worth reading first: most of the detection already existed.
criteria.mdI28 is the coercive-emphasis check, andinstruction-scan.shalready marked its two families. What was missing was a route to a remediation surface — and a fence making that route safe. The fence turned out to be the substantive part: the scanner read whole files including YAML frontmatter, soIMPORTANT:inside adescription:was a live candidate, directly against #3118's hard constraint.Fix
Body-scope fence, applied twice.
instruction-scan.sh --body-onlyskips YAML frontmatter, so no row can point at adescription,when_to_use, or a trigger phrase quoted in one. It is opt-in, not default: the human-facing audit legitimately reports on frontmatter content, and what must never happen is such a row reaching an apply relay.emit-findings.shthen recomputes the fence rather than trusting its input — a fence living only in the caller is one caller away from being bypassed — and additionally declines any body row quoting a trigger phrase present in the file's own description. An unclosed leading---fences the whole file (fail-safe); a mid-document---opens nothing. Every decline is counted in## Surfaces, never silent.All three delimiter checks match
^---[[:space:]]*$— deliberately identical toskill_frontmatter::extract, which is whatcheck-skill.shactually parses frontmatter with. Exact"---"equality would be stricter than that parser, and the mismatch runs the dangerous way (see Review rounds below).The fence exists because
plugins/skill-quality/scripts/check-skill.sh:414hard-FAILs a dropped'trigger phrase'versus the base ref: a remediation editing one is an auto-invocation regression, not a debatable suggestion.Out-of-repo fence. Phase A inventories user-level surfaces under
${CLAUDE_CONFIG_DIR:-~/.claude}, butLocationis contractually repo-relative and the fix action fences each remediation to it. Rows outside the repo root are declined asreason=outside-repo-rootand stay in the human report.Two crosswalk rules,
rule-coercive-emphasisandrule-blanket-tool-default, bothIMPORTANT. Argued fromseverity.md's tests, first match winning: CRITICAL fails every limb because instruction prose computes nothing — what emphasis changes is how likely a trigger is to fire, a distribution over behavior rather than a determined result. IMPORTANT's degradation-with-a-named-trigger limb then matches, the trigger being the first session in which the over-emphasized instruction fires on input its actual condition does not cover. Official guidance supplies the mechanism rather than a preference, so SUGGESTION's catch-all is never reached: the alternatives do not both work, one overtriggers.Emitted set is a strict subset of what the scanner marks. Only the two I28 families have crosswalk rows; the other eight are counted as declined with
reason=no-severity-crosswalk-row. "No crosswalk row, no relay" made visible rather than enforced by silence.The read-only contract is unchanged. Persisting is opt-in behind
--persist-findings; a bare invocation reports and stops. The file is a proposal artifact for a human-gated relay, never an applied edit.One contract refinement the end-to-end run forced. #3120 asked for a test proving the directive survives "byte-for-byte". Run against a real leading wrapper, that fails: dropping
CRITICAL: You MUSTpromotes the next word to sentence-initial position, so…MUST resolve the item id→Resolve the item idand exactly one byte changes. It is forced by the edit, not chosen — and the official source's own worked example makes the same change (use→Use), so a strict byte-equality contract would reject upstream's own demonstration. The contract now asserts verbatim survival apart from that forced capitalization; any other wording change means the remediation overreached.One out-of-theme change, called out deliberately. SKILL.md had 4 lines of headroom under the 500-line hard cap, so documenting a new capability required moving something out. The Phase D state-key mechanics went to
context/report-keying.mdas a progressive-disclosure spoke. That block also restated rules theplugin-data-report-keyingconvention already owns, so the spoke points at the owner instead. Every removed claim was verified recovered.Review rounds
Nine automated review passes ran across three reviewers — Codex, the security-review lane (six passes), and the code-review lane (three). Six findings total, all reproduced empirically before fixing rather than accepted on assertion, and all fixed:
---delimiters (P1). Exact"---"equality is stricter thanskill_frontmatter::extract's^---[[:space:]]*$. A CRLF or trailing-whitespace delimiter is real frontmatter to the gate but was invisible to the fence, so the block read as body. Measured: a CRLF fixture emitted a finding pointing at its owndescriptionline. Becausedescr()shares the detection, the quoted-trigger fence was disabled in the same case, and because the writer recomputed the same predicate, both layers failed together rather than independently — the one property the defense-in-depth design was meant to buy. All three checks now use the authoritative pattern.Locationreached the relay (P1). Declined and counted, per above.persist-findings.mdtold the model to drop carve-out rows before the writer saw them while the same file requires every decline counted. New--declined-carveout Nflag records the count.@foo/!foo/#fooare valid git branches; emitted plain,#fooreads as a comment. Quoting is conditional — an ordinary branch stays a byte-identical plain scalar, so the common path and parity with the siblingai-slopproducer are preserved. (That producer has the same unquoted interpolation; flagged as a fleet-wide edge case, left out of scope here.)source-line-unreadabledecline had zero test coverage — the only decline path without an assertion. Covered by case 8b, verified load-bearing by mutation (deleting the branch fails exactly its three assertions).The final pass of each lane skip-gated, having nothing new to review. The last substantive security verdict: "no outstanding security issues."
fixture-git-isolation-gateseparately caught that the new tests build agit initfixture without clearing the ambient git environment — a real hazard, since an inherited absoluteGIT_DIRoutranksgit -Cand would write the fixture identity into the caller's.git/config. Cleared at the top of the suite, matching plugin-suite precedent; not added to the baseline file, which records pre-existing debt only.Verification
Run, not inspected.
End-to-end proof (acceptance criterion 3) against a sample surface carrying coercive emphasis in both frontmatter and body:
emit-findings.shwrites to the rung-5 findings home for this branchfix-pass-mode.md"Step 1" verified against the written file:.mdextension,type: review-findings,branch:exact match, parseable table, no subtractingfix-pass-record— all PASSLocation: frontmatter digest unchanged,descriptionandwhen_to_usebyte-identical, quoted trigger phrases intact, directive verbatim modulo the one forced capital, instruction not deleted, protected-content line untouchedGates (final):
emit-findings.test.shinstruction-scan.test.shconflict-scan.test.shskill-quality:check(source checkout)check-fixture-git-isolation.shEvery suite was additionally verified under CI's detached-HEAD condition, which is what surfaced the
--branchresolution ordering that made one assertion measure the wrong thing.The negative test feeds the writer deliberately unfenced scan output and asserts no emitted row points at a description or
when_to_use. Protected content gets one no-flag line per category the container spec names. Fence regressions cover CRLF, trailing space, trailing tab, and unclosed frontmatter in both layers. The remainingcheck-skillwarning (criteria.md:1020) reproduces on the base ref.Pre-PR diff gate: frontmatter blast radius is nil — only
argument-hintchanged,descriptionbyte-identical. The cross-plugin contract change is purely additive; no existing crosswalk row or rule modified.Related
Refs #3118 — parent spec container; this is its tracer-bullet slice, carrying the producer wiring D1 and D3 will reuse.
Two container-level observations, neither blocking this slice:
docs/specs/agent-doc-register-detectors-brief.mdas its authoring surface; that file does not exist in the repo.criteria.mdrather than left as a silent gap, following the precedent whereai-slopdeferred its third negative-parallelism pattern.🤖 Generated with Claude Code
https://claude.ai/code/session_01Xn6aUXRQSYx4cGuP5cDxbF