habit-hooks 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. habit_hooks-1.0.0/.github/dependabot.yml +22 -0
  2. habit_hooks-1.0.0/.github/workflows/ci.yml +27 -0
  3. habit_hooks-1.0.0/.github/workflows/dependabot-auto-merge.yml +19 -0
  4. habit_hooks-1.0.0/.github/workflows/release.yml +48 -0
  5. habit_hooks-1.0.0/.gitignore +20 -0
  6. habit_hooks-1.0.0/.jscpd.json +6 -0
  7. habit_hooks-1.0.0/CHANGELOG.md +68 -0
  8. habit_hooks-1.0.0/CLAUDE.md +165 -0
  9. habit_hooks-1.0.0/LICENSE.md +21 -0
  10. habit_hooks-1.0.0/PKG-INFO +20 -0
  11. habit_hooks-1.0.0/README.md +370 -0
  12. habit_hooks-1.0.0/conftest.py +57 -0
  13. habit_hooks-1.0.0/docs/DECISIONS.md +134 -0
  14. habit_hooks-1.0.0/docs/architecture.md +226 -0
  15. habit_hooks-1.0.0/docs/authoring-plugins.spec.md +516 -0
  16. habit_hooks-1.0.0/docs/checklist.md +149 -0
  17. habit_hooks-1.0.0/docs/config.md +271 -0
  18. habit_hooks-1.0.0/docs/executable_spec.md +92 -0
  19. habit_hooks-1.0.0/docs/guide-includes.spec.md +256 -0
  20. habit_hooks-1.0.0/docs/habit-hooks.spec.md +133 -0
  21. habit_hooks-1.0.0/docs/habit-mapper.spec.md +671 -0
  22. habit_hooks-1.0.0/docs/habit-sensors.spec.md +560 -0
  23. habit_hooks-1.0.0/docs/habit-snooze.spec.md +271 -0
  24. habit_hooks-1.0.0/docs/sensor-interface.spec.md +127 -0
  25. habit_hooks-1.0.0/docs/smell-vocabulary.md +137 -0
  26. habit_hooks-1.0.0/habit-hooks +13 -0
  27. habit_hooks-1.0.0/habit-mapper +10 -0
  28. habit_hooks-1.0.0/habit-sensors +10 -0
  29. habit_hooks-1.0.0/habit-snooze +10 -0
  30. habit_hooks-1.0.0/plugins/generic/docs/generic-plugin.spec.md +134 -0
  31. habit_hooks-1.0.0/plugins/generic/package-lock.json +1440 -0
  32. habit_hooks-1.0.0/plugins/generic/package.json +8 -0
  33. habit_hooks-1.0.0/plugins/generic/pyproject.toml +17 -0
  34. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/.jscpd.json +7 -0
  35. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/__init__.py +6 -0
  36. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/config.toml +1 -0
  37. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/deep-nesting.md +14 -0
  38. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/duplicated-code.md +14 -0
  39. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/high-complexity.md +10 -0
  40. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/non-essential-comment.md +8 -0
  41. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/oversized-file.md +10 -0
  42. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/oversized-function.md +10 -0
  43. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/parse-error.md +6 -0
  44. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/swallowed-exception.md +10 -0
  45. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/too-many-parameters.md +10 -0
  46. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/unused-export.md +8 -0
  47. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/guides/warning-comment.md +2 -0
  48. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/jscpd.py +71 -0
  49. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/jscpd.toml +1 -0
  50. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/line-count.py +50 -0
  51. habit_hooks-1.0.0/plugins/generic/src/habit_hooks_generic/sensors/line-count.toml +2 -0
  52. habit_hooks-1.0.0/plugins/php/docs/php-plugin.spec.md +120 -0
  53. habit_hooks-1.0.0/plugins/php/pyproject.toml +17 -0
  54. habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/__init__.py +1 -0
  55. habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/config.toml +5 -0
  56. habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/guides/unused-variable.md +4 -0
  57. habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/sensors/phpmd.phar +0 -0
  58. habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/sensors/phpmd.toml +1 -0
  59. habit_hooks-1.0.0/plugins/php/src/habit_hooks_php/sensors/phpmd_sensor.py +91 -0
  60. habit_hooks-1.0.0/plugins/python/docs/python-plugin.spec.md +249 -0
  61. habit_hooks-1.0.0/plugins/python/pyproject.toml +16 -0
  62. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/__init__.py +1 -0
  63. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/config.toml +5 -0
  64. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/guides/high-complexity.md +12 -0
  65. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/guides/swallowed-exception.md +12 -0
  66. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/ruff.toml +6 -0
  67. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/sensors/deptry.toml +1 -0
  68. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/sensors/deptry_sensor.py +70 -0
  69. habit_hooks-1.0.0/plugins/python/src/habit_hooks_python/sensors/ruff.toml +28 -0
  70. habit_hooks-1.0.0/plugins/typescript/docs/typescript-plugin.spec.md +178 -0
  71. habit_hooks-1.0.0/plugins/typescript/package-lock.json +2048 -0
  72. habit_hooks-1.0.0/plugins/typescript/package.json +13 -0
  73. habit_hooks-1.0.0/plugins/typescript/pyproject.toml +16 -0
  74. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/__init__.py +1 -0
  75. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/config.toml +4 -0
  76. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/eslint.config.mjs +49 -0
  77. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/guides/non-null-assertion.md +11 -0
  78. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/knip.json +5 -0
  79. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/comment.js +78 -0
  80. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/comment.toml +1 -0
  81. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/eslint.toml +34 -0
  82. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/knip.js +58 -0
  83. habit_hooks-1.0.0/plugins/typescript/src/habit_hooks_typescript/sensors/knip.toml +1 -0
  84. habit_hooks-1.0.0/prompts/build-habit-hooks-overnight.md +315 -0
  85. habit_hooks-1.0.0/pyproject.toml +76 -0
  86. habit_hooks-1.0.0/skills/REVIEW.md +24 -0
  87. habit_hooks-1.0.0/skills/habit-hooks-prompting/SKILL.md +37 -0
  88. habit_hooks-1.0.0/skills/habit-hooks-review/SKILL.md +108 -0
  89. habit_hooks-1.0.0/src/habit_hooks/__init__.py +0 -0
  90. habit_hooks-1.0.0/src/habit_hooks/catalogue.py +34 -0
  91. habit_hooks-1.0.0/src/habit_hooks/config.py +61 -0
  92. habit_hooks-1.0.0/src/habit_hooks/guides/clean.md +3 -0
  93. habit_hooks-1.0.0/src/habit_hooks/guides/includes/file_level_issues.md +3 -0
  94. habit_hooks-1.0.0/src/habit_hooks/guides/includes/line_level_issues.md +3 -0
  95. habit_hooks-1.0.0/src/habit_hooks/guides/uncoached.md +8 -0
  96. habit_hooks-1.0.0/src/habit_hooks/hooks.py +27 -0
  97. habit_hooks-1.0.0/src/habit_hooks/mapper.py +131 -0
  98. habit_hooks-1.0.0/src/habit_hooks/recommend.py +46 -0
  99. habit_hooks-1.0.0/src/habit_hooks/resolve.py +101 -0
  100. habit_hooks-1.0.0/src/habit_hooks/scope.py +106 -0
  101. habit_hooks-1.0.0/src/habit_hooks/sensors/__init__.py +71 -0
  102. habit_hooks-1.0.0/src/habit_hooks/sensors/execution.py +108 -0
  103. habit_hooks-1.0.0/src/habit_hooks/sensors/loader.py +63 -0
  104. habit_hooks-1.0.0/src/habit_hooks/sensors/model.py +38 -0
  105. habit_hooks-1.0.0/src/habit_hooks/snooze.py +81 -0
  106. habit_hooks-1.0.0/tests/harness/__init__.py +13 -0
  107. habit_hooks-1.0.0/tests/harness/errors.py +9 -0
  108. habit_hooks-1.0.0/tests/harness/glyphs.py +12 -0
  109. habit_hooks-1.0.0/tests/harness/markdown.py +61 -0
  110. habit_hooks-1.0.0/tests/harness/parser.py +155 -0
  111. habit_hooks-1.0.0/tests/harness/runner.py +16 -0
  112. habit_hooks-1.0.0/tests/harness/steps.py +123 -0
  113. habit_hooks-1.0.0/tests/harness/text.py +13 -0
  114. habit_hooks-1.0.0/tests/test_harness.py +187 -0
  115. habit_hooks-1.0.0/tests/test_installed_wheel_smoke.py +249 -0
  116. habit_hooks-1.0.0/uv.lock +555 -0
  117. habit_hooks-1.0.0/write_code_like_this.png +0 -0
@@ -0,0 +1,22 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ uv-minor-patch:
9
+ dependency-type:
10
+ - production
11
+ - development
12
+ update-types:
13
+ - patch
14
+ - minor
15
+ - package-ecosystem: github-actions
16
+ directory: /
17
+ schedule:
18
+ interval: weekly
19
+ groups:
20
+ github-actions:
21
+ patterns:
22
+ - "*"
@@ -0,0 +1,27 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - name: Checkout
11
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
12
+ - name: Setup uv
13
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
14
+ with:
15
+ python-version: "3.11"
16
+ - name: Install dependencies
17
+ run: uv sync --frozen
18
+ - name: Setup Node
19
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
20
+ with:
21
+ node-version: '22'
22
+ - name: Install Node tool deps
23
+ run: |
24
+ npm ci --prefix plugins/typescript
25
+ npm ci --prefix plugins/generic
26
+ - name: Test
27
+ run: uv run pytest
@@ -0,0 +1,19 @@
1
+ name: Dependabot auto-merge
2
+ on: pull_request
3
+ jobs:
4
+ auto-merge:
5
+ if: github.actor == 'dependabot[bot]'
6
+ runs-on: ubuntu-latest
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+ steps:
11
+ - name: Fetch metadata
12
+ id: meta
13
+ uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
14
+ - name: Enable auto-merge for patch and minor updates
15
+ if: steps.meta.outputs.update-type == 'version-update:semver-patch' || steps.meta.outputs.update-type == 'version-update:semver-minor'
16
+ run: gh pr merge --auto --squash "$PR_URL"
17
+ env:
18
+ PR_URL: ${{ github.event.pull_request.html_url }}
19
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,48 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ - name: Build every workspace package
15
+ run: uv build --all-packages --out-dir dist
16
+ - uses: actions/upload-artifact@v4
17
+ with:
18
+ name: dist
19
+ path: dist/
20
+
21
+ publish:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ environment: ${{ matrix.environment }}
25
+ permissions:
26
+ id-token: write
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ include:
31
+ - { dist: habit_hooks, environment: pypi }
32
+ - { dist: habit_hooks_generic, environment: pypi-generic }
33
+ - { dist: habit_hooks_python, environment: pypi-python }
34
+ - { dist: habit_hooks_typescript, environment: pypi-typescript }
35
+ - { dist: habit_hooks_php, environment: pypi-php }
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - name: Isolate this package's distributions
42
+ run: |
43
+ mkdir -p upload
44
+ mv "dist/${{ matrix.dist }}-"*.whl "dist/${{ matrix.dist }}-"*.tar.gz upload/
45
+ - uses: pypa/gh-action-pypi-publish@release/v1
46
+ with:
47
+ packages-dir: upload/
48
+ skip-existing: true
@@ -0,0 +1,20 @@
1
+ # Per-plugin Node tool deps (installed via npm ci; see plugins/*/package.json)
2
+ node_modules/
3
+ dist
4
+ coverage
5
+ .DS_Store
6
+ .idea
7
+ .claude-channel/
8
+ *.tgz
9
+ *.log
10
+ .vscode/
11
+ .venv/
12
+ __pycache__/
13
+ *.pyc
14
+ .pytest_cache/
15
+ .spec-runs/
16
+
17
+ /ruff.toml
18
+
19
+ # Workflow orchestration script (run from ~/.claude, never a repo deliverable)
20
+ .claude/workflows-build-overnight.js
@@ -0,0 +1,6 @@
1
+ {
2
+ "path": ["src"],
3
+ "ignore": ["**/*.test.ts", "tests/fixtures/**", "dist/**", "node_modules/**"],
4
+ "threshold": 0,
5
+ "minLines": 5
6
+ }
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Sensors & languages
6
+ - **Consumer-defined sensors** (#16): the `sensors` config map is now the single way sensors are assembled — built-in and custom alike. Each entry is one of three mutually exclusive modes: `use` (reference a bundled sensor by id — `eslint`/`comment`/`jscpd`/`knip`/`ruff`/`deptry`/`line-count`/`needs-extraction`), a **wrapper script** (`command` + `produces` printing bag JSON), or a **declarative adapter** (`command` + `produces` + `items`/`fields`/`group`/`map`). The sensor id is the map key; `dependsOn` wires multi sensors. See `docs/sensors.md`.
7
+ - **Authoritative `sensors` semantics**: when `sensors` is present it replaces the language preset entirely (no merge), so removing a built-in is just deleting its entry. When `sensors` is absent the preset is used and a deprecation warning is emitted — this implicit fallback is **removed in the 1.0.0 release**.
8
+ - **Consumer-defined languages** (#16): `language` accepts any string. The built-ins (`typescript`/`python`) keep their preset + default file globs; any other value relies on the open `files` discovery globs plus a `sensors` map. A non-built-in language with no `files` emits a warning and discovers no source files.
9
+ - A custom sensor must be declared as a pair — its `sensors.<id>` entry **and** a matching `smells.<smell>` entry (a custom smell needs `id` + `source: "custom"` + `severity`). New config validation rejects malformed sensor specs (mode mixing, missing required fields) and bad `files` globs.
10
+
11
+ ## 0.2.0
12
+
13
+ ### Highlights
14
+ - Habit Hooks is now a smell-agnostic, config-driven coach: a three-stage pipeline (sensor → mapper → guide) connected by a JSON bag. Sensors detect findings and translate them into a canonical, tool-independent **smell vocabulary**; the mapper routes each smell to a fix; the guide coaches the agent and sets the exit code.
15
+ - Two language presets ship out of the box — **TypeScript/JavaScript** (ESLint + knip + jscpd + a ts-morph comment scan) and **Python** (ruff + jscpd + deptry + a line-count sensor). No sensors run by default; `init` enables the preset for the project's language.
16
+ - The smell catalogue, language presets, and per-language tool config drive behaviour. Concrete smell knowledge lives only in config and the language initializers — the runner, mapper, sensors, and checks are smell-agnostic.
17
+
18
+ ### CLI & config
19
+ - `habit-hooks` runs the configured sensors over a project, groups findings by smell, prints each smell's coaching, and sets the process exit code — non-zero when an enforced smell fires, zero on a clean run or suggested-only findings.
20
+ - Git-aware scope flags restrict a run to a change set: `--last <n>` (files changed in the last N commits), `--branch [name]` (vs a branch, default `scope.branchBase`), `--since <hash>` (since a commit), and `--all` (force every file). The four are mutually exclusive; the default scope and per-rule `changedFilesOnly` come from config. `--config <path>` points at an explicit config file; `--version` prints the version.
21
+ - `habit-hooks.config.{ts,js,mjs}` is intentionally small: per-smell/per-rule `include`/`exclude` globs, `severity` overrides, `disabled`, and `changedFilesOnly`; a `scope` block (`onlyChangedFiles`, `branchBase`); a `prompts` directory for custom/override coaching text; and `commentCheck` thresholds. All tool thresholds stay in the consumer's own eslint/knip/jscpd/ruff config.
22
+
23
+ ### Wrap model & coaching
24
+ - Habit Hooks drives the consumer's **own** installed eslint / knip / jscpd, surfacing whatever rules and thresholds those configs define; it falls back to the bundled binaries only when the project has none. The coaching layer (why-it's-a-smell + how-to-fix) is what Habit Hooks adds on top.
25
+ - The bundled coaching prompts for the size/complexity smells are adapted from the refakts refactoring-quality system — keeping its analyse-first / anti-mechanical-fix structure — with the remaining prompts explaining why each smell matters rather than restating the threshold.
26
+ - knip 5 and 6 are both supported: the consumer's installed major version is auto-detected so v5's `classMembers` and v6's per-issue `files`/`exports`/`dependencies` shapes are each read correctly, and v6 no longer loses every knip check over a rejected flag.
27
+
28
+ ### Smell catalogue
29
+ - A canonical, tool-independent catalogue (kebab-case keys, see `docs/smell-vocabulary.md`). `enforced` smells fail the run (exit 1); `suggested` smells coach but exit 0; the mapper config can override severity per project.
30
+ - Enforced size/complexity smells: `oversized-function`, `too-many-parameters`, `high-complexity`, `deep-nesting`, `oversized-file`.
31
+ - Enforced correctness smells: `unused-variable`, `loose-equality`, `var-declaration`, `non-const-binding`, `duplicate-import`, `redundant-type-annotation`, and the unused-code family from knip/deptry (`unused-file`, `unused-export`, `unused-dependency`, `unused-class-member`, `unused-import`).
32
+ - Suggested smells: `warning-comment`, `explicit-any`, `non-null-assertion`, `non-essential-comment`, `duplicated-code`.
33
+ - `needs-extraction` (enforced) is a **composite** smell; `parse-error` (enforced) is a supplemental smell for ESLint fatals with no catalogue rule.
34
+ - Each sensor owns its raw rule ID → smell translation; the mapper and prompts only ever key off the canonical smell, never the tool.
35
+
36
+ ### Sensors & presets
37
+ - **TypeScript/JavaScript preset**: ESLint (size/complexity/correctness/TS smells + `parse-error`), knip (`unused-file`/`unused-export`/`unused-dependency`/`unused-class-member`), jscpd (`duplicated-code`), and an in-process ts-morph scan (`non-essential-comment`). `comment-check` still runs in-process via ts-morph — it is not a shell-out sensor.
38
+ - **Python preset**: ruff (`high-complexity`/`too-many-parameters`/`oversized-function`/`unused-variable`/`unused-import`), jscpd on `.py` (`duplicated-code`), deptry (`unused-dependency`), and a language-agnostic line-count sensor (`oversized-file`).
39
+ - Preset thresholds come from the consumer's own tool config (e.g. ESLint `max-lines`/`complexity`, ruff `mccabe.max-complexity`/`pylint.max-args`), not from Habit Hooks.
40
+ - **Composite sensors via `dependsOn`** (#17): a multi sensor declares the smells it consumes, receives their issues in `ctx.deps`, and emits a derived smell. `needs-extraction` fires when one file is both `oversized-file` **and** `duplicated-code`. It augments by default (all three smells show); `needsExtraction.replace: true` suppresses the two inputs so only `needs-extraction` remains. The augment-vs-replace switch runs in the sensor stage, keeping the mapper a pure single-smell function. Wired into the TS preset and the Python preset.
41
+ - **deep-nesting** (#26): new enforced TS smell via ESLint `max-depth`. Python `deep-nesting` (ruff `PLR1702`) is deferred while that rule is preview/unstable.
42
+ - **Python `oversized-file`** (#19): a language-agnostic line-count sensor emits it for files over a threshold (`max-module-lines`, default 200). ruff has no `C0302` port and rejects an unknown `max-module-lines` key under `[tool.ruff]`, so the threshold is read by a no-TOML-parser text scan of the consumer's ruff config + `pyproject.toml`; set it in a ruff-ignored location such as `[tool.habit-hooks]`.
43
+ - **`command` fix action** (#18): a smell's fix can be a script instead of a prompt. The guide runs the command once per smell that has issues, streams its output into that smell's section, and folds its exit code into the run's exit code.
44
+ - **Declarative adapter**: a tool that already emits JSON can be wired as a sensor by declaring how to read it (`group`/`items`/`fields`/`map`, up to two levels of array nesting) — no wrapper script needed. Anything it can't express falls back to a wrapper script.
45
+ - **Sensor failures fail the run** (#25): a sensor spawn/timeout failure now exits 1 (instead of a false-clean) while still rendering every successful sensor's output. Failures travel on a shared `SensorSink`; the failure notice is shown on stderr.
46
+
47
+ ### Baseline, snooze & auto-prune
48
+ - A file-level baseline (snooze) is committed to the repo at `.habit-hooks-baseline.json`, so a whole team shares one snapshot of legacy violations. A snoozed file stays snoozed for every sensor only while it appears in the baseline, its last-commit hash matches, and its working tree is clean — touching the file re-arms every smell, so you cannot silently drift past snoozed violations.
49
+ - `habit-hooks baseline` subcommands manage it: `generate` (record current violations), `status` (list snoozed files and freshness), `snooze <files...>`, `forget <files...>`, and `prune` (drop stale/resolved entries).
50
+ - **Auto-prune of dead snooze entries** (#11): on a full-repo run, Habit Hooks re-scans baseline-free and reaps snooze entries whose file is present but no longer produces the smell, printing the pruned set. Scoped runs never mutate the baseline (a file can look clean only because its smell is outside the diff), so they are a guaranteed no-op. Auto-prune shares one reaper with the manual `baseline prune` command.
51
+ - A memoized, batched snooze index collapses the per-rule git spawns to O(1) status + O(files) memoized log calls.
52
+
53
+ ### Init
54
+ - `habit-hooks init [language]` onboards a project for its language. With no argument it detects the language and prints a report-only message; an explicit language threads through with no re-detect, and an unsupported language exits 2 before any side effect.
55
+ - Detects which tools are already installed/configured and scaffolds starter configs only for the missing pieces: an ESLint flat config (TS) or `ruff.toml` + `.jscpd.json` (Python), with package-manager install commands spanning pip and node ecosystems.
56
+ - The scaffolded ESLint config writes tunable thresholds including `max-depth: 4` (deep-nesting) alongside the other size/complexity rules, and exempts test files from size rules. Test globs derive from a single shared exclude list.
57
+ - Recommended thresholds mirror across languages from one source (ESLint `complexity 10` / `max-params 3` ↔ ruff `mccabe.max-complexity 10` / `pylint.max-args 3`); a freshly-scaffolded config is pinned to satisfy the drift check.
58
+ - Drift detection is additive — a recommended value is flagged only when its key is absent, never when you've tuned it. `--accept-recommendations` runs the install commands and additively merges absent recommended keys into Habit-Hooks-owned config, never overwriting user values or editing user-owned `ruff.toml`/`pyproject.toml`/ESLint config.
59
+ - Prompts cover package.json scripts, a pre-commit hook, and the bundled `habit-hooks-review` skill. `--dry-run` prints every intended write without touching disk.
60
+
61
+ ### Architecture
62
+ - **Config-driven, smell-agnostic** (#24): all tool/smell knowledge lives in `src/config/tool-smells.ts` — the ESLint raw→smell map, the eslint/knip/jscpd/comment `produces`, and the ruff + deptry adapter specs. ESLint/jscpd/comment data is **derived from the catalogue**, so adding a smell there auto-wires its translation and produces; the runner, sensors, checks, and rules registry import these instead of hardcoding any smell id. `deep-nesting` (#26) was added by touching only the catalogue and the init ESLint template — the live proof of #24.
63
+ - Single source for tool config discovery: `TOOL_CONFIG_FILENAMES` / `TOOL_PACKAGE_JSON_KEYS` in `src/detect/tool.ts`.
64
+ - A routed smell with no tuned `<smell>.md` template falls back to a generic `uncoached.md` body while keeping its severity; a truly unknown smell (no routing at all) goes to the uncoached bucket and never escalates the exit code.
65
+
66
+ ### Breaking changes
67
+ - The bundled "default rule set" and programmatic tool pinning of the beta are gone. Behaviour is driven by the smell catalogue plus the consumer's own tool configs; `knip` is no longer version-pinned, so the consumer's installed version determines available issue types.
68
+ - The `rules` config field is **deprecated in favour of `smells`** (#21). `rules` is still accepted and folded in (with `smells` winning on conflict), but a config using it now emits a deprecation warning on stderr. Hard removal of `rules` is scheduled for a release after 0.2.0.
@@ -0,0 +1,165 @@
1
+ # habit-hooks notes
2
+
3
+ ## Architecture
4
+
5
+ ### Plugins are installed packages discovered via entry points (human-requested by Ivett)
6
+
7
+ The core finds plugins through the `habit_hooks.plugins` entry-point group, NOT
8
+ by walking a sibling `plugins/` directory. Each plugin is a separately
9
+ installable dist `habit-hooks-<name>` whose import package `habit_hooks_<name>`
10
+ ships its `config.toml`/`sensors/`/`guides/`/helper scripts/phar as package data
11
+ (importlib.resources-accessible). `resolve.installed_plugin_dirs()` maps plugin
12
+ name -> package-data dir via `importlib.metadata.entry_points` +
13
+ `importlib.resources.files`. The override chain is
14
+ `.habit-hooks/<plugin>/<file>` (project) -> `<plugin package data>/<file>`
15
+ (default). A configured plugin that is neither overridden under `.habit-hooks/`
16
+ nor installed raises a clear error naming `pip install habit-hooks-<name>`
17
+ (`Resolver.require_plugin`) — that is the bug-1 root-cause guard.
18
+
19
+ The repo is a uv workspace (`[tool.uv.workspace] members = ["plugins/*"]`); the
20
+ four in-repo plugins live under `plugins/<name>/src/habit_hooks_<name>/` and are
21
+ installed editable by `uv sync` for dev. Keeping them in-repo is only a dev
22
+ convenience — they do not need to live here. `tests/test_installed_wheel_smoke.py`
23
+ builds + installs the core + generic wheels into a throwaway venv and asserts a
24
+ real finding comes out; it is the gate that catches "installed runs can't locate
25
+ plugins". `${dir}` in a sensor command resolves to the plugin's package-data dir,
26
+ so helper-script paths (`${dir}/line-count.py`, `${dir}/../.jscpd.json`) keep
27
+ working once the layout is preserved under the import package.
28
+
29
+ ### Sensor `args` live in the sensor's own toml, not the plugin `config.toml` (agent decision)
30
+
31
+ A sensor's default CLI args (e.g. line-count's `--max 200`) live as `args = [...]`
32
+ in `sensors/<name>.toml` and expand into the command via `${args}`. They cannot go
33
+ in the plugin `config.toml` because `sensors = [...]` (the ordered list) and a
34
+ `[sensors.<name>]` table collide as the same TOML key. A project replaces them
35
+ wholesale via `.habit-hooks/config.toml` `[sensors.<name>] args = [...]`
36
+ (replace-on-override — `SensorOverride.args`, threaded in `sensors._sensor_args`).
37
+
38
+ ### jscpd resolves a config's relative `path` against the config file, not cwd (agent decision)
39
+
40
+ When `jscpd --config <abs path>` loads `.jscpd.json`, its `path: ["src"]` resolves
41
+ relative to the config file's directory, so a plugin-shipped config scans nothing
42
+ in the consumer repo. `plugins/generic/sensors/jscpd.py` therefore reads `path`
43
+ out of the config and passes those as positional args (resolved against cwd),
44
+ keeping the config the single source for threshold/ignore/minLines/minTokens.
45
+
46
+ ## Gotchas
47
+
48
+ ### knip runs a gated second pass in production mode (issue #59)
49
+
50
+ `knipWrap` runs knip twice when — and only when — the consumer's knip
51
+ config marks production patterns with a trailing `!` (detected by
52
+ `knipConfigMarksProduction` in `knip-resolve.ts`). The default pass is
53
+ authoritative for everything (incl. unused devDependencies); the
54
+ `--production` pass contributes only dead-code findings
55
+ (`PRODUCTION_PASS_SOURCES` in `knip-merge.ts`), merged + deduped. This
56
+ catches code reached only by tests without losing devDep detection.
57
+ Gotchas: `--production` analyses NOTHING unless `!` is on BOTH `entry`
58
+ and `project` (a no-`!` config under `--production` silently reports
59
+ zero — so we never pass it there). Test files must be listed as
60
+ unmarked (non-production) `entry`, else knip 5 + a vitest config falsely
61
+ reports them as unused files. The merge intentionally keeps `knip:files`
62
+ from the production pass, so a wholly test-only production file can
63
+ surface as an unused file — that's the feature, not a bug.
64
+
65
+ ### JSDoc nodes are not MultiLineCommentTrivia in ts-morph
66
+
67
+ `/** ... */` blocks are `SyntaxKind.JSDoc` (321) when attached to a
68
+ declaration, NOT `MultiLineCommentTrivia`. To find them, query both. See
69
+ `src/checks/comment-check.ts`.
70
+
71
+ ### knip's `exports` field omits the bin path
72
+
73
+ `knip` exports only `.` and `./session`, so
74
+ `require.resolve('knip/bin/knip.js')` fails. The bundled-fallback resolver
75
+ in `src/checks/knip-wrap.ts` (`bundledKnipBin`) resolves `'knip'` (main
76
+ entry) and navigates up to `../bin/knip.js` instead. Consumer-detected
77
+ knip is found via `detectTool`, which walks `package.json#bin.knip` and
78
+ does not hit this hazard.
79
+
80
+ ### knip needs `package.json` in cwd
81
+
82
+ Running knip in a directory without `package.json` exits 2 with a help
83
+ message. `knipWrap` skips silently when no `package.json` is present —
84
+ the user's project always has one, but our internal test temp dirs
85
+ often don't.
86
+
87
+ ### knip 5 vs 6 — issue type drift
88
+
89
+ We no longer pin knip; the consumer's installed version drives what
90
+ fires. v5 emits `classMembers`; v6 dropped that key and surfaces unused
91
+ exports via `files` / `exports` / `dependencies` instead. We ship
92
+ coaching prompts for all four so either version is covered. If a future
93
+ knip introduces a new top-level issue key, the wrap surfaces it as an
94
+ uncoached violation (see `unknownKeysForIssue` in
95
+ `src/checks/knip-wrap.ts`); add a prompt to coach it.
96
+
97
+ ### comment-check file discovery doesn't honour project ignores
98
+
99
+ `runner.discoverFiles` uses fast-glob with a hardcoded ignore set
100
+ (`node_modules`, `dist`, `coverage`). Only `comment-check` consumes that
101
+ list directly — the eslint, knip, and jscpd wraps delegate discovery to
102
+ their respective tools. Fixtures under `tests/fixtures/**` therefore get
103
+ swept by comment-check when you run `node dist/cli.js` against the repo
104
+ root, which is why a smoke run on our own source shows comment
105
+ violations from inside fixtures.
106
+
107
+ ### Bumping pnpm 10 → 11 needs Corepack, not auto-switch
108
+
109
+ pnpm 11 split its launcher: the main `pnpm` npm package owns
110
+ `dist/pnpm.mjs`, while `@pnpm/macos-arm64` (and siblings) ship only the
111
+ native loader. pnpm 10's `packageManager` auto-switch fetches only the
112
+ platform package, producing a binary missing its JS bootstrap —
113
+ `Cannot find module .../dist/pnpm.mjs`. Bootstrap pnpm 11 via Corepack
114
+ (`corepack prepare pnpm@<v> --activate`) or the official installer
115
+ instead. The standalone shim at `~/Library/pnpm/pnpm` is from the old
116
+ installer; once Corepack is on PATH, remove the shim so it stops
117
+ shadowing it.
118
+
119
+ ### Wrap shell-out semantics — failures sit in `result.warnings`
120
+
121
+ `src/wrap/shell.ts` never throws on spawn/timeout failure. A spawn
122
+ failure surfaces as `exitCode === -1` with the cause in `warnings`; a
123
+ non-zero exit from the tool itself comes back with the real `exitCode`
124
+ and an empty `warnings`. The helpers `isSpawnFailure` and
125
+ `spawnFailureWarning` in `src/wrap/notices.ts` separate the two so a
126
+ crashed tool produces a stderr notice (and zero violations) instead of
127
+ silently swallowing the run.
128
+
129
+ ### jscpd `-n` (noSymlinks) is baked in
130
+
131
+ `jscpdWrap` always passes `-n`. A consumer with intentionally symlinked
132
+ source directories (monorepo `src/shared -> ../shared-lib`) silently
133
+ will not get duplication detection on the linked paths. Surface this if
134
+ a user reports missing jscpd hits on a symlinked tree.
135
+
136
+ ### Bundled habit-hooks ESLint vs project-local ESLint disagree on type-position param names
137
+
138
+ Our bundled config flags an unused parameter in a type-only function
139
+ signature (e.g. `resolve: (_result: ShellResult) => void`); the
140
+ project's flat config does not. Underscoring the param satisfies both.
141
+ This bit us during Phase 1/2 — if a wrap introduces a new callback type
142
+ and the build trips on `no-unused-vars` for a positional name, prefix
143
+ with `_`.
144
+
145
+ ### Tool config filename lists live in `src/detect/tool.ts`
146
+
147
+ `TOOL_CONFIG_FILENAMES` and `TOOL_PACKAGE_JSON_KEYS` are the single
148
+ source for tool config discovery. When you add a new tool, a new config
149
+ filename, or a new `package.json` key (e.g. `eslint.config.cjs`,
150
+ `knip.jsonc`), update those tables only — `detectTool` and every
151
+ caller flows through them. Individual wraps may keep their own narrower
152
+ lists for internal "has-config" checks, but those should mirror the
153
+ canonical set.
154
+
155
+ ### A sensor named `ruff.toml` collides with ruff's config discovery
156
+
157
+ `plugins/python/sensors/ruff.toml` is a sensor spec (`command = ...`),
158
+ but ruff treats any file literally named `ruff.toml` as its own config.
159
+ A `ruff check` whose upward config-discovery walk passes through
160
+ `plugins/python/sensors/` hard-fails with `unknown field 'command'`.
161
+ Harmless in normal consumer operation — the file lives inside the
162
+ habit-hooks package, off the consumer's discovery path — but a future
163
+ dogfooding ruff run from inside that tree will be mystifying. Point ruff
164
+ at an explicit `--config` (the repo-root `ruff.toml`, gitignored as
165
+ `/ruff.toml`) if you hit this.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivett Ördög
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: habit-hooks
3
+ Version: 1.0.0
4
+ Summary: Turn best-practice coding advice into AI habits — structural code-smell coaching for AI agents
5
+ License-File: LICENSE.md
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: habit-hooks-generic~=1.0
8
+ Requires-Dist: jinja2>=3
9
+ Requires-Dist: pathspec>=0.12
10
+ Requires-Dist: pydantic>=2
11
+ Provides-Extra: all
12
+ Requires-Dist: habit-hooks-php~=1.0; extra == 'all'
13
+ Requires-Dist: habit-hooks-python~=1.0; extra == 'all'
14
+ Requires-Dist: habit-hooks-typescript~=1.0; extra == 'all'
15
+ Provides-Extra: php
16
+ Requires-Dist: habit-hooks-php~=1.0; extra == 'php'
17
+ Provides-Extra: python
18
+ Requires-Dist: habit-hooks-python~=1.0; extra == 'python'
19
+ Provides-Extra: typescript
20
+ Requires-Dist: habit-hooks-typescript~=1.0; extra == 'typescript'