Give every lint-fixing skill and agent the lint lessons and a shared offense analysis #989

Closed
opened 2026-10-11 14:42:45 +00:00 by jared · 2 comments
Owner

Problem

Lint lessons reach only one place today: the pipeline gate. Other agents and skills that fix lint offenses never see them. None of them ask the agent to name the smell before it fixes or exempts an offense.

  1. The gate loads the lessons. lib/os_sdlc/command/gate/run.rb line 9 loads Lesson::Library.new(dir: .../"lint-lessons"). Lines 72, 80, and 102-103 build Lesson::Relay. The renderer is lib/os_sdlc/presenter/lesson/relay.rb.
  2. The other lint paths do not read lessons. skills/fix-lints, bin/lint-worklist, and the worklist code never read them.
  3. No instruction asks the agent to name the smell. Only agents/refactorer.md:15 says "state in one line what design fault it hints at". agents/programmer.md has nothing on this.
  4. The stop rule is weak. skills/fix-lints/SKILL.md:46-47 says to stop and report if a cop looks wrong. The ladder skills/fix-lints/references/lint-exemption-ladder.md:13 suggests only rubocop --show-cops.

All paths are under the plugin root plugins/os-sdlc.

Observed cost (n8n-api, issue jaredswanson/n8n-api#2, commit c51ebb3): a pipeline programmer added a Naming/PredicateMethod Exclude for 13 resource methods that always returned true. It did not ask why a method returns a constant. A human review found the real smell.

Survey (AC1)

Five places format rubocop offenses for an agent. One adds a lesson. All paths under plugins/os-sdlc.

Path Formats offenses in Offense object Lesson
gate reply lib/os_sdlc/presenter/gate/reply.rb lint_output via LintFeedback raw rubocop JSON hashes yes, Lesson::Relay with seen_keys: dispatch.violations_dataset.keys (command/gate/run.rb:103)
batch assignment file lib/os_sdlc/model/worklist.rb Offense#detail_line Worklist::Offense (cop, file, line, message) no
residual prompt lib/os_sdlc/model/residual_worklist.rb FileUnit#prompt Worklist::Offense no
iterative stdout lib/os_sdlc/controller/lint_worklist.rb print_iterative_cop Worklist::Offense no
brief lint_offense_history lib/os_sdlc/presenter/brief.rb rendered_lint_offense_history via LintOffenseLine LintOffenseLine::Entry built from LintReport::Offense no

Agents: agents/lint-fixer.md reads an assignment file verbatim. agents/programmer.md sees offenses only through gate reruns. agents/refactorer.md:15 asks for one line on the design fault. skills/fix-lints/SKILL.md forbids rubocop:disable and points at the exemption ladder; it asks no smell question.

Approach

One presenter owns the block an agent reads about offenses. Every path above calls it.

Required work: none beyond the acceptance criteria.

Owner sketch

module OsSdlc
  class Presenter::OffenseAnalysis
    QUESTIONS = <<~TEXT
      For each cop above, answer in one line before you change code:
      1. What smell does this cop name?
      2. What is the correct way to address it?
      An exemption proposal must quote your answer to question 1. A human applies it, never you.
    TEXT

    def initialize(library:, seen_keys: [])
      @relay = Lesson::Relay.new(library: library, seen_keys: seen_keys)
    end

    def render(offenses)
      return "" if offenses.empty?
      [offenses.map(&:to_s).join("\n"), @relay.render(offenses.map(&:cop_name).uniq), QUESTIONS].reject(&:empty?).join("\n\n")
    end
  end
end

LintReport::Offense (cop_name, path, line, message, to_s) is the one offense value. Worklist::Offense and LintOffenseLine::Entry convert to it; Worklist::Offense#detail_line and the FileUnit#prompt format go away.

Callers delegate:

# presenter/gate/reply.rb
def lint_output(output) = analysis.render(LintReport.parse(output).offenses)
# model/worklist.rb and model/residual_worklist.rb
def prompt = "#{file}\n\n#{analysis.render(offenses)}"
# controller/lint_worklist.rb, iterative phase
puts analysis.render(remedy_offenses)
# presenter/brief.rb
def rendered_lint_offense_history(entries) = analysis.render(entries.map(&:offense))

The gate keeps its seen-set from the dispatch. The fix-lints paths have no dispatch and pass no seen keys, so a fresh agent always gets the full lesson. The library is Lesson::Library.new(dir: File.join(PluginRoot::PATH, "lint-lessons")) in every caller.

Agent and skill text: agents/lint-fixer.md, agents/programmer.md, agents/refactorer.md (replace line 15), and skills/fix-lints/SKILL.md each get one line: "Answer the two questions in the offense block before you edit." No copy of the questions in prose.

AC5 is a prompt rule in this change, carried by QUESTIONS on every path. A tested gate step that rejects an Exclude without a named smell is not required work; one incident so far.

Acceptance criteria

  1. A survey of how each lint-responding skill and agent receives offenses is recorded in the ticket or in the change.
  2. Every skill and agent that responds to lint offenses gets the lint lessons from the same source.
  3. Each offense shown to a fixing agent comes with the two questions: "What smell does this cop name?" and "What is the correct way to address it?"
  4. The offense analysis has one shared implementation, not one copy per workflow.
  5. A fixing agent cannot add an exemption for a cop without first naming the smell.

Origin

  • Session: 44cf3646-a8fe-4f26-8550-e92296c57f86
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-ruby-gems-n8n-api/44cf3646-a8fe-4f26-8550-e92296c57f86.jsonl
  • Where: /home/jared/dev/ruby-gems/n8n-api (filed from n8n-api; code lives in /home/jared/dev/cc-os/plugins/os-sdlc)

Discoverer: /home/jared/dev/ruby-gems/n8n-api, session 44cf3646-a8fe-4f26-8550-e92296c57f86, 2026-10-11. A pipeline programmer exempted a cop instead of finding the smell, because lint lessons and smell questions do not reach it.

## Problem Lint lessons reach only one place today: the pipeline gate. Other agents and skills that fix lint offenses never see them. None of them ask the agent to name the smell before it fixes or exempts an offense. 1. The gate loads the lessons. `lib/os_sdlc/command/gate/run.rb` line 9 loads `Lesson::Library.new(dir: .../"lint-lessons")`. Lines 72, 80, and 102-103 build `Lesson::Relay`. The renderer is `lib/os_sdlc/presenter/lesson/relay.rb`. 2. The other lint paths do not read lessons. `skills/fix-lints`, `bin/lint-worklist`, and the worklist code never read them. 3. No instruction asks the agent to name the smell. Only `agents/refactorer.md:15` says "state in one line what design fault it hints at". `agents/programmer.md` has nothing on this. 4. The stop rule is weak. `skills/fix-lints/SKILL.md:46-47` says to stop and report if a cop looks wrong. The ladder `skills/fix-lints/references/lint-exemption-ladder.md:13` suggests only `rubocop --show-cops`. All paths are under the plugin root `plugins/os-sdlc`. Observed cost (n8n-api, issue jaredswanson/n8n-api#2, commit c51ebb3): a pipeline programmer added a `Naming/PredicateMethod` Exclude for 13 resource methods that always returned `true`. It did not ask why a method returns a constant. A human review found the real smell. ## Survey (AC1) Five places format rubocop offenses for an agent. One adds a lesson. All paths under `plugins/os-sdlc`. | Path | Formats offenses in | Offense object | Lesson | | --- | --- | --- | --- | | gate reply | `lib/os_sdlc/presenter/gate/reply.rb` `lint_output` via `LintFeedback` | raw rubocop JSON hashes | yes, `Lesson::Relay` with `seen_keys: dispatch.violations_dataset.keys` (`command/gate/run.rb:103`) | | batch assignment file | `lib/os_sdlc/model/worklist.rb` `Offense#detail_line` | `Worklist::Offense` (`cop`, `file`, `line`, `message`) | no | | residual prompt | `lib/os_sdlc/model/residual_worklist.rb` `FileUnit#prompt` | `Worklist::Offense` | no | | iterative stdout | `lib/os_sdlc/controller/lint_worklist.rb` `print_iterative_cop` | `Worklist::Offense` | no | | brief `lint_offense_history` | `lib/os_sdlc/presenter/brief.rb` `rendered_lint_offense_history` via `LintOffenseLine` | `LintOffenseLine::Entry` built from `LintReport::Offense` | no | Agents: `agents/lint-fixer.md` reads an assignment file verbatim. `agents/programmer.md` sees offenses only through gate reruns. `agents/refactorer.md:15` asks for one line on the design fault. `skills/fix-lints/SKILL.md` forbids `rubocop:disable` and points at the exemption ladder; it asks no smell question. ## Approach One presenter owns the block an agent reads about offenses. Every path above calls it. Required work: none beyond the acceptance criteria. ## Owner sketch ```ruby module OsSdlc class Presenter::OffenseAnalysis QUESTIONS = <<~TEXT For each cop above, answer in one line before you change code: 1. What smell does this cop name? 2. What is the correct way to address it? An exemption proposal must quote your answer to question 1. A human applies it, never you. TEXT def initialize(library:, seen_keys: []) @relay = Lesson::Relay.new(library: library, seen_keys: seen_keys) end def render(offenses) return "" if offenses.empty? [offenses.map(&:to_s).join("\n"), @relay.render(offenses.map(&:cop_name).uniq), QUESTIONS].reject(&:empty?).join("\n\n") end end end ``` `LintReport::Offense` (`cop_name`, `path`, `line`, `message`, `to_s`) is the one offense value. `Worklist::Offense` and `LintOffenseLine::Entry` convert to it; `Worklist::Offense#detail_line` and the `FileUnit#prompt` format go away. Callers delegate: ```ruby # presenter/gate/reply.rb def lint_output(output) = analysis.render(LintReport.parse(output).offenses) # model/worklist.rb and model/residual_worklist.rb def prompt = "#{file}\n\n#{analysis.render(offenses)}" # controller/lint_worklist.rb, iterative phase puts analysis.render(remedy_offenses) # presenter/brief.rb def rendered_lint_offense_history(entries) = analysis.render(entries.map(&:offense)) ``` The gate keeps its seen-set from the dispatch. The fix-lints paths have no dispatch and pass no seen keys, so a fresh agent always gets the full lesson. The library is `Lesson::Library.new(dir: File.join(PluginRoot::PATH, "lint-lessons"))` in every caller. Agent and skill text: `agents/lint-fixer.md`, `agents/programmer.md`, `agents/refactorer.md` (replace line 15), and `skills/fix-lints/SKILL.md` each get one line: "Answer the two questions in the offense block before you edit." No copy of the questions in prose. AC5 is a prompt rule in this change, carried by `QUESTIONS` on every path. A tested gate step that rejects an `Exclude` without a named smell is not required work; one incident so far. ## Acceptance criteria 1. A survey of how each lint-responding skill and agent receives offenses is recorded in the ticket or in the change. 2. Every skill and agent that responds to lint offenses gets the lint lessons from the same source. 3. Each offense shown to a fixing agent comes with the two questions: "What smell does this cop name?" and "What is the correct way to address it?" 4. The offense analysis has one shared implementation, not one copy per workflow. 5. A fixing agent cannot add an exemption for a cop without first naming the smell. ## Origin - Session: 44cf3646-a8fe-4f26-8550-e92296c57f86 - Transcript: /home/jared/.claude/projects/-home-jared-dev-ruby-gems-n8n-api/44cf3646-a8fe-4f26-8550-e92296c57f86.jsonl - Where: /home/jared/dev/ruby-gems/n8n-api (filed from n8n-api; code lives in /home/jared/dev/cc-os/plugins/os-sdlc) -------- **Discoverer:** /home/jared/dev/ruby-gems/n8n-api, session 44cf3646-a8fe-4f26-8550-e92296c57f86, 2026-10-11. A pipeline programmer exempted a cop instead of finding the smell, because lint lessons and smell questions do not reach it.
Author
Owner

Work started on branch ticket-989 (os-sdlc implement pipeline, poodr-ticket-implementation map).

Work started on branch ticket-989 (os-sdlc implement pipeline, poodr-ticket-implementation map).
Author
Owner

Resolution

Done: One OsSdlc::OffenseAnalysis owns the lint lessons plus the two smell questions (what smell does this cop name; what is the correct way to address it) and every offense path calls it: gate reply, batch assignment file, residual assignment file, iterative stdout, and the brief's lint_offense_history. Survey of the five paths recorded in the ticket Approach. lint-fixer, programmer, refactorer, and fix-lints each gained the one-line rule to answer both questions before editing; an exemption proposal must quote the named smell.

Evidence: main commits 59064bb7, 38d979be, 5beb1580 via os-sdlc implementation 36 (15 dispatches, 13 min 54 s, one escalation rerouted at programmer-repair). Suite 1540 runs, 0 failures. Lens challenge on A10/A11 and A15 found two code defects; both fixed in 5beb1580 (residual guidance moved to ResidualDispatchReport; OffenseAnalysis moved to lib/os_sdlc/offense_analysis.rb).

Follow-ups: #994 captured: lesson for Naming/PredicateMethod, the stock cop whose Exclude started this ticket (ticket-skeptic VERDICT: CREATE). Dropped: unifying the four per-path offense line formats onto LintReport::Offense; the formats are pinned by tests and no longer differ in what the agent learns. AC5 remains a prompt rule; a tested gate step that rejects an Exclude without a named smell is not filed, one incident so far.

## Resolution **Done:** One OsSdlc::OffenseAnalysis owns the lint lessons plus the two smell questions (what smell does this cop name; what is the correct way to address it) and every offense path calls it: gate reply, batch assignment file, residual assignment file, iterative stdout, and the brief's lint_offense_history. Survey of the five paths recorded in the ticket Approach. lint-fixer, programmer, refactorer, and fix-lints each gained the one-line rule to answer both questions before editing; an exemption proposal must quote the named smell. **Evidence:** main commits 59064bb7, 38d979be, 5beb1580 via os-sdlc implementation 36 (15 dispatches, 13 min 54 s, one escalation rerouted at programmer-repair). Suite 1540 runs, 0 failures. Lens challenge on A10/A11 and A15 found two code defects; both fixed in 5beb1580 (residual guidance moved to ResidualDispatchReport; OffenseAnalysis moved to lib/os_sdlc/offense_analysis.rb). **Follow-ups:** #994 captured: lesson for Naming/PredicateMethod, the stock cop whose Exclude started this ticket (ticket-skeptic VERDICT: CREATE). Dropped: unifying the four per-path offense line formats onto LintReport::Offense; the formats are pinned by tests and no longer differ in what the agent learns. AC5 remains a prompt rule; a tested gate step that rejects an Exclude without a named smell is not filed, one incident so far.
jared closed this issue 2026-10-11 22:47:49 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#989
No description provided.