Skip to content

Scope the Husky Prose Gate to Staged Paths and Skip Generated Trees by Flag - #3823

Merged
ptr727 merged 7 commits into
developfrom
burndown/1657-staged-prose-hook
Nov 2, 2026
Merged

ptr727 merged 7 commits into
developfrom
burndown/1657-staged-prose-hook

Conversation

@ptr727

@ptr727 ptr727 commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

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 a generated_trees directory the same way a directory scan does. It reads the carried list and the repository's own .prose-gate.json additions. The pre-commit snippet passes the flag and drops its exclude: (^|/)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 in discover() to a named file. Without the flag a named file is still read wherever it sits, so prose_lint.py reports/x.md by 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's error-not-a-path refusal. The inner sh skips the gate on an empty list, since a gate given no path scans . with untracked files. It checks itself rather than relying on xargs -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 failing git diff stops the hook rather than reading as an empty list. The catalog snippet calls uv run --no-project python "$hub_fetch_run" for the gate, the form the pre-commit snippet already uses, since xargs cannot call the run_py shell 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.py repo_root(): git hands a hook in a linked worktree GIT_DIR, and under it git -C <dir> rev-parse --show-toplevel reports <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 variables checkout_provenance already 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 in TestTheScanScopeIsTheScopeReported, on constructed data. A staged file at reports/run/audit.md reports its bait when named and passes under --skip-generated. A repository-declared generated_trees entry 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 under tests/requirements.txt: 3336 tests OK.
  • tests/test_pre_commit_hook.py: the fixture moves to a shared HookCase. 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 under reports/ 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.
  • Hub hook driven through constructed commits in a throwaway repository. An untracked scratch comment, a copy of reports/_template.md staged at reports/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.
  • The catalog Husky snippet driven the same way, with a local shim standing in for hub_fetch_run.py. An untracked note and a staged report pass, and an added comment is refused.
  • shellcheck and shfmt clean on both hooks. ruff format, ruff check, mypy clean. repo_gate.py clean.

The hook comments are wanted, so this PR carries the comments label.

Local review

  • The typechange gap is fixed: ACMR dropped a symlink replaced by a regular file, so T is kept. So is the per-path repository lookup, which now runs once per directory. The duplicated git-environment filter now shares one git_env() helper. And the Husky README's count of hub_fetch_run.py calls is dropped.
  • Second pass: a staged-then-removed path and a typechange each now have a hook case. Each fails when the [ -f ] filter or the T filter is dropped. lookup_start() now holds where a git lookup starts, for repo_root() and main().
  • Declined: moving the non-file filter and the empty-list skip into the gate. The gate's error-not-a-path refusal deliberately refuses rather than falling back to another scan, for every caller. The pre-commit framework already drops a non-file through its default types: [file].
  • Declined: xargs may split a very large commit into several gate runs. The hook still fails when any batch fails, since xargs exits 123, and each batch's scope line names its gate commit. So a split never turns into a false pass.
  • Third pass, edit budget spent. Declined: catalog/snippets/hub_fetch_run.py line 9's example prose_lint.py . --diff HEAD is a valid by-hand run of the fetcher, the form scripts/README.md documents. It makes no claim about what a hook passes, so it is left as written.
  • Fix pass, after the hosted review: the inner sh takes a literal sh as $0. So an error such as a missing uv names the shell, not hub_fetch_run.py or the base. main()'s scan_root fallback now calls lookup_start() too.
  • Last pass, edit budget spent. Declined: the standalone read of the staged list before the pipeline was called over-hardening, since git has loaded the index before it starts the hook. It stays. Without it a failed read silently skips the gate, and the hook is also run by hand, which its own comment allows for. The cost is one 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 replaced hub_fetch_run.py, dotnet and the formatters.
  • catalog/snippets/husky/pre-commit, catalog/snippets/pre-commit/.pre-commit-config.yaml: a merge commit downstream still diffs against HEAD rather than MERGE_HEAD (correctness, pre-existing). It was closed unfixed under the backlog-scope decision, so it is not refiled here.

Dropped findings

  • prose_lint.py discover(): its start computation, with a . fallback for a path that is neither file nor directory, could call lookup_start (consolidation, untouched text).

  • prose_lint.py GIT_LOCATION: its list differs from spec/git_env.py's, which the fetched gate cannot import. The list predates this change, which only moves its comprehension into git_env() (consolidation, untouched text).

Deferred

Pre-existing findings, filed

Closes on promotion: #1657, #3443

🤖 Generated with Claude Code

ptr727 and others added 4 commits November 2, 2026 10:22
…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>
Copilot AI lite review requested due to automatic review settings November 2, 2026 18:49
@ptr727 ptr727 added the comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses label Nov 2, 2026
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@ptr727

ptr727 commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 96deead83ecdd4d10e527b6d2dd88bc9359820d8, the content this pull request carries at that commit against develop, and it recorded 3 findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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-generated support 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 -r and 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

Comment thread .husky/pre-commit Resolved
Comment thread .husky/pre-commit Resolved
Comment thread catalog/snippets/husky/pre-commit Resolved
Comment thread catalog/snippets/husky/pre-commit Resolved
@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.91%. Comparing base (b38e084) to head (dfc5093).
⚠️ Report is 76 commits behind head on develop.

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     
Flag Coverage Δ
python-3.13 82.91% <ø> (+1.30%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ptr727 and others added 2 commits November 2, 2026 10:57
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>
@ptr727

ptr727 commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 02ed8f1b79420c5aa54ee32326ff5945e58f0a01, the content this pull request carries at that commit against develop, and it recorded 1 finding.

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>
@ptr727

ptr727 commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head dfc5093cb836d17e2e1f668ec7599fa107f0b436, the content this pull request carries at that commit against develop, and it recorded 2 findings.

@ptr727
ptr727 merged commit c11bea7 into develop Nov 2, 2026
13 checks passed
@ptr727
ptr727 deleted the burndown/1657-staged-prose-hook branch November 2, 2026 19:48
ptr727 added a commit that referenced this pull request Nov 3, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants