From 9955afe8b63945b0a9dec63f6ea57ad93084f614 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 26 Aug 2026 21:51:24 -0500 Subject: [PATCH 1/2] fix(gate): rule 3c's deny named a mechanism a scoped write does not use (BACKLOG #1082) Rule 3c had ZERO mentions of --global/--system, so every disarm write got the same sentence: "would change the SHARED git configuration of ". That is false for those scopes -- the write lands in the per-user or machine-wide file. THE VERDICT DOES NOT MOVE AND MUST NOT. Git falls back to those scopes when the repository does not set the key, so the write can still disarm the checkout by inheritance. An earlier filing of this as a FALSE DENY was withdrawn by three verifiers who measured the hook genuinely not firing afterwards; flipping rule 3c to ALLOW would have been a fail-open. The opening and the mechanism paragraph now branch on scope. A scoped write is told which file it actually writes and why it is refused anyway. The repository-scope wording is unchanged and pinned by its own control, so this did not trade one false sentence for another. NOTHING IN THE NEW TEXT REASSURES, and the tests pin the ABSENCE of four phrases rather than the presence of one. That is this row's round-4 lesson: segments are judged one at a time and Write-Deny exits on the first hit, so a reassurance prints over a real disarm when a LATER segment does a local write. A candidate that printed "This does NOT change the shared configuration" was graded BLOCKING by three verifiers for exactly that. TESTED BY THE STRING, NOT THE VERDICT -- forced, not stylistic. The verdict is identical before and after, so a row ending in a bare assert_denied cannot see this defect, and the round-4 mutant that reinstated the old sentence survived such a row. Mutation: disabling the scope branch reds FOUR wording rows and leaves THREE green -- the two verdict rows and the repository-scope control. That split is the evidence that the verdict genuinely does not move. A QUALIFIER THE ROW DOES NOT HAVE, measured here: whether a --global write takes effect depends on WHERE IT IS READ FROM. From a worktree core.hooksPath is set at worktree scope (75 of 76 config.worktree files) so global loses; from the primary it is unset at every scope (--get exit 1, control core.bare exit 0) so global WOULD take effect. The row states only the first. Denying is correct for both. The multi-line residual the row names is NOT fixed here and still needs its own item: segments split on lines and the first hit exits. Regression: 638 passed, 0 failed across all eight gate suites plus the new file -- 631 before, so the delta is exactly the seven tests added. Co-Authored-By: Claude Opus 5 --- docs/BACKLOG.md | 22 ++++++ scripts/hooks/worktree_gate.ps1 | 51 ++++++++++-- tests/test_worktree_gate_scope_wording.py | 95 +++++++++++++++++++++++ 3 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 tests/test_worktree_gate_scope_wording.py diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index d7bf9efb7..6bb1b9a68 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -5214,6 +5214,28 @@ A bare `Write-Output main$(calc):seed.txt` emits `mainPWNED-EXECUTED:seed.txt`. **Source:** found 2026-08-06 by a three-pass audit (inventory, adversarial attack, refutation) of the committed gate, prompted by a coordination message from a peer session generalising this gate. That message's own premise was overturned by measurement: the values it named are newline-unreachable here, and the reachable defect was one its proposed fix would not have closed. ## 1082. Rule 3c's deny text for a `--global` or `--system` disarm write names the wrong mechanism, and whether the write takes effect is not knowable from the command +> **THE WORDING IS FIXED 2026-08-27; the banner stays open for the archive pass. THE VERDICT DID NOT +> MOVE, which is the property this item most needed protecting.** Rule 3c had ZERO mentions of +> `--global`/`--system`, so it emitted the shared-config sentence for every scope. It now branches: a +> scoped write is told which file it actually writes, and why it is refused anyway (git falls back to +> that scope when the repository does not set the key). The repository-scope wording is UNCHANGED and +> pinned by its own control, so this did not trade one false sentence for another. +> +> **NOTHING IN THE NEW TEXT REASSURES**, and the tests pin the absence of four phrases rather than the +> presence of one. That is this row's round-4 lesson: segments are judged one at a time and `Write-Deny` +> exits on the first hit, so a reassurance prints over a real disarm when a LATER segment does a local +> write. +> +> **A QUALIFIER THIS ROW DOES NOT HAVE, measured 2026-08-27: whether a `--global` write takes effect +> depends on WHERE IT IS READ FROM, not only on the repository.** From a worktree `core.hooksPath` is +> set at worktree scope -- **75 of 76** `config.worktree` files set it -- so global loses to the more +> specific scope. **From the primary it is unset at every scope** (`--get` exit 1, against a control of +> `core.bare` exit 0), so a `--global` write there WOULD take effect. The row states only the first and +> reads as though it settled the question for the repository. Denying stays correct for both. +> +> **The multi-line residual this row names is NOT fixed here** and still needs its own item: segments +> are split on lines and the first hit exits, so a multi-line command is judged by its first segment. +> > 🔢 **Re-scored 2026-08-20 -> P3.** Value **4/10** · Difficulty **3/10** · _fill-in_. The wrong sentence is still the only one rule 3c emits, and the rule reaches it for a --global write because the only pre-deny exclusion at :981 is for reads, so a governed cwd resolves and denies with prose that names a mechanism the write does not use. The verdict must stay deny, so the work is a scope-aware wording plus a test that asserts the string rather than the verdict. _(was 4/10 · 3/10.)_ > diff --git a/scripts/hooks/worktree_gate.ps1 b/scripts/hooks/worktree_gate.ps1 index 1a8e0cff3..983181736 100644 --- a/scripts/hooks/worktree_gate.ps1 +++ b/scripts/hooks/worktree_gate.ps1 @@ -1417,13 +1417,54 @@ What to do instead: } if (-not $govCfg) { continue } + # BACKLOG #1082: SAY WHAT IS TRUE OF THE SCOPE ACTUALLY BEING WRITTEN. A --global or --system + # write does NOT land in this repository's config -- it lands in the per-user or machine-wide + # file -- so the sentence below was false for those scopes. THE VERDICT DOES NOT MOVE: git + # falls back to those scopes when the repository does not set the key, so the write can still + # disarm this checkout by inheritance, and whether it does is NOT knowable from the command. + # + # Measured 2026-08-27 and it depends on WHERE YOU READ FROM, which the row does not say: + # from a worktree core.hooksPath IS set at worktree scope (75 of 76 config.worktree files) + # -> a --global write of that key loses to the more specific scope + # from the primary UNSET at every scope (exit 1; control core.bare exit 0) + # -> a --global write of that key WOULD take effect + # Denying is the correct conservative default for both. + # + # NOTHING HERE MAY REASSURE. A round-4 candidate printed "This does NOT change the shared + # configuration" and three verifiers graded it BLOCKING: segments are judged one at a time and + # Write-Deny exits on the first hit, so on a multi-line command whose LATER segment does a + # local write, that sentence would print over a real disarm. + $scopeFlag = if ($seg.Scan -cmatch '(?:^|\s)--(?global|system)(?:\s|$)') { $Matches['sc'] } else { $null } + $opening = if ($scopeFlag) { + "BLOCKED: '$badKey' is on the disarm list, and --$scopeFlag writes your " + + $(if ($scopeFlag -eq 'global') { 'per-user' } else { 'machine-wide' }) + + " git configuration rather than this repository's.`n`nGit falls back to that scope when " + + "$($govCfg.Display) does not set the key itself, so the write can still disarm this " + + "checkout's hooks. Which case applies is not knowable from the command alone, so it is refused." + } + else { + "BLOCKED: setting '$badKey' would change the SHARED git configuration of $($govCfg.Display)." + } + # The mechanism paragraph is scope-aware for the same reason the opening is: the shared-.git + # sentence is TRUE of a repository-scope write and FALSE of --global/--system. + $mechanism = if ($scopeFlag) { + "This is refused on the REACH, not on where the file lands: a key inherited from the " + + "$(if ($scopeFlag -eq 'global') { 'per-user' } else { 'machine-wide' }) scope applies to " + + "every repository on this machine that does not override it, including every worktree of " + + "$($govCfg.Display)." + } + else { + "Every worktree of this repository shares one .git directory, so this is not a local " + + "change: it takes effect for all of them at once." + } Write-Deny -Rule "3c" -Detail "git config $badKey" -Reason @" -BLOCKED: setting '$badKey' would change the SHARED git configuration of $($govCfg.Display). +$opening + +$mechanism -Every worktree of this repository shares one .git directory, so this is not a local change: it takes -effect for all of them at once. Repointing core.hooksPath (or aliasing a command, or redirecting -core.worktree) disables the commit-time ledger, claim and secret-leak gates for every session on this -machine, and nothing would report that they had stopped running. +Repointing core.hooksPath (or aliasing a command, or redirecting core.worktree) disables the +commit-time ledger, claim and secret-leak gates, and nothing would report that they had stopped +running. What to do instead: * If a commit hook is failing, FIX THE CAUSE -- the hook output names it. Never route around a gate; diff --git a/tests/test_worktree_gate_scope_wording.py b/tests/test_worktree_gate_scope_wording.py new file mode 100644 index 000000000..78dc1ab5a --- /dev/null +++ b/tests/test_worktree_gate_scope_wording.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 MessageFoundry Organization and contributors +"""BACKLOG #1082: rule 3c's deny text named a mechanism a ``--global``/``--system`` write does not use. + +**THE VERDICT WAS ALWAYS CORRECT AND DOES NOT MOVE HERE.** ``git config --global core.hooksPath `` +aimed at a governed repo is refused, and should be: git falls back to the per-user scope when the +repository does not set the key, so the write can still disarm the checkout **by inheritance**. An +earlier filing of this as a FALSE DENY was withdrawn by three independent verifiers who measured the +hook genuinely not firing afterwards. Flipping rule 3c to ALLOW would have been a fail-open. + +What was wrong is the SENTENCE. It said the write *"would change the SHARED git configuration of +"*, and it does not -- it writes ``~/.gitconfig`` or the machine-wide file. + +**EVERY TEST HERE ASSERTS THE EMITTED STRING, NOT THE VERDICT.** That is forced, not stylistic: the +verdict is identical before and after, so a row ending in a bare ``assert_denied`` cannot see this +defect at all, and a mutant that reinstated the old sentence would pass such a row unchanged. + +**NOTHING IN THE MESSAGE MAY REASSURE.** A round-4 candidate printed *"This does NOT change the shared +configuration"* and three verifiers graded it BLOCKING: ``Get-ScannableSegments`` splits on lines and +``Write-Deny`` exits on the first hit, so on a multi-line command whose LATER segment does a local +write, that reassurance would print over a real disarm. The negative rows below pin its absence. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from tests.test_worktree_gate import assert_denied, run_gate +from tests.test_worktree_gate_control_plane import repo, shell # noqa: F401 + +SCOPED = [ + ("global", "git config --global core.hooksPath /dev/null"), + ("system", "git config --system core.hooksPath /dev/null"), +] + +#: Phrases that must never appear in a scoped deny. The first is the false mechanism this item is +#: about; the rest are the reassurance shapes graded BLOCKING in round 4. +FORBIDDEN_IN_SCOPED = [ + "SHARED git configuration", + "does NOT change", + "is harmless", + "safe to run", +] + + +@pytest.mark.parametrize("scope,command", SCOPED) +def test_a_scoped_write_is_still_denied(repo: SimpleNamespace, scope: str, command: str) -> None: # noqa: F811 + """THE VERDICT ARM. Kept separate from the wording arms so a future change that fixed the sentence + by ALLOWING the write fails here rather than passing quietly.""" + assert run_gate(shell(command, cwd=repo.primary), repo.repos) is not None, ( + f"--{scope} disarm write was ALLOWED. The verdict must not move: git inherits from that scope " + "when the repository does not set the key, so the write can still disarm this checkout." + ) + + +@pytest.mark.parametrize("scope,command", SCOPED) +def test_a_scoped_deny_does_not_claim_the_repo_config_changed( + repo: SimpleNamespace, # noqa: F811 + scope: str, + command: str, +) -> None: + reason = assert_denied(run_gate(shell(command, cwd=repo.primary), repo.repos)) + for phrase in FORBIDDEN_IN_SCOPED: + assert phrase not in reason, ( + f"--{scope} deny text contains {phrase!r}. Either it names a mechanism this write does not " + "use, or it reassures -- and a reassurance prints over a real disarm when a later segment " + f"of a multi-line command does a local write.\n\n{reason}" + ) + + +@pytest.mark.parametrize("scope,command", SCOPED) +def test_a_scoped_deny_names_the_scope_and_the_inheritance( + repo: SimpleNamespace, # noqa: F811 + scope: str, + command: str, +) -> None: + """Say what IS true: which file is written, and why it is refused anyway.""" + reason = assert_denied(run_gate(shell(command, cwd=repo.primary), repo.repos)) + assert f"--{scope}" in reason, f"the deny text does not name the scope it refused\n\n{reason}" + assert "inherit" in reason.lower() or "falls back" in reason.lower(), ( + f"the deny text does not say WHY a write to another file is refused\n\n{reason}" + ) + + +def test_a_repository_scope_write_still_says_SHARED(repo: SimpleNamespace) -> None: # noqa: F811 + """THE CONTROL. The original sentence is CORRECT for a repository-scope write and must survive + unchanged -- otherwise this fix would have traded one false sentence for another.""" + reason = assert_denied( + run_gate(shell("git config core.hooksPath /dev/null", cwd=repo.primary), repo.repos) + ) + assert "SHARED git configuration" in reason, ( + f"the repository-scope wording was changed; it was already true and should not move\n\n{reason}" + ) From 60a93944ae7055a2709caadac4721bc3c9be3568 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 27 Aug 2026 18:28:46 -0500 Subject: [PATCH 2/2] test(tooling): list the scope-wording gate test in the tooling manifest (BACKLOG #1082) PR 635 is RED on three required legs with one deterministic cause: tests/test_tooling_partition.py::test_every_non_engine_test_is_classified "unclassified: ['test_worktree_gate_scope_wording.py']" Same defect and same fix as PR 634, which is the point worth recording: two branches off one lane both added a gate test file and neither listed it, so this is a lane habit rather than a one-off slip. The same lane's #1296 branch DOES get it right, so the knowledge was present and the step was skipped. Manifest, not _STAYS_WITHOUT_IMPORTING: the test drives the hook as a subprocess and does not read engine source. NOT A FLAKE. The lander playbook records tooling_partition as never flaky and says a re-run past one lands a defect on purpose. Three legs, one cause, is the signature. INSERTED IN THE FILE'S OWN ORDER, NEVER SORTED -- tests/tooling_manifest.txt is not plain lexicographic and a sort() rewrites unrelated lines. Anchored on the following entry. Measured, with an interpreter borrowed from a sibling worktree (this one has no venv) and VERIFIED to read THIS tree rather than the lender's: red-first: reverting only this line reds test_every_non_engine_test_is_classified and nothing else (1 failed / 8 passed); restoring it returns 9 passed git diff --numstat reads 1/0 -- a re-sort would have shown roughly 22 on both sides rebased onto a490993b4 first; the rebase was clean Rebase note for whoever pushes: this branch was 23 behind and is now ahead 1 of a490993b4, so the PR head moves and needs a force-update with a lease. Co-Authored-By: Claude Opus 5 --- tests/tooling_manifest.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tooling_manifest.txt b/tests/tooling_manifest.txt index 433d8cd67..d7ed7ccfb 100644 --- a/tests/tooling_manifest.txt +++ b/tests/tooling_manifest.txt @@ -130,6 +130,7 @@ tests/test_worktree_gate_quote_straddle.py tests/test_worktree_gate_receipts.py tests/test_worktree_gate_remedy_families.py tests/test_worktree_gate_rule_agnostic_coverage.py +tests/test_worktree_gate_scope_wording.py tests/test_worktree_gate_shell_semantics.py tests/test_worktree_gate.py tests/test_worktree_new_cleanup_advice.py