Repository navigation
Scope the Husky Prose Gate to Staged Paths and Skip Generated Trees by Flag - #3823
Conversation
…y flag The Husky pre-commit hooks name the paths the commit stages, filtered to regular files, instead of scanning `.`, so an untracked scratch file no longer refuses every commit. The gate gains `--skip-generated`, which drops a named file inside a generated tree as a directory scan does, so the pre-commit snippet drops its hand-kept copy of `generated_trees`. `repo_root` drops git's location variables, since a hook in a linked worktree inherits `GIT_DIR` and git then reports each named file's directory as its own top level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The staged list keeps a typechange, which a symlink replaced by a regular file is, so its content reaches the gate. The gate resolves the repository once per distinct directory rather than once per named path, and both environment filters share one `git_env` helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three cases on a shared fixture: a deletion-only commit skips the gate, an edit plus a staged report under `reports/` passes, and a staged finding is refused without naming the untracked file. Each fails against the hook that scanned `.`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A staged-then-removed path and a symlink replaced by a file each get a hook case, failing when the `[ -f ]` filter or the `T` filter is dropped. `lookup_start` holds where a git lookup for a path starts, for `repo_root` and the per-directory lookup in `main`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
|
A recorded local strict-review pass covers head |
There was a problem hiding this comment.
🟡 Changes recommended
The pre-commit hooks still have critical portability and fail-open issues: GNU-only xargs -r breaks on macOS, while staged-path enumeration failures can incorrectly allow commits to proceed.
4 open issues
What changed in this PR
This pull request scopes Husky prose checks to staged regular files, adds generated-tree filtering for named paths, and improves linked-worktree repository detection.
Changes:
- Adds
--skip-generatedsupport using repository-configured generated trees. - Updates Husky and pre-commit integrations to process staged paths while excluding deleted and non-file paths.
- Expands tests and documentation for generated files, untracked files, deletions, type changes, and linked worktrees.
- Identifies critical portability and fail-closed issues in both Husky hooks involving
xargs -rand staged-path enumeration failures.
| File | Description |
|---|---|
tests/test_prose_lint.py |
Tests generated-tree filtering, staged-path scope, and linked-worktree lookup behavior. |
tests/test_pre_commit_hook.py |
Tests staged-path filtering and hook behavior through real commits. |
scripts/README.md |
Documents staged-path scope and generated-tree handling. |
catalog/snippets/pre-commit/README.md |
Updates pre-commit scope and generated-tree documentation. |
catalog/snippets/pre-commit/.pre-commit-config.yaml |
Passes --skip-generated to the prose gate. |
catalog/snippets/husky/README.md |
Documents staged-path processing for Husky hooks. |
catalog/snippets/husky/pre-commit |
Filters staged paths and invokes the fetched prose gate. |
.husky/pre-commit |
Filters staged paths and invokes the local prose gate. |
.github/actions/prose-gate/prose-gate.json |
Declares the new --skip-generated option. |
.github/actions/prose-gate/prose_lint.py |
Implements generated-tree filtering and linked-worktree-safe repository lookup. |
Review effort: Lite
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #3823 +/- ##
===========================================
+ Coverage 81.60% 82.91% +1.30%
===========================================
Files 26 29 +3
Lines 9070 9750 +680
===========================================
+ Hits 7402 8084 +682
+ Misses 1668 1666 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The inner sh filters the list and skips the gate when nothing is left, since GNU xargs runs once on empty input and BSD xargs does not. The staged list is read once on its own first, so a failing `git diff` stops the hook rather than reading as an empty list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inner sh takes a literal `sh` as its name and reads the base or the fetcher path from its first argument, so an error names the shell. The scan root's fallback calls `lookup_start` rather than restating it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
Git prints a Windows top level with forward slashes, so the test compares resolved paths rather than strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
…rge on a Running Hosted Review, and Gate Skill Description Length (#3843) Promotes the changes merged into `develop` since promotion #3797 to `main`, the end of an away-mode backlog burndown. - [#3803](#3803) exempts the Roslyn embedded-language marker (`// lang=json`, `language=`) from the prose gate's comment rules as a tool directive. - [#3807](#3807) has `local_review.py record` take `--report`, the file the reviewer wrote, bind the pass to its sha256, and derive the finding count from it. `status` gains `reportPath`. - [#3809](#3809) holds `pr_review.py merge` while a hosted review is still running on the head, and routes a declined Qodo finding to a dismiss request. - [#3811](#3811) dates an anchored `intentRef` by the section its anchor names, and lets a `driftNote` acknowledge a verified intent advisory. - [#3816](#3816) moves the null GraphQL node check into the shared `gh_graphql` transport as `NullNodeError`. - [#3818](#3818) explains the workspace-edit default in `GOVERNANCE.md` and names the aggregator's expression form. - [#3822](#3822) moves the abandon discard to the `backlog-burndown` orchestrator, aligns the abandoned-branch delete, and holds a grown agent-safety set. - [#3823](#3823) scopes the Husky prose gate to staged paths and adds `--skip-generated` so a named file inside a generated tree is skipped. - [#3828](#3828) reads Copilot's `Open findings:` label, drops the duplicate `null_node`, and matches `scripts/README.md` to the Merge Gate. - [#3829](#3829) bumps the two stale hub caller snippet pins and documents the fallback's mixed-pair refusal. - [#3831](#3831) keeps an inherited `GIT_DIR` from rerooting the prose gate's subdirectory git reads. - [#3832](#3832) gives `backlog-burndown` a disposition for a worker's standalone clone and keeps the agent-safety hold past a park. - [#3835](#3835) makes `carry.py check-sections` refuse what `apply-sections` would, and name an open fence. - [#3836](#3836) pins the reusable-workflow stubs by placeholder and ticks the PlexCleaner gates item. - [#3838](#3838) has the `runtime-secrets` audit dimension detect a bare `secrets/` directory from the tracked files that name it. - [#3839](#3839) gates skill descriptions at the 1024-character limit and trims every one over it. - [#3846](#3846) fixes an adjacent doc, an in-file pre-existing defect, and an edge case on new code in the open pull request instead of filing an issue for each, and keeps filing for code that runs elsewhere. - [#3847](#3847) refuses skill frontmatter that parses to a list or a scalar, so the description length gate cannot be bypassed. Fixes #3770 Fixes #3699 Fixes #3705 Fixes #3789 Fixes #1819 Fixes #727 Fixes #3710 Fixes #3706 Fixes #3623 Fixes #3687 Fixes #3617 Fixes #3757 Fixes #3790 Fixes #3786 Fixes #3785 Fixes #3772 Fixes #1657 Fixes #3443 Fixes #3824 Fixes #3817 Fixes #3805 Fixes #1666 Fixes #3700 Fixes #3821 Fixes #3771 Fixes #3794 Fixes #3825 Fixes #3690 Fixes #1472 Fixes #3813 Fixes #3719 Fixes #1592 Fixes #3845 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Summary
The Husky pre-commit hooks now hand the prose gate the paths the commit stages, rather than
.. An unrelated untracked scratch file no longer refuses every commit (#1657, option 1 of the maintainer's answer).Naming paths would have made the gate read a staged file inside a generated tree, which a
.scan skips. So the gate gains--skip-generated. It drops a named file inside agenerated_treesdirectory the same way a directory scan does. It reads the carried list and the repository's own.prose-gate.jsonadditions. Thepre-commitsnippet passes the flag and drops itsexclude: (^|/)reports/copy of that list, and its README drops the sentence about mirroring it (#3443)..github/actions/prose-gate/prose_lint.py,prose-gate.json:--skip-generated, applied indiscover()to a named file. Without the flag a named file is still read wherever it sits, soprose_lint.py reports/x.mdby hand is unchanged..husky/pre-commit,catalog/snippets/husky/pre-commit:git diff --cached --name-only --diff-filter=ACMRT -z, kept only where[ -f ]holds, so a submodule, a dangling symlink, or a staged-then-deleted path does not reach the gate'serror-not-a-pathrefusal. The innershskips the gate on an empty list, since a gate given no path scans.with untracked files. It checks itself rather than relying onxargs -r, because GNU xargs runs once on empty input and BSD xargs does not. The list is read once on its own before the pipeline, so a failinggit diffstops the hook rather than reading as an empty list. The catalog snippet callsuv run --no-project python "$hub_fetch_run"for the gate, the form thepre-commitsnippet already uses, since xargs cannot call therun_pyshell function. The working-tree content is still what is read, so a partially staged file is still judged on all of its edits.prose_lint.pyrepo_root(): git hands a hook in a linked worktreeGIT_DIR, and under itgit -C <dir> rev-parse --show-toplevelreports<dir>itself. So the first commit through the new hook, from a worktree, was refused as spanning several repositories. The lookup now drops the location variablescheckout_provenancealready drops (GIT_LOCATION). A.scan never met this, because the hook stands at the top level.scripts/README.md,catalog/snippets/pre-commit/README.md: the hook's scope and the generated-tree sentence now describe this.Verification
tests/test_prose_lint.py: three new cases inTestTheScanScopeIsTheScopeReported, on constructed data. A staged file atreports/run/audit.mdreports its bait when named and passes under--skip-generated. A repository-declaredgenerated_treesentry is honored while an authored file in the same run is still read. An untracked file is not read when the run names only the staged path. A test also pins one repository lookup per directory. Full suite undertests/requirements.txt: 3336 tests OK.tests/test_pre_commit_hook.py: the fixture moves to a sharedHookCase. Five new cases drive the hub hook through real commits beside an untracked file holding a finding. A deletion-only commit skips the gate. An edit plus a staged report underreports/passes, and a staged finding is refused without naming the untracked file. Those three fail against the hook that scanned.. A staged-then-removed path is dropped and a typechange is read, each failing once its filter is removed. The merge-commit cases still pass.reports/_template.mdstaged atreports/run/audit.md, a deletion-only commit, a staged-then-deleted path, and a dangling symlink each exit 0. An authored file with a duplicated word, its name holding a space, exits 1. So do a new comment line and a symlink replaced by a regular file holding a finding.hub_fetch_run.py. An untracked note and a staged report pass, and an added comment is refused.repo_gate.pyclean.The hook comments are wanted, so this PR carries the
commentslabel.Local review
ACMRdropped a symlink replaced by a regular file, soTis kept. So is the per-path repository lookup, which now runs once per directory. The duplicated git-environment filter now shares onegit_env()helper. And the Husky README's count ofhub_fetch_run.pycalls is dropped.[ -f ]filter or theTfilter is dropped.lookup_start()now holds where a git lookup starts, forrepo_root()andmain().error-not-a-pathrefusal deliberately refuses rather than falling back to another scan, for every caller. Thepre-commitframework already drops a non-file through its defaulttypes: [file].catalog/snippets/hub_fetch_run.pyline 9's exampleprose_lint.py . --diff HEADis a valid by-hand run of the fetcher, the formscripts/README.mddocuments. It makes no claim about what a hook passes, so it is left as written.shtakes a literalshas$0. So an error such as a missing uv names the shell, nothub_fetch_run.pyor the base.main()'sscan_rootfallback now callslookup_start()too.git diff --name-only.Pre-existing findings, not filed
catalog/snippets/husky/pre-commit: no test drives the catalog Husky snippet, only the hub's own hook (coverage, pre-existing). This change ran it by hand against constructed commits, as listed under Verification. Stand-ins replacedhub_fetch_run.py,dotnetand the formatters.catalog/snippets/husky/pre-commit,catalog/snippets/pre-commit/.pre-commit-config.yaml: a merge commit downstream still diffs againstHEADrather thanMERGE_HEAD(correctness, pre-existing). It was closed unfixed under the backlog-scope decision, so it is not refiled here.Dropped findings
prose_lint.pydiscover(): its start computation, with a.fallback for a path that is neither file nor directory, could calllookup_start(consolidation, untouched text).prose_lint.pyGIT_LOCATION: its list differs fromspec/git_env.py's, which the fetched gate cannot import. The list predates this change, which only moves its comprehension intogit_env()(consolidation, untouched text).Deferred
GOVERNANCE.mdline 299 still implies a plain git hook scans the tree. That file is held by another lane, so the one-clause fix is Say in GOVERNANCE.md That the Husky Hook Also Passes the Staged Paths to the Prose Gate #3820.Pre-existing findings, filed
prose_lint.pytracked_paths,untracked_paths,shallow_checkout,git_bytes: an inheritedGIT_DIRreroots a subdirectory run inside a linked-worktree hook (Keep an Inherited GIT_DIR From Rerooting the Prose Gate's Subdirectory Git Reads #3821).Closes on promotion: #1657, #3443
🤖 Generated with Claude Code