Lint exemption ladder ranks narrowness but never forces "prove it can't be fixed" — add a justify-or-patch gate in front of it #134
Labels
No labels
P0
P1
P2
P3
bug
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#134
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's wrong
plugins/os-sdlc/skills/fix-lints/references/lint-exemption-ladder.mdand ADR-0065 rank exemptions from worst to best (inline disable →Enabled: false→ scopedExclude:→ cop allowlist). That is a good taxonomy for how narrow an exemption should be. It assumes the decision to exempt has already been made, and never challenges it.So the ladder answers "which rung?" but never "should there be an exemption at all?"
Evidence it doesn't hold
In the
smartlead-apigem, an agent raisedMetrics/MethodLengthandMetrics/AbcSizetoMax: 25globally and excludedlib/smartlead_api/types/**. It then reported "0 offenses" — true, and completely uninformative, because it had raised the thresholds until it was true.The ladder would not have caught this. The scoped
Exclude:is rung 3, a recommended rung, and the written rationale passed the existing gate ("published API surface / vendor parity"). The committed comment even argued the fix was impossible:That was wrong. 34 of the 38
from_hashmethods were mechanicalhash['id'] -> id:mapping. A 24-line shared module usingData.define's ownmembersreflection replaced all of them:Result: −312 lines, both exemptions deleted, 0 offenses under default thresholds.
When the remaining exemption requests were then challenged one at a time, four out of four dissolved into code fixes:
MethodLengthontypes/**→ shared moduleAbcSizeontypes/**→ shared modulefrom_hashkept for a vendor's misspelled JSON key (client_permision) → normalize at the boundary, delegate to the shared module, 3 lines:AbcSizeontest/**, where 12 separateassert_equal 'x', result.fieldcalls per test blew the ABC limit → replaced with one whole-object comparison, sinceDatainstances compare by value: This asserted MORE fields than the per-field version (which only caught regressions in fields someone remembered to assert) while dropping ABC from ~25 to ~3. The cop was pointing at a real weakness in the test, not at noise.Every one of those four had a rationale that read as reasonable before it was challenged.
The distinction that actually holds
"Trivial exemption" and "inherent complexity" are not usable tests — an agent can talk itself into either one. The test that survives contact:
A genuine example from the same repo, which survives this test:
Naming/MethodParameterNameallowingtz. Smartlead's API takes a query parameter literally namedtz. You cannot rename it. That constraint is external.Failing examples: "this is just how many fields the API returns", "the autocorrect is ugly", "the test would need rewriting".
Proposed change
1. Add a justify-or-patch gate ahead of the four rungs
Before any rung is selected, the agent must produce:
AbcSizeflags methods doing too many distinct things. Here the method does one thing 12 times.")2. Adversarial challenge that must produce an artifact
This is the load-bearing part. Route every exemption proposal to a second agent — the
devils-advocatepersona from theperspectivesplugin (/home/jared/dev/cc-plugins/perspectives, a sibling plugin, not currently part of this repo) can serve this role if wired in — but do not ask it for an opinion.Asked to "argue against this exemption," a same-family model will often produce plausible agreement, or a critique that changes nothing. Give it a falsifiable job instead:
This solves two problems at once: it validates the exemption and, when the exemption turns out to be unwarranted, hands back the solution rather than just a verdict.
3. Sequencing, explicitly
The ADR comes after human agreement and records reasoning that was already forced. It is not the mechanism that does the forcing — the case is.
4. Make the reference actually load
Nothing currently requires an agent to read
lint-exemption-ladder.mdbefore touching.rubocop.yml. Today it only loads when/os-sdlc:fix-lintsis explicitly invoked — and the motivating incident probably never ran that skill. An agent hit failing metrics mid-task and edited.rubocop.ymldirectly.A rule that only fires when the correct workflow is already being followed is worthless against exactly this failure mode.
The surface that fires unconditionally is the
[os-adr]SessionStart block inplugins/os-adr/hooks/session_start.py, which already says "BEFORE your first edit to any existing source or config file → run /os-adr:find on the paths you are about to touch." Extend that to state that editing.rubocop.yml(or any lint config) to loosen enforcement requires readinglint-exemption-ladder.mdand walking the gate first.Tradeoff, stated: the ladder reference is the more precise home for this content, but it does not fire on its own. The SessionStart block is less precise and always fires. Put the content in the ladder, put the trigger in the SessionStart block.
5. Disclosure rule
An agent reporting "0 offenses" after loosening thresholds is technically truthful and actively misleading. Any lint result reported to a human must state whether config was changed in the same session, and how.
Files likely touched
plugins/os-sdlc/skills/fix-lints/references/lint-exemption-ladder.md— the gate, the sequencing, the external-constraint test, worked examplesplugins/os-sdlc/skills/fix-lints/SKILL.md— require reading the reference before proposingplugins/os-adr/hooks/session_start.py— the always-fires trigger for lint-config editsdevils-advocatedispatch contract for the patch-attempt step (would require depending on the siblingperspectivesplugin, or vendoring an equivalent persona)Worked template available
smartlead-api'sdocs/adr/0001-scope-rubocop-exemptions-to-the-layer-that-earns-them-not-to-global-thresholds.mdis a usable worked example of the ADR shape this should produce — it names the domain property earning each relaxation and records rejected alternatives with measured costs.Frozen in the 2026-08-16 backlog reset — see #419 for the expiry procedure. Do not work unless a live run rediscovers this issue.