Skip to content

feat: support Husky as equivalent pre-commit hook framework#467

Open
aviavraham wants to merge 1 commit into
ambient-code:mainfrom
aviavraham:feat/433-husky-support
Open

feat: support Husky as equivalent pre-commit hook framework#467
aviavraham wants to merge 1 commit into
ambient-code:mainfrom
aviavraham:feat/433-husky-support

Conversation

@aviavraham
Copy link
Copy Markdown

@aviavraham aviavraham commented May 26, 2026

Summary

Closes #433

  • .husky/ with hook scripts now scores 60 pts (same as .pre-commit-config.yaml), so JS/TS/Java projects using Husky can pass the deterministic enforcement attribute
  • Empty .husky/ directory (no hook scripts) stays at 10 pts
  • Underscore-prefixed files (e.g. _/husky.sh) are correctly ignored
  • Remediation guidance updated to list Husky as an alternative (npx husky init)
  • docs/attributes.md updated with new scoring breakdown

Test plan

  • 3 new unit tests: Husky with hooks passes, empty dir doesn't pass, underscore files ignored
  • All 1104 existing unit tests pass
  • Linting clean (black, isort, ruff)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed assessment of Git hooks configuration to verify hook scripts actually exist, now distinguishing between directories with and without executable scripts.
  • Documentation

    • Updated deterministic enforcement documentation to reflect refined scoring criteria and updated pass conditions for hook verification.

…code#433)

Husky (.husky/) is the most popular pre-commit hook tool for
JavaScript/TypeScript/Java projects. Previously it scored only 10
points (always failing). Now .husky/ with hook scripts scores 60
points — same as .pre-commit-config.yaml — so projects using either
framework can pass the deterministic enforcement attribute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

This PR adds Husky support to the deterministic enforcement assessment. The assessor now scans .husky directories for hook scripts and awards conditional scoring. Documentation, tests, and remediation guidance are updated to reflect the new behavior.

Changes

Husky Support for Deterministic Enforcement Assessment

Layer / File(s) Summary
.husky assessment implementation and remediation
src/agentready/assessors/testing.py
DeterministicEnforcementAssessor now checks if .husky exists as a directory, enumerates hook script files (excluding underscore-prefixed), and awards higher score with filename evidence when hooks are present versus lower score for empty directories. Remediation guidance adds husky to tools list and includes npx husky init command alongside pre-commit setup.
Test coverage for .husky behavior
tests/unit/test_assessors_testing.py
Three new tests validate assessment passing with hook scripts (pre-commit, commit-msg), failing with exact 10.0 score for empty .husky, and ignoring underscore-prefixed content while still passing with valid hooks.
Deterministic Enforcement documentation
docs/attributes.md
Measurable criteria split .husky into two distinct cases: with hook scripts (60 pts) versus without (10 pts). Pass condition updated to accept either .pre-commit-config.yaml presence or .husky with hook scripts.

Possibly related issues

  • #461: Main changes directly implement hook-scoring reprioritization described in the architecture decision record, making them related.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (feat: description) and accurately describes the main change of adding Husky support.
Linked Issues check ✅ Passed Changes implement the primary objective from issue #433: extend pre-commit hook detection to include Husky scanning for .husky directory with appropriate scoring.
Out of Scope Changes check ✅ Passed All changes directly support Husky detection and scoring—documentation updates, assessor logic refinement, and comprehensive test coverage align with issue #433 scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@aviavraham aviavraham marked this pull request as ready for review May 26, 2026 14:30
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agentready/assessors/testing.py`:
- Around line 592-600: The current comprehension treats any regular file in
husky_dir as a hook (hook_scripts), so update the filter to only include real
hook scripts by checking executability and common hook signatures: use
f.is_file() and not f.name.startswith("_") AND (os.access(f, os.X_OK) OR the
file’s first line starts with "#!" OR f.name is in a small KNOWN_HOOKS set like
{"pre-commit","commit-msg","pre-push","post-commit","pre-commit.sample"}); keep
the rest of the logic (score += 60.0, hooks_list, evidence.append) but only
trigger it when this stricter hook_scripts list is non-empty, and import os and
read the first line safely when computing the filter.
- Around line 591-596: The assessor currently uses husky_dir.iterdir() (in
assess()) which can raise OSError/PermissionError and crash the run; wrap the
iteration in a try/except that catches OSError/PermissionError around the
husky_dir.iterdir() call (the block building hook_scripts from husky_dir) and
handle failures by returning or recording a skipped/error Finding instead of
letting the exception propagate; ensure you reference husky_dir and hook_scripts
in the handler and log the permission/error detail so assess() exits gracefully.

In `@tests/unit/test_assessors_testing.py`:
- Around line 594-606: The test test_husky_ignores_underscore_files currently
creates an underscore directory so it doesn't exercise the underscore-file
filtering; update the test to create an underscore-prefixed file (e.g., create a
file named ".husky/_local" with hook-like content) instead of or in addition to
the directory, using the same tmp_path and _make_repo setup, then run
DeterministicEnforcementAssessor().assess(repo) and assert the finding.score
remains >= 60.0 (or that hook detection count doesn't increase) to verify
underscore-prefixed files are ignored; reference the test function name
test_husky_ignores_underscore_files and the helper _make_repo to locate where to
change the fixture.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: c019d9a9-b680-4495-a76b-4c1e5f4c66ae

📥 Commits

Reviewing files that changed from the base of the PR and between 871fe25 and c7d80a0.

📒 Files selected for processing (3)
  • docs/attributes.md
  • src/agentready/assessors/testing.py
  • tests/unit/test_assessors_testing.py

Comment on lines +591 to +596
if husky_dir.exists() and husky_dir.is_dir():
hook_scripts = [
f.name
for f in husky_dir.iterdir()
if f.is_file() and not f.name.startswith("_")
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle unreadable .husky/ safely instead of crashing the assessor

husky_dir.iterdir() can raise OSError/PermissionError; right now that bubbles out of assess() and breaks assessment execution.

As per coding guidelines, "Check for proper error handling (return skipped/error Finding, don't crash)."

Suggested fix
         if husky_dir.exists() and husky_dir.is_dir():
-            hook_scripts = [
-                f.name
-                for f in husky_dir.iterdir()
-                if f.is_file() and not f.name.startswith("_")
-            ]
+            try:
+                hook_scripts = [
+                    f.name
+                    for f in husky_dir.iterdir()
+                    if f.is_file() and not f.name.startswith("_")
+                ]
+            except OSError:
+                hook_scripts = []
+                evidence.append(".husky directory exists but could not be read")
             if hook_scripts:
                 score += 60.0
                 hooks_list = ", ".join(sorted(hook_scripts))
                 evidence.append(f".husky directory found with hooks: {hooks_list}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agentready/assessors/testing.py` around lines 591 - 596, The assessor
currently uses husky_dir.iterdir() (in assess()) which can raise
OSError/PermissionError and crash the run; wrap the iteration in a try/except
that catches OSError/PermissionError around the husky_dir.iterdir() call (the
block building hook_scripts from husky_dir) and handle failures by returning or
recording a skipped/error Finding instead of letting the exception propagate;
ensure you reference husky_dir and hook_scripts in the handler and log the
permission/error detail so assess() exits gracefully.

Comment on lines +592 to +600
hook_scripts = [
f.name
for f in husky_dir.iterdir()
if f.is_file() and not f.name.startswith("_")
]
if hook_scripts:
score += 60.0
hooks_list = ", ".join(sorted(hook_scripts))
evidence.append(f".husky directory found with hooks: {hooks_list}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t treat arbitrary files in .husky/ as hook scripts

Current detection counts any regular file (except underscore-prefixed), so files like .gitignore or README.md can incorrectly produce a 60-point pass. That violates the “with hook scripts” scoring intent.

Suggested fix
+            valid_hook_names = {
+                "applypatch-msg",
+                "commit-msg",
+                "post-applypatch",
+                "post-checkout",
+                "post-commit",
+                "post-merge",
+                "post-rewrite",
+                "pre-applypatch",
+                "pre-auto-gc",
+                "pre-commit",
+                "pre-merge-commit",
+                "pre-push",
+                "pre-rebase",
+                "prepare-commit-msg",
+            }
             try:
                 hook_scripts = [
                     f.name
                     for f in husky_dir.iterdir()
-                    if f.is_file() and not f.name.startswith("_")
+                    if f.is_file()
+                    and not f.name.startswith("_")
+                    and f.name in valid_hook_names
                 ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
hook_scripts = [
f.name
for f in husky_dir.iterdir()
if f.is_file() and not f.name.startswith("_")
]
if hook_scripts:
score += 60.0
hooks_list = ", ".join(sorted(hook_scripts))
evidence.append(f".husky directory found with hooks: {hooks_list}")
valid_hook_names = {
"applypatch-msg",
"commit-msg",
"post-applypatch",
"post-checkout",
"post-commit",
"post-merge",
"post-rewrite",
"pre-applypatch",
"pre-auto-gc",
"pre-commit",
"pre-merge-commit",
"pre-push",
"pre-rebase",
"prepare-commit-msg",
}
hook_scripts = [
f.name
for f in husky_dir.iterdir()
if f.is_file()
and not f.name.startswith("_")
and f.name in valid_hook_names
]
if hook_scripts:
score += 60.0
hooks_list = ", ".join(sorted(hook_scripts))
evidence.append(f".husky directory found with hooks: {hooks_list}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agentready/assessors/testing.py` around lines 592 - 600, The current
comprehension treats any regular file in husky_dir as a hook (hook_scripts), so
update the filter to only include real hook scripts by checking executability
and common hook signatures: use f.is_file() and not f.name.startswith("_") AND
(os.access(f, os.X_OK) OR the file’s first line starts with "#!" OR f.name is in
a small KNOWN_HOOKS set like
{"pre-commit","commit-msg","pre-push","post-commit","pre-commit.sample"}); keep
the rest of the logic (score += 60.0, hooks_list, evidence.append) but only
trigger it when this stricter hook_scripts list is non-empty, and import os and
read the first line safely when computing the filter.

Comment on lines +594 to +606
def test_husky_ignores_underscore_files(self, tmp_path):
"""Test that files starting with _ (like _/husky.sh) are ignored."""
husky_dir = tmp_path / ".husky"
husky_dir.mkdir()
(husky_dir / "_").mkdir()
(husky_dir / "pre-commit").write_text("#!/bin/sh\nnpx lint-staged\n")

repo = _make_repo(tmp_path)
assessor = DeterministicEnforcementAssessor()
finding = assessor.assess(repo)

assert finding.score >= 60.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

test_husky_ignores_underscore_files doesn’t verify the intended rule

This test uses an underscore directory (.husky/_), but production logic already ignores directories via f.is_file(). So it won’t catch regressions in underscore-file filtering. Add a case with an underscore-prefixed file (e.g., .husky/_local) and assert it does not contribute to hook detection/score.

As per coding guidelines, "tests/**: Verify test actually tests the intended behavior."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_assessors_testing.py` around lines 594 - 606, The test
test_husky_ignores_underscore_files currently creates an underscore directory so
it doesn't exercise the underscore-file filtering; update the test to create an
underscore-prefixed file (e.g., create a file named ".husky/_local" with
hook-like content) instead of or in addition to the directory, using the same
tmp_path and _make_repo setup, then run
DeterministicEnforcementAssessor().assess(repo) and assert the finding.score
remains >= 60.0 (or that hook detection count doesn't increase) to verify
underscore-prefixed files are ignored; reference the test function name
test_husky_ignores_underscore_files and the helper _make_repo to locate where to
change the fixture.

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.

[FEATURE] Support Husky for pre-commit hooks

1 participant