Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,10 +789,11 @@ Pre-commit hooks give immediate local feedback. They can be bypassed with `--no-
The assessor scores on a 100-point scale:

- **`.pre-commit-config.yaml` present** (60 pts): pre-commit hooks configured
- **`.husky` directory with hook scripts** (60 pts): Husky git hooks configured (e.g., pre-commit, commit-msg)
- **`.husky` directory without hook scripts** (10 pts): Husky directory exists but no hooks defined
- **`.claude/settings.json` with hooks** (30 pts): Claude Code hook configuration present
- **`.husky` directory** (10 pts): Husky git hooks configured

**Pass threshold**: 60 points or higher. A `.pre-commit-config.yaml` alone is sufficient to pass.
**Pass threshold**: 60 points or higher. Either `.pre-commit-config.yaml` or `.husky` with hook scripts is sufficient to pass.

#### Remediation

Expand Down
24 changes: 17 additions & 7 deletions src/agentready/assessors/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,19 @@ def assess(self, repository: Repository) -> Finding:
score += 10.0
evidence.append(".claude/settings.json exists")

if husky_dir.exists():
score += 10.0
evidence.append(".husky directory found (git hooks)")
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("_")
]
Comment on lines +591 to +596
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.

if hook_scripts:
score += 60.0
hooks_list = ", ".join(sorted(hook_scripts))
evidence.append(f".husky directory found with hooks: {hooks_list}")
Comment on lines +592 to +600
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.

else:
score += 10.0
evidence.append(".husky directory found but no hook scripts")

score = min(score, 100.0)

Expand Down Expand Up @@ -634,14 +644,14 @@ def _create_remediation(self) -> Remediation:
summary="Set up deterministic enforcement with hooks and lint rules",
steps=[
"Start with 2 hooks: auto-format on edit + block destructive operations",
"Install pre-commit framework for git hooks",
"Install pre-commit (Python) or Husky (Node.js) for git hooks",
"Configure .claude/settings.json with agent hooks for team-wide sharing",
"Add lint rules for import restrictions and architectural boundaries",
],
tools=["pre-commit"],
tools=["pre-commit", "husky"],
commands=[
"pip install pre-commit",
"pre-commit install",
"pip install pre-commit && pre-commit install",
"npx husky init",
"mkdir -p .claude",
],
examples=[
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_assessors_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,49 @@ def test_pre_commit_config_passes(self, tmp_path):
assert finding.score >= 60.0
assert any("pre-commit" in e.lower() for e in finding.evidence)

def test_husky_with_hooks_passes(self, tmp_path):
"""Test that .husky directory with hook scripts passes."""
husky_dir = tmp_path / ".husky"
husky_dir.mkdir()
(husky_dir / "pre-commit").write_text("#!/bin/sh\nnpx lint-staged\n")
(husky_dir / "commit-msg").write_text("#!/bin/sh\nnpx commitlint --edit $1\n")

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

assert finding.status == "pass"
assert finding.score >= 60.0
assert any(".husky" in e for e in finding.evidence)
assert any("pre-commit" in e for e in finding.evidence)
assert any("commit-msg" in e for e in finding.evidence)

def test_husky_empty_dir_does_not_pass(self, tmp_path):
"""Test that .husky directory without hook scripts only gives partial score."""
husky_dir = tmp_path / ".husky"
husky_dir.mkdir()

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

assert finding.status == "fail"
assert finding.score == 10.0
assert any("no hook scripts" in e for e in finding.evidence)

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

Comment on lines +594 to +606
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.

def test_no_config_fails(self, tmp_path):
"""Test fail when no enforcement config exists."""
repo = _make_repo(tmp_path)
Expand Down
Loading