8000 adding rubocopy support as ruby plugin by ghouston · Pull Request #150 · habit-hooks/habit-hooks · GitHub
[go: up one dir, main page]

Skip to content

adding rubocopy support as ruby plugin - #150

Closed
ghouston wants to merge 23 commits into
habit-hooks:mainfrom
ghouston:add-rubocop-support
Closed

ghouston wants to merge 23 commits into
habit-hooks:mainfrom
ghouston:add-rubocop-support

Conversation

@ghouston
@ghouston ghouston commented Aug 25, 2026 •
Copy link
Copy Markdown
Contributor

Add a ruby plugin wrapping rubocop

Adds habit-hooks-ruby plugin wrapping rubocop. The sensor runs the project's own rubocop and translates what comes back into the smell vocabulary, so a Rails app gets coaching instead of a cop list.

What it does

A finding looks like this (one method tripping all three complexity cops, folded into a single coached block):

── high-complexity (3 issues) ──

High complexity means one method makes too many decisions at once. RuboCop measures it three ways: cyclomatic counts branches, perceived weights nesting, AbcSize counts assignments and calls. One method can therefore be listed more than once below. The count is the symptom; tangled responsibilities are the cause.
...

app/services/billing.rb:14 rubocop:Metrics/CyclomaticComplexity
  Cyclomatic complexity for charge is too high. [12/7]
app/services/billing.rb:14 rubocop:Metrics/PerceivedComplexity
  Perceived complexity for charge is too high. [13/8]
app/services/billing.rb:14 rubocop:Metrics/AbcSize
  Assignment Branch Condition size for charge is too high. [38.4/20]

The decisions worth reviewing

The project's .rubocop.yml decides everything. The sensor passes no --only and no --config. RuboCop finds the project's config by its own upward walk, so a habit-hooks run is the run the project gets from rubocop directly. There is no shipped fallback config. --force-exclusion is the one flag added, because rubocop otherwise lints files named on the command line even when AllCops: Exclude covers them.

Ten cops map to canonical smells. Metrics/ParameterLists to too-many-parameters, MethodLength to oversized-function, the three complexity cops to high-complexity, and so on. Metrics/ClassLength and Metrics/ModuleLength stay unmapped on purpose: they measure a class, and the vocabulary has no class-scoped smell for them to back.

Unmapped cops are forwarded under their own names, the same exception eslint has. A cop that fired is one the project's own config turned on, so it is the project's vocabulary. It renders through uncoached.md and follows the root uncoached policy. The plugin ships its own uncoached.md that keeps rubocop's message visible and points [Correctable] findings at rubocop -a first.

The binstub problem. rubocop is a RubyGems binstub with a #!/usr/bin/env ruby shebang, only as good as the ruby that answers first. Pointed at the wrong interpreter it dies in find_spec_for_exe with exit 1, the code reserved for "offences found". Judged on the exit code alone that is a false clean, the class #88 exists for. So the report is the evidence: --format json prints its envelope on every completed run, and no envelope means no run. project_paths also looks along the project's bin/ first, so bundle binstubs rubocop puts the bundled rubocop (the one that can load rubocop-rails and friends) in front of whatever the machine has.

Issue listings carry the cop and its message. The plugin ships guides/includes/rubocop_line_level_issues.md, which renders file:line rubocop:<cop> with rubocop's message indented underneath. A correctable offence adds rubocop's own [Correctable] tag to the source. RuboCop's JSON has no safety flag per offence, so the tag means an autocorrect exists, nothing more.

Testing

  • plugins/ruby/docs/ruby-plugin.spec.md: the acceptance spec, same shape as the other plugins.
  • 43 unit tests. The split worth noting: test_the_rubocop_pipeline_maps_cops_to_smells.py says what rubocop's output becomes with no subprocess; test_the_rubocop_sensor_runs_the_real_tool.py proves the real tool still produces output of that shape. A rubocop upgrade that renames cop_name fails the second alone.

Manually verified end-to-end against a real Rails app: the plugin runs the app's bundled rubocop, honours its inherited configs, and coached fabricated smells through both the mapped and the uncoached paths.

Notes for the reviewer

  • CI gets a ruby leg (.github/workflows/ci.yml), and release.yml maps the new package to its own PyPI environment, per "Each released package needs its own publish environment".
  • The plugin floors follow the rc3 spelling (>=1.4.dev0,<2) and the floor gate passes.
  • Two things surfaced while testing that I deliberately did not change here; both are questions for the maintainer first (possibly for future core changes):
    1. resolve_guide's uncoached.md fallback uses raw config.plugins order rather than the language-ordered list it computed a line earlier. With two language plugins, an uncoached finding can render the other language's fallback guide. This plugin's own uncoached.md is what made that visible.
    2. The "detected typescript" recommendation hint has no off switch. A project that has considered and declined a plugin gets re-pitched on every run.

@ghouston
ghouston marked this pull request as ready for review August 26, 2026 16:29
@ghouston ghouston mentioned this pull request Aug 26, 2026
8000
@devill
devill commented Aug 27, 2026
Copy link
Copy Markdown
Collaborator

I didn't have time yet to review the entire PR, but I did notice that many of the guides are duplicated from the generic plugin. Was that intentional? The point of the generic plugin is to avoid repeating the same advice in ever plugin.

@ghouston
ghouston commented Aug 28, 2026 •
Copy link
Copy Markdown
Contributor Author

I didn't have time yet to review the entire PR, but I did notice that many of the guides are duplicated from the generic plugin. Was that intentional? The point of the generic plugin is to avoid repeating the same advice in ever plugin.

Yes it was intentional. 1. Several guides are modified for ruby. 2. I needed a different format for the line issues and unfortunately that is not easily plug-able without copying the guide and changing that code in the md.

the line output is shown in this sample. it include the source (rubocop:Metrics/CyclomaticComplexity), message and score:

── high-complexity (3 issues) ──

High complexity means one method makes too many decisions at once. RuboCop measures it three ways: cyclomatic counts branches, perceived weights nesting, AbcSize counts assignments and calls. One method can therefore be listed more than once below. The count is the symptom; tangled responsibilities are the cause.
...

app/services/billing.rb:14 rubocop:Metrics/CyclomaticComplexity
  Cyclomatic complexity for charge is too high. [12/7]
app/services/billing.rb:14 rubocop:Metrics/PerceivedComplexity
  Perceived complexity for charge is too high. [13/8]
app/services/billing.rb:14 rubocop:Metrics/AbcSize
  Assignment Branch Condition size for charge is too high. [38.4/20]

@devill
devill commented Aug 28, 2026
Copy link
Copy Markdown
Collaborator

Yes it was intentional. 1. Several guides are modified for ruby. 2. I needed a different format for the line issues and unfortunately that is not easily plug-able without copying the guide and changing that code in the md.

the line output is shown in this sample. it include the source (rubocop:Metrics/CyclomaticComplexity), message and score:

Okay, that makes sense.

Just to be clear: what follows is me trying to understand how to improve the framework itself to avoid having to repeat the guides in a plugin, not a criticism of this PR.

So the part that I'd like to understand:

  1. Why do you prefer to have the source of the report in the line level details?

My design philosophy was to deliberately not include that, since it invites the agent to try and game the metric. In ideal case habit hooks is there to coach the agent on how to improve the code, and hide the actual signal as much as possible. (It tends to figure it out anyway if it really wants, but let's not make it easy) If the agent know the signal it starts to focus on making the signal go away instead of fixing the actual design issue.

  1. What modifications were necessary?

I do see that casing is different in some files. Were there any other modifications necessary? (I could imagine a feature where the plugin specifies casing rules and the generic templates contain a formatting function call to ensure correct casing for the language)

I'll now review the PR in more depth.

@ghouston
Copy link
Copy Markdown
Contributor Author

@devill fyi - i am looking at the CI failures. i will have some updates soon.

devill added 4 commits August 28, 2026 20:14
Use a writable Ruby toolchain on both CI platforms so the pinned RuboCop
installation can complete, and keep the new plugin at the core release version.
Keep the Ruby sensor below the repository's file-size gate by separating
process execution from report parsing and finding shaping.
Leave autofix metadata to RuboCop, and protect scoped filenames from option and
glob interpretation before invoking the tool.
Keep vendored and temporary Rakefiles, Gemfiles, gemspecs, and rake files out
of the Ruby scope without excluding other languages.
Comment thread .github/workflows/ci.yml Resolved
Keep a language-specific fallback from coaching findings produced by another
language plugin; use the generic or core guide instead.
@devill
devill commented Aug 28, 2026
Copy link
Copy Markdown
Collaborator

The core uncoached.md was never intended to be replaced by a different-language plugin, and adding the Ruby-specific one revealed a gap that is now fixed on main.

I’m still not really sure about this solution, though. I’m leaning on accepting it for now, but my main concern is that smells that are generic across languages should keep the generic prompt templates. This way, any improvement in one language can improve the experience for everyone. Copying and modifying the templates opts Ruby out of that.

@ghouston
Copy link
Copy Markdown
Contributor Author

fyi - additional notes regarding the changes in the ruby guides:

  1. I made several changes that better matched ruby (function changed to method, added block syntax, etc).
  2. I used matt pocock's /writing_for_agents skill to improve the writing of the guide. link The results of the skill are all in commit 59043d04.
  3. I did a few manual changes to the guides based on my own experience and taste.

@ghouston
Copy link
Copy Markdown
Contributor Author

@devill thanks! i've incorporated your changes and addressed the sensor failure case. This PR is ready for review again.

devill added 3 commits August 28, 2026 23:59
Keep project-local tool directories in plugin configuration so only the tools
that need them search those directories.
Keep path-list separators out of detector-owned search paths and document the
plugin-scoped lookup precisely.
Keep bare argv programs on the same detector-owned search path used by setup and
placeholder expansion.
@devill
devill commented Aug 29, 2026
Copy link
Copy Markdown
Collaborator

Alright! I will look at the diffs between the duplicated prompts and the original. I'm still more in favour of modifying the generic prompts then duplicating the knowledge between languages. I'm open to having a ruby-prompts plugin though that has further customisations.

@devill
devill commented Aug 29, 2026
Copy link
Copy Markdown
Collaborator

The current change adds project/bin to the core search path, which affects every command detector. A potential fix is 2486d51. It moves the extra path into the RuboCop detector declaration, so only RuboCop searches bin, while setup, ${detector:rubocop}, and the supported bare argv[0] form still use the same resolved file. If you agree, please cherry pick this commit too.

PR habit-hooks#150 forks nine prompts that already exist. Seven say the same thing once
Ruby nouns are substituted, which the generic guides now carry as
"function/method", so they are dropped and the generic guide renders.

Two remain. parse-error states RuboCop facts no shared guide can hold: ruby -c,
TargetRubyVersion, cops rather than rules. oversized-block is a smell only this
plugin reports, so nothing else coaches it.

The forked line-level include goes with them. It existed to print the cop's
message, which is the metric the guides deliberately do not hand the agent, so
oversized-block uses the shared listing.
@devill
devill commented Aug 29, 2026
Copy link
Copy Markdown
Collaborator

Looking at the prompt changes there are many different categories of changes. For some I'd like to move them into generic, for some I very much disagree, and for some we should probably add features that let plugins customise vocabulary.

In the interest of moving forward I suggest cutting the prompts from this PR with some notable exceptions (see other comments) and I will open separate issues with draft PRs for the other changes.

Hope that is an acceptable trade-off for you. Since Habit hooks allows for project level overrides of guides you can continue using your variants by moving them to be project level overrides.

I've put the cut on add-rubocop-support as 66b0a8c if you want to cherry-pick it rather than redo it by hand.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd like to keep this one. While reading it I realised the generic guide you were overriding was written entirely for ESLint, so it shouldn't have been sitting in generic in the first place. I've moved it over to the typescript plugin and opened #153 to write a proper generic one.

Fun fact: the original was added by an agent unprompted and I didn't notice. Its wording was terrible, so the generic one will probably be based on yours instead. Gotta love agents... they love going behind your back when you rush :D

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd like to keep this one as well. For now only this plugin reports oversized-block, but since the feature itself exists in other languages it will probably be moved into generic with a slightly changed wording.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is the one I'd most like to drop, together with the uncoached guide that uses it.

It prints RuboCop's message, and the message carries the count — something like Cyclomatic complexity for foo is too high. [8/7]. I'd rather not hand the agent that number, because it invites gaming the metric. Sometimes I include the limit, but never the current value. Goodhart's law is real with AI agents. Especially the smarter ones. Once you give them the metric, they will aim for the easiest way to reach the target. (See also recent instances where frontier models hacked into places. In every single report the common thread was over optimising on getting to a metric instead of solving the problem.)

The cop name is already printed in the banner above each block, so you don't lose anything by falling back to the shared listing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

in the interest of moving forward i am agreeable. i can experiment using project level coaching to understand how the agent behaves. if i find a significant difference, it can go into a future PR.

@devill
devill commented Aug 29, 2026
Copy link
Copy Markdown
Collaborator

Once you rebase onto orign/main and cherry pick the temporary removal of the prompts from this PR I'll merge it. The prompts won't go lost, I'm opening the issues and draft PRs right now.

@ghouston
ghouston commented Sep 4, 2026
Copy link
Copy Markdown
Contributor Author

fyi - i like most of the changes you recommended. i want to trial them as i incorporate them. so there will be a little delay to get everything updated.

@devill question: have you considered getting habit hooks to also provide a human only output? the human view would provide the details of the failing sensor, scores, etc. this would allow the human to decide which sensor rules to enable/disable, and priorities about what to fix. it would also help the human in the loop to review the changes made by the ai.

i have another unpublished project (thinking about publishing it this fall), which is a sidecar to run things like habit-hooks. it provides a UI to let the human see a human summary and details of the failing checks/sensors/evals, and it provides an API the AI can curl or hook into to get the ai written version of the checks/sensors.

@devill
devill commented Sep 4, 2026
Copy link
Copy Markdown
Collaborator

I didn't really consider that. My approach was to make sure I have to review as little as possible. But I'm open to adding something like that if you have a workflow that really needs it.

@ghouston
ghouston commented Sep 4, 2026
Copy link
Copy Markdown
Contributor Author

@devill PR ready for review. I've incorporated your changes.

I did a rewrite of the ruby plugin README.md to be a guide for ruby plugin setup and configuration, plus customization. The customization was more involved than it should be. Customization in some cases required copying py and toml files, rather than just making a small config or override change. Maybe the core of habit-hooks could make customization easier in the future.

devill added a commit that referenced this pull request Sep 5, 2026
PR #150 forks nine prompts that already exist. Seven say the same thing once
Ruby nouns are substituted, which the generic guides now carry as
"function/method", so they are dropped and the generic guide renders.

Two remain. parse-error states RuboCop facts no shared guide can hold: ruby -c,
TargetRubyVersion, cops rather than rules. oversized-block is a smell only this
plugin reports, so nothing else coaches it.

The forked line-level include goes with them. It existed to print the cop's
message, which is the metric the guides deliberately do not hand the agent, so
oversized-block uses the shared listing.
@devill
devill commented Sep 5, 2026
Copy link
Copy Markdown
Collaborator

Merged — thank you for putting in the effort.

I rebased onto main so the history stays linear; your authorship is preserved.

Four small fixes I added after merging:

  • Setup said plugins = ["ruby"]. That key replaces the default list rather than
    adding to it, so generic is removed, and 8 of the 10 cops this plugin maps
    lose their prompts.
  • bin is the rubocop detector's own search path, not the default one every tool
    shares. The root README and the changelog entry said otherwise.
  • The custom-sensor example spelled the sensor custom_check in its last
    snippet. A sensor's name is its recipe's filename, so only custom-check
    loads.
  • Two typos.

@devill devill closed this Sep 5, 2026
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.

2 participants

0