fix(coord): make the claim gate satisfiable from a second repository (BACKLOG #1346) - #798
Conversation
…(BACKLOG #1346) claim.ps1 used ONE value for TWO questions -- where the registry is, and who holds the claim. Inside one repository those are the same tree, so the conflation was invisible; across two they diverge. The vault's installed claim_check.py therefore read a registry the claim tool never wrote, and no code-touching vault commit whose SUBJECT cited a ledger number could pass, however honestly the item was held. The only route through was to cite the item in the commit BODY, which the gate permits by design -- a gate whose sole remedy is a sanctioned way around it is the state that manufactures evasion. Option taken: ONE SHARED REGISTRY, both gates reading it. `git config mefor.claimsRoot <path>` is set in the repository that does not host the registry, and BOTH halves resolve from the repository the claim is FOR, so they cannot disagree about where to look. `claim.ps1 -AsWorktree <tree>` names the holder when the tool runs from a tree it does not live in -- the half the pointer alone does not cover, because the gate also compares the record's worktree against the tree being committed. Unset, behaviour is what shipped before, and that is asserted rather than asserted about, so no existing claim is invalidated. An unresolvable pointer FAILS CLOSED on both sides: the silent fallback would send the gate to a directory nothing writes, where a misconfigured pointer presents as an honestly unclaimed item. The gate now also says WHERE it looked and prints a remedy that runs from the repository it is printed in; install-git-hooks.ps1 -Status reports the same directory. That missing diagnostic is most of why the split went unnoticed. REPRODUCED FIRST, then proven by mutation. tests/test_claim_shared_registry.py git-inits two independent checkouts: 5 of 6 arms red before the fix, with the single-repository arm green in the same run as the positive control. The measured refusal was "is NOT CLAIMED", not "claimed by ANOTHER worktree" -- the proof the gate was reading an empty registry rather than adjudicating one. Reverting the pointer reproduces all 5; reverting the holder split kills exactly the one arm it serves. The vault half is unverified by construction: CLAUDE.md limits reading that tree to roles/, so whether it carries its own claim.ps1 is still unmeasured. The fix is built not to depend on the answer -- the pass arm is tested in both shapes. claim.ps1's divergence note was reworded because -AsWorktree makes its old sentence false in exactly the case the flag exists for. The behaviour it asserts is unchanged.
…oduce (BACKLOG #1346)
|
LANDER: applying SO READ THIS LABEL AS A BYPASS TOKEN, NOT A REVIEW. It records that the gate was cleared by instruction. It does not assert that anyone read this diff, and no later reader should infer one from it. What I can say: this PR appeared in a read-only pre-verification sweep of all 59 non-draft open PRs (per-PR defect read, plus pairwise -- Lander |
tests/test_tooling_partition.py::test_every_non_engine_test_is_classified reds all three required test legs when a test that does not import the engine is absent from tests/tooling_manifest.txt. This PR adds such a test, so CI could not go green as it stood. The manifest is read as a set, so this is a single inserted line at its alphabetical slot; no existing line moves.
|
Lander: added the missing
Verified before pushing: the test fails on the previous head naming this exact file, and passes with Re-applied the |
The conflict is mine, not this branch's. During the manifest repair I inserted a line into this pull request and into 825 at the SAME alphabetical slot, between tests/test_claim_check.py and tests/test_gate_ci_mirror_parity.py. Each insert was correct alone; together they conflict, which surfaced once 825 landed. Both lines are needed, so both are kept in alphabetical order. Verified with tests/test_tooling_partition.py and by confirming each path appears exactly once.
Closes BACKLOG #1346.
The unpassable gate reproduced, by execution, before anything was changed
tests/test_claim_shared_registry.pygit-inits two independent checkouts, claimsfrom one and commits in the other. Before the fix: 5 of 6 arms red, and the
single-repository arm GREEN in the same run as the positive control that both
the harness and the gate work.
The measured refusal was
is NOT CLAIMED-- notclaimed by ANOTHER worktree. That is the proof the gate was reading an empty registry rather thanadjudicating one, and it is why the "must fail" control arm asserts the refusal
TEXT: without that, the arm is green and blind, because a gate that cannot see
refuses everything and scores as a gate that works.
Re-measured rather than trusted. The row's engine-side line numbers all hold:
claim.ps1anchors$repoon$PSScriptRootand derives the claims directoryfrom it;
claim_check.py::_claims_dirresolved the committing tree's owngit-common-dir;
install-git-hooks.ps1anchorsRepoRooton its own script andships only
claim_check.pyandpush_guard.py, so it cannot install a writerinto a second repository.
The root cause is narrower than the row states, and naming it changed the fix
claim.ps1used one value for two questions: where the registry is andwho holds the claim. Inside one repository those are always the same tree, so
the conflation is invisible. Across two they diverge.
This matters for the design ruling. The Console directed the first option --
the gate reads the registry the tool writes -- and that option is correct, but it
is necessary and not sufficient on its own. The gate also compares the
record's
worktreeagainst the tree being committed, so a claim taken by theengine's tool still reads as held by another worktree from the second
repository. The gate stays unpassable with the pointer alone.
So both halves shipped. This is still the FIRST option, completed -- it is not
the second. Nothing writes a vault-local registry; there is one registry, and
both gates read it.
What was built
git config mefor.claimsRoot <path>, set in the repository that does NOT hostthe registry. Both halves resolve from the repository the claim is FOR,
never from the tree a script happens to live in, so the tool and the gate
cannot disagree about where to look.
claim.ps1 -AsWorktree <tree>names the holder when the tool is run from atree it does not live in. This is not a retreat from the
$PSScriptRootanchoring of #1060: that defect was a SILENT read of the caller's cwd; this is
an explicit argument, recorded in the claim, printed on every surface that
shows a holder, and refused outright when it names no repository.
asserted about, so no existing claim is invalidated.
would send the gate to a directory nothing writes, where a misconfigured
pointer presents as an honestly unclaimed item -- worse than a refusal.
the repository it is printed in (absolute tool path, quoted, with the holder to
record).
install-git-hooks.ps1 -Statusreports the same directory. Theabsence of that diagnostic is most of why the split went unnoticed: a refusal
against a registry in another repository is indistinguishable, from outside,
from an item nobody claimed.
Proven by mutation, both halves
mefor.claimsRoot-AsWorktreebecomes a no-opA fix to a gate that is not first shown failing is not evidence, so both arms
were reverted and re-run rather than argued.
I did not open the MessageFoundry-vault checkout
CLAUDE.md limits reading that tree to
roles/. Whether the vault carries itsown
claim.ps1is still the one unmeasured fact, exactly as the row said, andnothing here should be read as a claim about that tree's contents.
The fix is therefore built not to depend on the answer. The pass arm is
tested in BOTH shapes the second repository can be in: one test runs that
repository's own copy of the tool, the other runs this repository's copy by
absolute path with
-AsWorktree. Both land one record, in one registry, that thesecond repository's gate accepts.
What remains for whoever may open that tree: set
mefor.claimsRootthere,and re-run
install-git-hooks.ps1so the shared.git/hookspayload is not thepre-change copy.
The distinction the row insists on
Citing the item in the commit BODY to get past this gate was not the same act
as the 2026-08-06 evasion a sibling row records. There, claiming properly was
possible and the body was a way around a PASSABLE gate. Here it was the only
route through an unpassable one. The distinction is whether a correct alternative
existed -- it did not, and now one does.
Checks
Ran, all green:
ruff check .andruff format --check .(whole repo)mypy messagefoundry(strict, 267 files) andmypy scripts/hooks/claim_check.pypytestover the nine coordination, claim, hook and ledger suites --test_claim_shared_registry(7),test_claim_check,test_script_root_anchoring,test_coord_claim_refresh,test_coord_claim_release_history,test_coord_claim_adjudicate,test_hook_prose_folding,test_worktree_prune_merged,test_ledger_check: 191 passedscripts/docs/backlog_status_check.py(657 items, one status each)pre-commitbattery, on both commitscode, and it passed against the claim held for this worktree
Not run here, and they must be read on the runner: the full
pytestsuite andevery hosted-only leg.
One pre-existing red a reviewer will hit locally, and it is NOT this change
tests/test_installed_coord_hooks.pyreports the INSTALLED.git/hooks/push_guard.pyas stale against source.push_guard.pyis untouchedby this branch (
git diff HEAD -- scripts/hooks/push_guard.pyis empty) -- theinstalled copy on this box was already behind. The same module SKIPS
claim_check.pywith an explicit reason while its source has uncommittedchanges, which is the designed behaviour.
I did not re-install the hooks. That writes into the COMMON
.git/hooks,shared by every worktree and every running session on this box -- outside this
worktree, and not something git can undo. After merge, an operator should run
pwsh -NoProfile -File scripts\coord\install-git-hooks.ps1so the payload thatactually fires is this one.