fix(hooks): unify gate-marker root across linked worktrees, isolate palette tests from repo state - #616
Merged
Merged
Conversation
…alette tests from repo state CLAUDE_PROJECT_DIR) and the H-09b/H-10b/H-14 guards (a hook subprocess whose CLAUDE_PROJECT_DIR names the main checkout) resolved DIFFERENT roots for .codearbiter/.markers/ inside a linked git worktree, so a legitimately recorded gate pass was invisible to the guard. Adds hostapi.Host.marker_root() (git_worktree_main_root escalates project_root() to the main checkout only when it names a linked worktree's own checkout) and wires it into both producers and both guard checks, without moving the diff/migration SCAN root (which must stay bound to the tree actually being committed). Proven with a real `git worktree add` fixture: marker lands at the main checkout, the guard honors it, and still blocks with no recorded pass (test_repo_resolution.py, test_host_project_root.py). Also fixes the 10-test pre-existing failure family in test_git_hooks.py / test_repo_resolution.py that fires whenever the suite itself runs from inside a linked worktree: _githooks's own __file__ resolves to an ephemeral path, which is_ephemeral_path (#441/ADR-0014) correctly refuses to register into a fixture's shared drop-in dir. Fixtures now resolve the enforcer path from a durable_plugin_copy (the #442 fix's existing pattern) instead. project's .codearbiter/ state (task/override/gate-event counts) when no explicit cwd pinned it elsewhere, so a maintainer's own accumulated audit trail could make a required custom-palette color intermittently disappear. Pinned the state-sensitive render to an isolated temp dir, and added a new test that appends adversarial override/gate-event/task rows in its own fixture and proves the palette-completeness check still holds regardless. Version advance: ca 2.11.7 -> 2.11.10, ca-codex 0.4.6 -> 0.4.9, ca-pi 0.2.6 -> 0.2.9 (root package.json regenerated). CHANGELOG: linked-worktree sessions no longer lose a recorded security/migration gate pass, and the statusline palette test suite no longer depends on the developer's own accumulated .codearbiter/ audit trail. Claude-Session: https://claude.ai/code/session_01QjJeSbcwPHwMmd6CEZeagB
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
SUaDtL
added a commit
that referenced
this pull request
Aug 5, 2026
…elease-marker code (#567, #568) (#617) - #567: MARKER_FRESHNESS_MINUTES was five independent hardcoded `30` literals (pre-write.py, pre-edit.py, _bashguardlib.py, git-enforce.py, _protectedstatelib.py) with no import relationship. Centralized into one declaration in _hooklib.py beside marker_fresh itself; every flank now imports it. New test_marker_freshness_window.py pins the single-source property by value AND by source-level AST inspection of each call site (catches a flank that imports the constant but still passes a literal). - #568: _releaselib._PRERELEASE_MARKERS was unreachable dead code behind the anchored release-tag regex (confirmed unreachable, not merely suspected). Deleted along with its dead branch in last_tag_select; the obsolete "second line of defense" test is replaced with a pin proving the tuple stays gone. - #569: verified already fully satisfied by prior work (--cleanup=verbatim in the release skill, plus the real git-tag-a-F round-trip test in test_consumer_smoke.py) -- no code change needed; evidence cited in the PR body. Version advance: ca -> 2.11.11, ca-codex -> 0.4.10, ca-pi -> 0.2.11 -- the final slot in the ascending merge train (#614 took .8/.7/.7, #615 took .9/.8/.8, #616 took .10/.9/.9 on main; this PR merges last). See the PR body for the full sequencing note. CHANGELOG: The H-11 marker freshness window is now a single, imported constant instead of five independently hardcoded copies, and _releaselib's unreachable dead prerelease-marker code is removed. Claude-Session: https://claude.ai/code/session_01QjJeSbcwPHwMmd6CEZeagB Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes two "works on a clean checkout, breaks in a real environment" defects that have repeatedly polluted this repo's local test runs.
#604 — gate-marker root mismatch in a linked worktree
security-pass.py/migration-pass.py(invoked bare via a Bash tool call, noCLAUDE_PROJECT_DIRin that shell) and the H-09b/H-10b/H-14 commit guards (a hook subprocess whoseCLAUDE_PROJECT_DIRnames the main checkout) resolved different roots for.codearbiter/.markers/inside a linked git worktree — a legitimately-recorded gate pass was invisible to the guard.Fix: a new
hostapi.Host.marker_root()seam.git_worktree_main_root(root)escalates an already-resolvedproject_root()answer to the MAIN checkout only when that root is itself a linked worktree's own checkout (distinguished from a submodule via thegitdir:pointer's.git/worktrees/segment). Wired into both producers (security-pass.py,migration-pass.py) for the marker WRITE, and both guard checks (_check_h09b_h10b_crypto_secret,_check_h14_migration) for the marker READ — via_marker_root()in_bashguardlib.py. Deliberately not wired intoproject_root()itself:security-pass.py's diff SCAN must stay bound to the worktree's own tree (the issue's own warning — binding digests to the wrong tree would review lines nobody staged).git_toplevel'sgit rev-parsemechanism is unchanged, per the board history caution (bug #125, symlink/8.3 canonicalization) — the fix makes the two callers agree through the existing seam rather than replacing it.Also fixes the 10-test pre-existing failure family in
test_git_hooks.py/test_repo_resolution.pythat fires whenever the suite itself runs from inside a linked worktree:_githooks's own__file__resolves to an ephemeral path, whichis_ephemeral_path(#441/ADR-0014) correctly refuses to register into a fixture's shared drop-in dir. Fixtures now resolve the enforcer path from adurable_plugin_copy(the #442 fix's existing pattern) instead of the live (possibly-ephemeral) checkout.Before/after, run inside a real linked worktree of this repo (dirty local state):
test_git_hooks.py+test_repo_resolution.pyplugins/ca/hooks/tests(full suite)Proven end-to-end with a real
git worktree addfixture (TestGateMarkerAgreesAcrossLinkedWorktreeintest_repo_resolution.py): a marker written bysecurity-pass.py(bare, noCLAUDE_PROJECT_DIR) lands at the main checkout, not the worktree;pre-bash.py's H-09b guard honors it; and the guard still blocks with no recorded pass (no-fail-open converse).#552 — palette-compatibility tests read real repo state
test_colorlib.py's palette-compatibility tests render the live statusline against the real repository when a subprocess call has no explicitcwd—project_root()falls back to a git-toplevel climb from the test runner's own cwd, so a maintainer's own accumulated.codearbiter/audit trail (task counts, override counts, gate-event rows) could intermittently drop a required custom-palette color from the assertion window.Fix: pinned the state-sensitive subprocess render to an isolated temp dir (no
.git/.codearbiterreachable), and addedtest_custom_palette_survives_arbitrary_appended_codearbiter_state— a new test that builds its own.codearbiter/fixture, appends 40 override rows, 200 gate-event rows, several in-flight tasks, and an unresolved open question (the exact adversarial shape #552 describes accumulating over a repo's lifetime), and proves the palette-completeness assertion still holds.Mutation proofs
hostapi.git_worktree_main_root: forced it to returnNoneunconditionally →GitWorktreeMainRootTests,MarkerRootTests, and bothTestGateMarkerAgreesAcrossLinkedWorktreepositive-proof tests went red with the exact expected assertion failures; the no-fail-open converse test stayed green. Restored, re-verified green, re-synced._githooks's durable-copy fixture patch: disabledself._enforcer_patch.start()in both test files → the exact same 10 pre-existing failures reproduced with identical messages. Restored, re-verified green._colorlib._read_custom: forced it to returnNoneunconditionally → the new adversarial-state test failed with "missing custom colors" for all 7 required colors. Restored, re-synced, re-verified green.Version advance
ca 2.11.9 → 2.11.10, ca-codex 0.4.8 → 0.4.9, ca-pi 0.2.8 → 0.2.9 (root
package.jsonregenerated). All landed inside this PR's rebase onto the latestorigin/main(which had independently advanced to 2.11.9/0.4.8/0.2.8 via #614/#615 while this branch was in flight) — the target versions in this PR were already exactly one patch above that new base, so no further bump was needed after resolving the conflict.Deviations from plan
~/.claude/plugins/cache/codearbiter/ca/2.10.0) registered as the git-level.git/hooks/pre-commitenforcer — an environment fact unrelated to this fix (predates it, would recur on any PR). Resolved by running the same stalesecurity-pass.py(not this branch's fixed copy) so its root resolution matched what the stale enforcer checks — no gate was bypassed;/ca:overridewas considered and rejected since it required fabricating user acknowledgement for a security-critical stop that a non-bypass fix cleanly resolved instead.Closes #604
Closes #552
https://claude.ai/code/session_01QjJeSbcwPHwMmd6CEZeagB