Repository navigation
Releases
v1.5.0
Ruby. A Gemfile or a .rubocop.yml in your project and habit-hooks now
coaches your Ruby the way it already coached your Python and TypeScript.
Added
-
The
rubyplugin.pip install "habit-hooks[ruby]", add"ruby"to
plugins, and arubocopsensor translates cop names into the same smell
vocabulary every other plugin speaks:too-many-parameters,
oversized-function,oversized-block,high-complexity,deep-nesting,
unused-variable,swallowed-exceptionandparse-error. All three Metrics
complexity cops —
CyclomaticComplexity,PerceivedComplexityandAbcSize— coach as
high-complexity, since a nesting-heavy or call-heavy method can trip one
without the others.Your
.rubocop.ymldecides everything. The sensor passes no--onlyand no
--config, so RuboCop discovers your config by its own upward walk and your
habit-hooks run is the run you get from RuboCop directly. A cop this plugin
has no smell for is forwarded under its own name rather than dropped, because
a cop only fires if your config switched it on.--force-exclusionis the one
flag the sensor adds, soAllCops: Exclude:keeps applying once habit-hooks
names files on the command line. -
A detector can name the directories its own tool lives in, through
search_pathsin a plugin'sconfig.toml. The ruby plugin's rubocop detector
namesbin, wherebundle binstubswrites binstubs, and for Ruby that matters
more than a version difference usually does: a.rubocop.ymlnaming
rubocop-railsorrubocop-rspeccannot be read at all by a rubocop outside
the bundle those gems live in. It stays off the default search path, which
every tool shares, becausebinis a name a project may keep its own scripts
under. -
Java reports
deep-nesting. PMD'sAvoidDeeplyNestedIfStmts, pinned to
problemDepth=3in the bundled ruleset, so the third nestedifis the first
one reported. A project running its own PMD ruleset gets the smell only when
that ruleset enables the rule itself. Contributed by @renanfranca (#160).
Fixed
-
A wrapped tool that never started no longer reads as a clean file. The
rubocopbinstub begins#!/usr/bin/env ruby, so pointing it at an
interpreter it is not installed into makes it die infind_spec_for_exeand
exit 1 — the code that otherwise means "I found offences". The sensor judges
the report rather than the exit code: RuboCop prints its JSON envelope on
every run it completed, so no envelope means no run, and the run fails instead
of reporting nothing wrong. -
An uncoached smell no longer coaches in another language. A smell no
plugin ships a guide for falls back to theuncoachedguide, and that
fallback was resolved across every active plugin rather than the ones the
finding's own language names — so a project running two language plugins
could be handed the wrong language's wording for its own file. The fallback
now follows the same language-filtered order every other guide does.
v1.4.0
Native Windows support, plus everything else the two Windows reporters found on
the way — none of which is a Windows bug; all of it reproduces on any platform.
Reported by @FrankRaiser (#133, #140, #143) and @imalliaras (#134, #142),
neither of whom could get a run to complete when they started.
Fixed
-
habit-hooks runs on native Windows. Every sensor was spawned through
bash -c, which Windows has not got — and wherebashdoes resolve there it
is usuallyC:\Windows\System32\bash.exe, the WSL launcher, which answers
about a different filesystem. Nine of the eleven shipped sensors and
transformers never needed a shell; the other two only needed one to reach
jq. All eleven now spellargvand are spawned directly. -
Nothing decodes in the console's codepage any more. Every guide, config
and spec file this tool reads is UTF-8 and now says so. On a cp1252 console
the first guide it rendered killed the run with aUnicodeDecodeError(#133);
the same bug hit any non-UTF-8 locale, includingLC_ALL=Con Linux. The
streams it writes are UTF-8 too, so a printed block's box-drawing characters
cannot fail on the way out. -
A tool installed as
.cmdor.batis found. Windows appends only.exe
to a bare command name, sojscpd.CMD,knip.cmdandpmd.batwere cleared
at setup and unreachable at run time. One lookup now answers both questions. -
initno longer offers an install it cannot run. Its install command went
throughshell=True, which on Windows iscmd.exe, andcmd.exedoes not
understand the quotes inuv tool install 'habit-hooks[python]'. -
A wedged sensor still says why. Windows raises its timeout carrying no
output at all, so the notice quoted back nothing where the tool had explained
itself. -
A mistyped
argvin a sensor spec answers with a sentence.argv = "ruff"
is a string, and a string is iterable: it spawned four one-character arguments
and the run reported needing thercommand. A non-string element raised a
traceback. Both are now refused by name at exit 2 — the code for a broken
tool — instead of exit 1, the code reserved for a finding to fix. -
A sensor no longer dies without a word when its tool has a lot to say.
Node caps a captured stream at 1 MB unless told otherwise, and the eslint
sensor never told it otherwise: past that, the spawn came back with no exit
status, a truncated report and an empty stderr, and the sensor forwarded the
empty stderr. The whole notice was a command line. Around 100 files with
findings is enough to cross it, on any platform (#142). What a project tool's
run answered — how much of it there may be, whether it broke, and what to say
about it — is now one seam's question rather than each caller's, so the two
callers cannot answer it differently again. A complaint can no longer be empty. -
--allno longer scans what git ignores. Every git-derived mode asks git
and so had never seen a build artifact; a whole-project run walked the
directory tree and measured everything it found. One project, two universes:
a real pnpm monorepo held 321 tracked.ts/.tsxfiles and 843 on disk, and
its owner had to hand-write!**/dist/**into[files]before a run was
usable (#142). A project git cannot answer for — outside a repository, on a
machine with no git, or one that is itself ignored by the repository around it
— still measures what is on disk, because a run that silently scans nothing is
worse than one that over-scans. A submodule's files leave the scope, as they
always had from every git-derived mode, and the run now says which submodule
rather than quietly measuring less. -
A scan that measured nothing says so. A
[files]that matches no file —
because it is too narrow, or because git ignores the tree it was written for —
used to render the clean guide over a run that had read nothing at all. -
A smell is coached once, not once per sensor that saw it. Two sensors can
report one smell — eslint'smax-linesand the line counter both mean
oversized-file— and the mapper printed a block per finding, so the same
~200-word guide arrived twice (#140). Findings that resolve to the same guide
are now one finding. A sensor's own issue list is never thinned: two unused
variables declared on one line are still two. -
habit-hooks no longer reports its own tools as unused dependencies. It
asks a project to installeslint,knip,ts-morph,jscpdand the two
@typescript-eslintpackages, and then reported all of them as declared and
imported nowhere (#143). The shipped knip config overlooks them. A project
with its own knip config is unaffected, as it is for every other setting. -
A failing tool's own words are bounded within a line, not only across
lines. The notice quoted back at most 20 lines and any number of bytes, and
eslint -f jsonis one line however many megabytes it holds — so a sensor
that died mid-report put the whole report into a coding agent's context. Both
ends of a long line now survive, with the middle named. -
A
[runners]command nobody installed answers in one line. Routing a smell
to an executable guide instead of a Markdown one, and naming a command that is
not there, printed a Python stack trace — the same first-contact mistake #114
answered everywhere else, in the one place that sweep did not reach. -
The knip sensor reports a column under the name the contract gives it.
knip spells itcol; every other sensor anddocs/sensor-interface.spec.md
spell itcolumn, so the position was invisible to anything asking by name.
Changed
-
jqis no longer a dependency of any plugin. Theruffandeslint
sensors piped their tool through it; both are now helpers beside their spec,
like every other sensor here. The only install hint we could offer was
brew install jq. -
A sensor spec may spell
argv = [...]. It is spawned as written, with no
shell, and it is the only form that runs on native Windows.command = "..."
remains for a plugin needing syntax a list cannot carry, and is refused off
POSIX rather than spawned into a shell that is not there. -
A sensor spec may name the tool it wraps.
${detector:<name>}expands to
the file this project runs for a tool the plugin declared in itsdetectors,
so the sensor is handed a path rather than a name something has to look up
again. Only the first word of a spawn was ever resolved, and no shipped sensor
spells its wrapped tool there — each is a helper that spawnsruff,deptry,
jscpd,pmdorphpone process further in, which is exactly where a
.cmdor.batshim went unfound. A declared tool that is not installed is
now answered before the sensor is spawned, so the notice naming it no longer
depends on each plugin's helper producing that answer itself. -
eslintandknipare found in the project, not onPATH. They are
resolved fromnode_modulesand run through Node directly, so a globally
installed copy is no longer used — the shipped eslint config could never have
worked with one, since it resolves its plugins from the project.initnames
npm install --save-dev eslint knipwhen they are missing. -
An argument that
cmd.exewould read as syntax is refused. A.bator
.cmdis run throughcmd.exe, and sensor arguments are filenames out of
whatever branch is checked out. The question is asked of every program those
arguments reach, the tool a sensor's helper spawns included — which is where
pmd.batand npm'sjscpd.CMDactually are.
Known limits
- The executable documentation under
docs/is POSIX shell and is skipped on
Windows (#137). The tool's own test suite runs there in full.
v1.4.0rc3
Reported by @imalliaras (#142) and @FrankRaiser (#140, #143). None of them is a
Windows bug: all three reproduce on any platform, and were found there only
because that is where these two were running.
Fixed
-
A sensor no longer dies without a word when its tool has a lot to say.
Node caps a captured stream at 1 MB unless told otherwise, and the eslint
sensor never told it otherwise: past that, the spawn came back with no exit
status, a truncated report and an empty stderr, and the sensor forwarded the
empty stderr. The whole notice was a command line. Around 100 files with
findings is enough to cross it, on any platform (#142). What a project tool's
run answered — how much of it there may be, whether it broke, and what to say
about it — is now one seam's question rather than each caller's, so the two
callers cannot answer it differently again. A complaint can no longer be empty. -
--allno longer scans what git ignores. Every git-derived mode asks git
and so had never seen a build artifact; a whole-project run walked the
directory tree and measured everything it found. One project, two universes:
a real pnpm monorepo held 321 tracked.ts/.tsxfiles and 843 on disk, and
its owner had to hand-write!**/dist/**into[files]before a run was
usable (#142). A project git cannot answer for — outside a repository, on a
machine with no git, or one that is itself ignored by the repository around it
— still measures what is on disk, because a run that silently scans nothing is
worse than one that over-scans. A submodule's files leave the scope, as they
always had from every git-derived mode, and the run now says which submodule
rather than quietly measuring less. -
A scan that measured nothing says so. A
[files]that matches no file —
because it is too narrow, or because git ignores the tree it was written for —
used to render the clean guide over a run that had read nothing at all. -
A smell is coached once, not once per sensor that saw it. Two sensors can
report one smell — eslint'smax-linesand the line counter both mean
oversized-file— and the mapper printed a block per finding, so the same
~200-word guide arrived twice (#140). Findings that resolve to the same guide
are now one finding. A sensor's own issue list is never thinned: two unused
variables declared on one line are still two. -
habit-hooks no longer reports its own tools as unused dependencies. It
asks a project to installeslint,knip,ts-morph,jscpdand the two
@typescript-eslintpackages, and then reported all of them as declared and
imported nowhere (#143). The shipped knip config overlooks them. A project
with its own knip config is unaffected, as it is for every other setting. -
A failing tool's own words are bounded within a line, not only across
lines. The notice quoted back at most 20 lines and any number of bytes, and
eslint -f jsonis one line however many megabytes it holds — so a sensor
that died mid-report put the whole report into a coding agent's context. Both
ends of a long line now survive, with the middle named. -
A
[runners]command nobody installed answers in one line. Routing a smell
to an executable guide instead of a Markdown one, and naming a command that is
not there, printed a Python stack trace — the same first-contact mistake #114
answered everywhere else, in the one place that sweep did not reach. -
The knip sensor reports a column under the name the contract gives it.
knip spells itcol; every other sensor anddocs/sensor-interface.spec.md
spell itcolumn, so the position was invisible to anything asking by name.
v1.4.0rc2
Native Windows support. Reported by @FrankRaiser (#133) and @imalliaras (#134),
neither of whom could get a run to complete.
New in rc2, on top of everything 1.4.0rc1 shipped: a sensor spec names the
tool it wraps, the cmd.exe argument guard reaches the tool a helper spawns,
and a mistyped argv answers with a sentence.
Fixed
-
habit-hooks runs on native Windows. Every sensor was spawned through
bash -c, which Windows has not got — and wherebashdoes resolve there it
is usuallyC:\Windows\System32\bash.exe, the WSL launcher, which answers
about a different filesystem. Nine of the eleven shipped sensors and
transformers never needed a shell; the other two only needed one to reach
jq. All eleven now spellargvand are spawned directly. -
Nothing decodes in the console's codepage any more. Every guide, config
and spec file this tool reads is UTF-8 and now says so. On a cp1252 console
the first guide it rendered killed the run with aUnicodeDecodeError(#133);
the same bug hit any non-UTF-8 locale, includingLC_ALL=Con Linux. The
streams it writes are UTF-8 too, so a printed block's box-drawing characters
cannot fail on the way out. -
A tool installed as
.cmdor.batis found. Windows appends only.exe
to a bare command name, sojscpd.CMD,knip.cmdandpmd.batwere cleared
at setup and unreachable at run time. One lookup now answers both questions. -
initno longer offers an install it cannot run. Its install command went
throughshell=True, which on Windows iscmd.exe, andcmd.exedoes not
understand the quotes inuv tool install 'habit-hooks[python]'. -
A wedged sensor still says why. Windows raises its timeout carrying no
output at all, so the notice quoted back nothing where the tool had explained
itself. -
A mistyped
argvin a sensor spec answers with a sentence.argv = "ruff"
is a string, and a string is iterable: it spawned four one-character arguments
and the run reported needing thercommand. A non-string element raised a
traceback. Both are now refused by name at exit 2 — the code for a broken
tool — instead of exit 1, the code reserved for a finding to fix.
Changed
-
jqis no longer a dependency of any plugin. Theruffandeslint
sensors piped their tool through it; both are now helpers beside their spec,
like every other sensor here. The only install hint we could offer was
brew install jq. -
A sensor spec may spell
argv = [...]. It is spawned as written, with no
shell, and it is the only form that runs on native Windows.command = "..."
remains for a plugin needing syntax a list cannot carry, and is refused off
POSIX rather than spawned into a shell that is not there. -
A sensor spec may name the tool it wraps.
${detector:<name>}expands to
the file this project runs for a tool the plugin declared in itsdetectors,
so the sensor is handed a path rather than a name something has to look up
again. Only the first word of a spawn was ever resolved, and no shipped sensor
spells its wrapped tool there — each is a helper that spawnsruff,deptry,
jscpd,pmdorphpone process further in, which is exactly where a
.cmdor.batshim went unfound. A declared tool that is not installed is
now answered before the sensor is spawned, so the notice naming it no longer
depends on each plugin's helper producing that answer itself. -
eslintandknipare found in the project, not onPATH. They are
resolved fromnode_modulesand run through Node directly, so a globally
installed copy is no longer used — the shipped eslint config could never have
worked with one, since it resolves its plugins from the project.initnames
npm install --save-dev eslint knipwhen they are missing. -
An argument that
cmd.exewould read as syntax is refused. A.bator
.cmdis run throughcmd.exe, and sensor arguments are filenames out of
whatever branch is checked out. The question is asked of every program those
arguments reach, the tool a sensor's helper spawns included — which is where
pmd.batand npm'sjscpd.CMDactually are.
Known limits
- The executable documentation under
docs/is POSIX shell and is skipped on
Windows (#137). The tool's own test suite runs there in full.
v1.4.0rc1
Native Windows support. Reported by @FrankRaiser (#133) and @imalliaras (#134),
neither of whom could get a run to complete.
Fixed
-
habit-hooks runs on native Windows. Every sensor was spawned through
bash -c, which Windows has not got — and wherebashdoes resolve there it
is usuallyC:\Windows\System32\bash.exe, the WSL launcher, which answers
about a different filesystem. Nine of the eleven shipped sensors and
transformers never needed a shell; the other two only needed one to reach
jq. All eleven now spellargvand are spawned directly. -
Nothing decodes in the console's codepage any more. Every guide, config
and spec file this tool reads is UTF-8 and now says so. On a cp1252 console
the first guide it rendered killed the run with aUnicodeDecodeError(#133);
the same bug hit any non-UTF-8 locale, includingLC_ALL=Con Linux. The
streams it writes are UTF-8 too, so a printed block's box-drawing characters
cannot fail on the way out. -
A tool installed as
.cmdor.batis found. Windows appends only.exe
to a bare command name, sojscpd.CMD,knip.cmdandpmd.batwere cleared
at setup and unreachable at run time. One lookup now answers both questions. -
initno longer offers an install it cannot run. Its install command went
throughshell=True, which on Windows iscmd.exe, andcmd.exedoes not
understand the quotes inuv tool install 'habit-hooks[python]'. -
A wedged sensor still says why. Windows raises its timeout carrying no
output at all, so the notice quoted back nothing where the tool had explained
itself.
Changed
-
jqis no longer a dependency of any plugin. Theruffandeslint
sensors piped their tool through it; both are now helpers beside their spec,
like every other sensor here. The only install hint we could offer was
brew install jq. -
A sensor spec may spell
argv = [...]. It is spawned as written, with no
shell, and it is the only form that runs on native Windows.command = "..."
remains for a plugin needing syntax a list cannot carry, and is refused off
POSIX rather than spawned into a shell that is not there. -
eslintandknipare found in the project, not onPATH. They are
resolved fromnode_modulesand run through Node directly, so a globally
installed copy is no longer used — the shipped eslint config could never have
worked with one, since it resolves its plugins from the project.initnames
npm install --save-dev eslint knipwhen they are missing. -
An argument that
cmd.exewould read as syntax is refused. A.bator
.cmdis run throughcmd.exe, and sensor arguments are filenames out of
whatever branch is checked out.
Known limits
- The executable documentation under
docs/is POSIX shell and is skipped on
Windows (#137). The tool's own test suite runs there in full.
v1.3.1
Discoverability and documentation. No behaviour changes.
Changed
-
Every package now has a PyPI page. All six shipped with an empty one: no
README, no keywords, no classifiers, no project links, no license. Each plugin
gains a README of its own, and the core's README is now its long description. -
The README says what the tool actually does. It had been wrong about six
things, five of them for several releases:- A plain
habit-hooksrun scans every file, not your branch's changes.
[scope] autoBranchOffMaindefaults tofalse, andinitwrites no
[scope]block. initwritespluginsand nothing else. The config the README showed added
files = ["**/*.py"], which replaces the python plugin's exclusions
wholesale — so--allthen walked the reader's.venv.- A language plugin's guide wins over
generic's wherevergenericsits in
plugins, so ordering it last was never load-bearing. - The override chain resolves to installed package data, not to a sibling
plugins/directory. - The sample output was invented. It is now a real run.
test-only-dead-codewas missing from the enforced list.
- A plain
-
Onboarding says how to start on a codebase that already has smells.
habit-sensors --all | habit-snooze --snoozenow appears where the first run
happens, rather than two hundred lines below it. -
docs/config.md's[scope]table gains a Default column. Both its
examples setautoBranchOffMain = truewhile the default isfalse, and
neither said so.
v1.3.0
Java. pom.xml or build.gradle in your project and habit-hooks now coaches
your Java the way it already coached your Python and TypeScript.
Added
-
The
javaplugin.pip install "habit-hooks[java]", add"java"to
plugins, and apmdsensor translates PMD 7's rule names into the same
smell vocabulary every other plugin speaks:too-many-parameters,
high-complexity,oversized-function,unused-variable,unused-import
andswallowed-exception. PMD exits 4 when it finds violations and 5 when it
cannot parse a file, so the sensor tells those apart — a crashed PMD fails
the run rather than reporting clean.Your own PMD ruleset wins. PMD never looks one up by itself, so the
sensor does: a--rulesetsyou pass through[sensors.pmd] argsfirst, then
the conventional places a Java project keeps one
(src/main/resources/pmd/ruleset.xml,pmd/ruleset.xml,ruleset.xml,
pmd.xml). The ruleset the plugin ships is only the answer to "this project
has none". Any other PMD flag —--aux-classpath,--minimum-priority—
goes throughargsto PMD untouched.Generated code is left alone: Maven's
target/and Gradle'sbuild/are
excluded from what the plugin calls source.habit-hooks initrecognises a
Java project and namespmdamong the tools to install.With thanks to @pfichtner, who wrote it.
Changed
- The
too-many-parametersguide names the concept, not the bag. The
default fix for a long parameter list is an options object named after the
function that takes it — the report clears and the missing abstraction stays
unnamed. The guide now says to search wider than the file that fired, because
values that keep appearing side by side are usually one of the domain's own
nouns; to use that entity everywhere it fits, since a call passing three of
its fields is the same concept under the threshold; and that aFooPropsis
the{ ...everything }bag under another name.
Fixed
-
A Python project that declares no dependencies could never get a clean
run. With nopyproject.tomland norequirements.txtthere is nothing for
deptry to check, and it says so by aborting — which the sensor read as a
broken tool. Every run came back as an incomplete one, and no change to the
code could make it green. A project that declares no dependencies has no
unused ones, so the sensor now reports nothing and the run completes. Every
other way deptry can fail still fails loudly. -
A broken tool was quoted back by the half of its output with nothing in
it. Past twenty lines, only the first twenty were shown — but a Python
traceback names what went wrong on its last line, so every sensor that
wraps a tool in Python answered a crash with framework internals and never
the error itself. Both ends are quoted now and the middle is counted, so the
diagnosis survives whichever end it landed on. The incomplete-run message no
longer promises a fuller diagnosis on stderr either, which was never there.
v1.2.1
Fixed
- Upgrading with
pipleft the plugins behind.pip install -U habit-hooksupgrades a dependency only when the new core stops being
satisfied by the installed one, and the core accepted any 1.x plugin — so an
upgrade to 1.2.0 gave you the new core with 1.1.0 plugins, and none of that
release's fixes, nearly all of which are in the plugins. Each plugin is now
floored at the core's own minor, and a test keeps the floor moving with it.
uv pip install --upgrade,uv tool upgradeandbrew upgradewere never
affected; if you upgraded withpipalready, upgrading again is enough.
v1.2.0
Everything here is about the first ten minutes. Installing habit-hooks was the
most frequent question the project got, and working through one user's report
found that three of the steps they had to figure out for themselves were our
bugs, not their setup.
Added
-
habit-hooks init. It detects the language your project is written in, writes
.habit-hooks/config.tomlenabling the plugins it needs, and lists what is still
missing — the plugins themselves, and the tools they reach for — beside the
command that installs each, offering to run them. The install command it prints matches how habit-hooks was
installed, so a plugin cannot land in some other Python than the one habit-hooks
runs from.Run it again on a project that already has a config and it changes nothing, only
reporting what is missing — so the same command answers "why is this run not
reporting anything?". For a language habit-hooks has no plugin for, it prints the
prompt to hand to your coding agent to build one. -
A plugin declares the tools it needs and how to install them. Each plugin's
config now carries adetectorslist — the tool's name, whether it is a command
onPATHor a package read as a library, and the command that installs it. A
third-party plugin gets this without the core learning its vocabulary.
Writing it down found two requirements nothing had ever stated: the typescript
plugin needsnodeitself, and the python plugin needsjq, which its ruff
sensor has always piped through.
Fixed
The first ten minutes
-
The typescript plugin's comment sensor now looks for
ts-morphin your project
rather than beside itself, so it no longer dies with a Node stack trace on every
install that puts habit-hooks outside your repository —pip,uv tooland
Homebrew alike. A project that has not installedts-morphis told to, in one
line, instead of being shown the module loader's. -
ts-morphis listed among the typescript plugin's detectors in the README,
which never mentioned it. -
A TypeScript project with no eslint config of its own is linted with the config
habit-hooks ships — which needs typescript-eslint in the project, something
neither the README nor the error said. The config now names itself and what to
install instead of failing with a module-loader stack trace, and the README
lists it. -
A
knipnobody installed is named the wayjscpd,deptryandphpalready
were — "install it, or disable the sensor" — instead ofError: spawnSync knip ENOENT. It was the one detector spawned from Node, and the only one still
missing that answer. -
A plugin's declared
filesreached into your dependencies. A project that
names nofilesof its own scans what its plugins declare, andhabit-hooks initwrites exactly such a config — so a bare**/*.tsmade the first run
report onnode_modules. On a small TypeScript project that was 23,573 findings
in other people's packages hiding the eight that were the project's own. The
typescript plugin now excludesnode_modules(nested copies in a monorepo
too), python excludessite-packagesand a virtualenv under either of the two
names one usually goes by, and php excludesvendor. An exclusion holds for
every active plugin, so a project running two languages cannot have one
plugin's globs hand back what another just excluded. -
Every
pluginsexample listedgenericfirst, which is the wrong way round:
the list is a lookup priority andgenericis the fallback, so listing it first
makes a language plugin's own guides unreachable — the python plugin ships
high-complexityandswallowed-exceptionguides exactly because those want a
Python answer, and neither could ever fire. The prose always said "falling back
togenericlast"; only the examples disagreed.habit-hooks initwrites the
right order. -
Naming two
uv tool installextras one after another loses the first. Each
one rebuilds the environment rather than adding to it, so following the README's
[python]line and then its[typescript]line left typescript alone — and a
Python project quietly stopped being checked. The README now names them in one
command and says why.piphas no such trap.
Documentation
- Install leads with
habit-hooks init; the four manual steps — install, add your
language's plugin, enable it, install the detectors — are still there for anyone
who wants them, because skipping one is the usual reason a first run reports
nothing. Every detector is listed with the command that installs it, php
included.
v1.0.0
First PyPI release of habit-hooks as a Python distribution.
Install: uv tool install habit-hooks / uvx habit-hooks / pip install habit-hooks — gives the core plus the language-agnostic generic plugin. Language plugins are opt-in: habit-hooks[python], [typescript], [php], or [all].
Published to PyPI: habit-hooks, habit-hooks-generic, habit-hooks-python, habit-hooks-typescript, habit-hooks-php (all 1.0.0).