feat(worktree): session-rescue tool (sessions.ps1) + an inert EnterWorktree rule - #36
Merged
Conversation
…rktree rule Ports the session-rescue work off the dormant salvage branch, where it had sat unmerged since 2026-07-24 while the failure it fixes stayed live. sessions.ps1 indexes sessions across every login on the box and rehomes a stranded one back to the primary's slug. It is read-only by default -- a bare run only lists -- and -Rehome is the sole destructive path, refusing a session that still looks live and honouring -WhatIf. The gate rule denying EnterWorktree is included but ships INERT: the live hook is a copy at ~/.claude/hooks, so nothing changes until install-gate.ps1 is re-run. That is deliberate. Rule 2 already denies a fan-out from the primary, so activating this one as well leaves a primary-resident session with no in-session path to a subagent at all -- it must be started in a worktree. That may well be the right trade, but it is a workflow change that deserves its own decision, and the rescue tool should not wait behind it. docs/WORKTREES.md records both sides so whoever runs install-gate.ps1 next sees the argument. Numbered rule 3 here, not 5 as on the source branch: this list has two entries, not four. Reconstructed rather than cherry-picked -- cherry-pick is refused by the worktree gate -- taking the four files whose base was byte-identical to main wholesale and applying the other two by anchor. Verified the tripwire can actually fail: pulling the matcher out of install-gate.ps1 reddens test_every_tool_the_gate_handles_is_registered_by_the_installer, and restoring it goes green. The source commit notes a rule once shipped dead, so the guard seemed worth exercising rather than trusting. Full suite: 9040 passed, 816 skipped, 1 failed. The failure -- test_anon_parity.py::test_golden_corpus_engine_output_equals_tee_output -- is PRE-EXISTING on main and unrelated: it reproduces identically on a clean baseline with no changes present, and in a subset run that collects neither of the test files touched here. It passes in isolation and fails only after other test_a* modules run, so it is a test-isolation defect. Reported separately.
wshallwshall
enabled auto-merge (squash)
July 29, 2026 03:01
wshallwshall
added a commit
that referenced
this pull request
Jul 29, 2026
The console publish job landed while this branch was open and installs pip, build and packaging unpinned. The guard added earlier on this branch rejects exactly that, so updating the branch against main turned all three test legs red -- the guard catching a real regression written after it was, which is the best evidence it works that this branch could have produced. pip and build take the same explicit pins the sibling build jobs use. packaging is derived from constraints.lock the way relsmoke and harnesssmoke already do, so one lock bump moves every release-path packaging install together instead of letting them drift apart. Also adds the check the pin guard structurally cannot make. Getting here, an edit put a literal 0x01 byte in release.yml -- a sed backreference written into a non-raw Python string -- and the file became unparseable YAML that GitHub Actions could never have run. The pin guard passed green through it, because it greps lines and never parses the document. So the file is now verified by parsing it, and its permissions blocks and triggers are compared against origin/main as parsed structures rather than eyeballed: jobs release, release-webconsole, release-harness; permissions identical; triggers identical. Verification: pin guard 15 passed; full suite 9331 passed, 818 skipped, 1 failed. That failure is tests/test_gate_installed_parity.py, which is PRE-EXISTING and unrelated -- it reproduces identically on clean main, this branch touches neither the gate source nor that test, and the test is LOCAL-MACHINE only (it skips on CI, where no installed gate exists). It reports that this workstation's installed ~/.claude/hooks copy is older than the source that PR #36 merged; re-running install-gate.ps1 is the fix, and that is deliberately an owner action because it activates the EnterWorktree rule for every session on the box.
wshallwshall
added a commit
that referenced
this pull request
Jul 29, 2026
…x, and pin the release toolchain (#48) * test(crypto): sweep the "encrypted at all" at-rest assertions off the frozen v1 prefix `[store].aad_bind` now defaults True (#38), so the default at-rest format is mfenc:v2. 41 test assertions anchored on `PREFIX` ("mfenc:v1:") whose actual claim is "this value is enciphered at rest at all" — a v1-only spelling that a v2 value silently fails. They move to the version-agnostic `MARKER_PREFIX` ("mfenc:"), which is the same anchor the store's own find-all/migration `mfenc:%` LIKE patterns and `is_encrypted()` already use. Evidence, not assertion. Under the ASVS correctness net `MEFOR_TEST_FORCE_AAD_BIND=1` (conftest forces every AesGcmCipher to the v2 writer), the swept files went 9 failed -> 1 failed. The one remaining failure is `test_store_aad_binding::test_v1_rows_still_read_under_aad_bind`, a site this commit deliberately does NOT touch: it fails on both branches because forcing v2 destroys its v1 premise, which is exactly what makes it the liveness receipt that the flag is live. DELIBERATELY LEFT as v1 (CRYPTO-1 / ADR 0019 coverage a sweep would DELETE), each now carrying a comment saying so — their absence is what made this dangerous to review: - the frozen-writer set (test_store_encryption: the _v1_blob oracle, the byte-identity gate, test_default_writer_is_v1_not_v2) - premise assertions whose point is "the row really IS v1" so a dual-read/no-rotation proof cannot go vacuous (test_v2_active_decrypts_v1_without_rotation, test_store_aad_binding, test_keyprovider x2, test_keyprovider_vault) - field-POSITION pins, where "mfenc:<fp>:" is a string no writer emits, so widening makes the positive unsatisfiable and the negative unfailable (test_key_id_is_a_fingerprint_not_zero, the legacy key_id='0' fixture) - undecryptable-payload injections (test_batch_claim_fifo, test_claim_fifo_heads): the marker version must be one the cipher DISPATCHES on, or the poison takes the unknown-version branch instead of the base64 failure the test is about Three checks got strictly stronger rather than merely wider: - the rotation assertion now anchors on `cipher.active_marker_prefix`, keeping the "re-encrypted under the ACTIVE key" half of the proof that a bare MARKER_PREFIX would have dropped - three NEGATIVE leak/plaintext checks (`not startswith` / `not in`) now exclude EVERY marker version; as v1-only they would have passed silently on a leaked or already-encrypted v2 value - test_ack_sent_store's manual decrypt now passes the cell AAD the store wrote with. Pre-existing and previously MASKED by the v1 assertion failing first; unmasked once the sweep let the test reach it. Harmless on v1 (that reader ignores the caller's aad by design). Comments naming "v1"/"the v1 marker" at swept sites were updated so they no longer lie. Verification (SQLite leg, from the worktree): 9135 passed, 816 skipped, 0 failed; ruff check + ruff format --check clean. Non-vacuity proven by injecting one regression (AesGcmCipher.encrypt returns plaintext): two swept assertions FAIL on a plaintext at-rest value and pass without it. * ci(supply-chain): pin the release-path toolchain + guard the pins against silent rot Five installs on the release path resolved whatever PyPI served at tag time (Scorecard PinnedDependenciesID; ADR 0034 §3). The `release` job holds contents/id-token/attestations: write and `release-harness` holds contents/id-token: write, so these ran with the OIDC identity that signs and publishes. The sharp one is `sigstore`: its step carries no `if:`, so it runs on every tag AND every dispatch, and the very next command signs the wheel, sdist, SBOM and VEX with that identity. release.yml sigstore -> ==4.4.0 release.yml pip + build (x2: engine and harness jobs) -> ==26.1.2 / ==1.5.0 release.yml cyclonedx-bom -> ~=7.3.1 release.yml packaging (x2) -> ==$PKG_PIN, derived from constraints.lock security.yml pip + cyclonedx-bom -> ==26.1.2 / ~=7.3.1 (keeps it the true SBOM twin) Three judgement calls, none of them the obvious choice: 1. sigstore 4.4.0, NOT the newer 4.5.0. .github/dependabot.yml sets a 5-day supply-chain cooldown whose stated purpose is dodging a package compromised shortly after publish; 4.5.0 is <48h old. Hard-pinning the SIGNING toolchain to an artifact fresher than the repo's own routine-update policy allows would invert that policy at the highest-privilege point in the pipeline. 4.4.0 has aged 23 days. Re-evaluate once 4.5.0 clears the window. 2. `packaging` is DERIVED from constraints.lock, not hardcoded. It IS a DEP-1 dependency (requirements.lock + constraints.lock both pin it at 26.2), so a literal would drift on the next Dependabot bump. Same run-time-read pattern as quality-advisory.yml's ruff pin, but FAIL-CLOSED instead of falling back to an unpinned fetch — a soft fallback on the release path defeats the pin exactly where it matters. Both installs also move ABOVE their `GITHUB_REF_TYPE = tag` guard so a workflow_dispatch dry-run exercises them; they were the only two of the five a dispatch could not reach, and an install that never runs before the tag cannot be validated before it. 3. ~=7.3.1 rather than ~=7.3 for cyclonedx-bom: the looser form floats the whole 7.x minor range, and a 7.4 could change the JSON shape scripts/security/sbom_finalize.py parses — which exits non-zero and FAILS the release. ~=7.3.1 still takes patch fixes. This is residual-risk reduction, NOT an alert closure: ADR 0034 §3 shows from this repo's own data that exactly-pinned tools (bandit==1.9.4, zizmor==1.5.2) are still flagged while --require-hashes installs are not. Pinning `sigstore` also pins the TOP only — its ~30 transitive deps still float at signing time. Closing it needs the hashed release-tools lock (ADR 0034 option B), which is an owner call and is deliberately NOT built here. GUARD: a pin nothing watches rots back to unpinned, and Dependabot has no updater for an inline `pip install X==Y` in a workflow (its `uv` ecosystem reads only pyproject.toml + uv.lock), so both a stale pin and a DELETED pin are invisible. tests/test_ci_venv_pinning.py gains two tests: a BLANKET scan of every pip-install target in release.yml (so a NEW unpinned install added tomorrow fails too — the case a fixed name list cannot see), plus a non-vacuity table asserting each tool is still installed and still pinned at EVERY occurrence. Guard proven by injecting one regression at a time rather than trusting a green run: un-pin sigstore -> 2 failed (blanket scan + the named backstop) DELETE the install -> 1 failed (only the backstop can see this, as designed) un-pin 1 of 2 `build` -> 3 failed (proves every occurrence is checked, not just the first) restored -> 12 passed Both workflows re-parsed as YAML. 192 passed across every test that reads .github/workflows. Line-neutral where ADR 0034 requires it: the scanner re-fires a dismissed alert under a NEW number when a line moves, so each pin was made in place. Not done: security.yml's `pip uv` (line 55) and `pip-audit` (81) stay unpinned — the DEP-1 job is contents: read and outside the release path. * test(crypto): finish the cell-AAD sweep and correct the rationale the sweep recorded Follow-up to 09767bf. The sweep widened the "encrypted at rest" assertions correctly, but review found five things it got wrong or left half-done. 1. Five more bare `decrypt()` calls on cell-bound columns (the defect the sweep fixed once, in test_ack_sent_store). Each fails closed on an mfenc:v2 value and was only invisible because the v1 prefix assertion above it failed first: - test_store_encryption x3, queue.last_error (store.py binds cell_aad("queue","last_error", <row id>)) - test_sqlserver_store x1, response.body (sqlserver.py:4326) - test_postgres_store x1, response.body (postgres.py:3668) The SS/PG pair skip without a live DB, so they surface only in CI. Both now read destination_name back from the row rather than rebuilding the "\x1fack:" sentinel. 2. Seven new comments asserted a causal link that does not exist: "the at-rest format follows [store].aad_bind (v2 by default), so a v1-only prefix would fail here". aad_bind is read in exactly one place -- base.py:1736, make_cipher(..., write_v2=settings.aad_bind), reachable only via build_cipher/open_store. Every one of these tests builds its own cipher, and make_cipher's write_v2 parameter still defaults False, so they observe mfenc:v1: in a normal run and the setting has no influence on them. Reworded to the true reason: the marker version is the cipher's business, not the assertion's. 3. Two poison-injection comments named the wrong failure mechanism. Measured: 'mfenc:v1:not-base64-$$$' -> ValueError: Nonce must be between 8 and 128 bytes 'mfenc:not-base64-$$$' -> CipherError: unknown at-rest marker version `_parse` splits on the second colon, which is absent, so the blob is empty and b64decode("") succeeds -- the raise comes out of AESGCM, not the base64 path. The decision to keep these two at v1 was right; only the stated mechanism was wrong. 4. test_store_encryption's M9 section claimed "WRITES v1 BY DEFAULT" / "the shipping default never emits a v2 marker" in the same file where the sweep added six "v2 by default" comments. ADR 0148 falsified the prose, not the tests: they pin make_cipher's PARAMETER default (still False), while the shipped store writes v2 via build_cipher. Both defaults now named separately. 5. test_bytes_per_message_amplification documented the at-rest shape as mfenc:v1:<key_id>:<b64>. Receipt (real numbers, run from this worktree). Default mode over the 11 edited modules: 166 passed, 351 skipped. Forced-v2 leg (MEFOR_TEST_FORCE_AAD_BIND=1) over all 60 test modules that import store.crypto -- the scanned set, not a 12-file window: 10 failed, 1159 passed, 359 skipped, down from 13 failed. The 3 that stopped failing are exactly the last_error sites in (1). All 10 remaining are deliberate-v1 sites that MUST red under a forced v2 writer: the four CRYPTO-1 frozen-writer pins, test_active_marker_prefix_v1_and_v2, test_key_id_is_a_fingerprint_not_zero, and the four v1-premise dual-read tests. No messagefoundry/ change. ruff check + ruff format --check clean over tests/. * ci(supply-chain): make the pin guard actually reject a non-pin, and finish the ADR 0034 rows Follow-up to 9bf7178. The pins were right; the guard protecting them was not, and two of the four ADR 0034 rows were only half-applied. THE GUARD ACCEPTED `>=` AS A PIN. `_VERSION_OPS` listed ("==", "~=", ">=", "<=", "!="), so `pip install "sigstore>=4.4.0"` — which resolves whatever PyPI serves at tag time, byte-for-byte the exposure ADR 0034:190 calls "the highest residual in the group … runs with the OIDC identity used to publish" — passed green under a test named test_release_toolchain_pin_is_present, and its own failure text never fired. Split into _PIN_OPS ("==", "~=") for the pin decision and _SPEC_OPS (every PEP 440 operator) for matching a token to a package NAME, so an unpinned `sigstore>=…` is reported as unpinned rather than as a missing step. Three more holes in the same scan: - `"/" not in target` exempted every URL and git+ target — the one class that cannot carry a pin at all. `pip install https://…/evil.whl` inserted before the Sigstore step scored ZERO failures. Remote is now tested FIRST and always reported; only local path shapes are exempt. - `line.split(" install ", 1)[1]` disagreed with the `\bpip\s+install\b` regex that selected the line, so `pip\tinstall` raised IndexError instead of asserting. Both now anchor on one regex, which also picks up `pip3 install` and `pip --quiet install`. - The vacuity floor said >= 6 while the file has 8 pip installs — two steps could be deleted before the scan noticed. Floor is now the real count. ADR 0034 ROWS FINISHED: - The harness `packaging` install got the pin but not the venv. ADR 0034:192 prescribes both, and the venv is the half carrying the risk: it landed in release-harness's MAIN interpreter, and the steps after it attach the wheel to the release and publish to PyPI. Now /tmp/harnesssmoke, mirroring the engine job's /tmp/relsmoke. - The two SBOM installs had drifted apart, which quietly retired ADR 0034:205's pre-tag validation route ("run security.yml's sbom job … the install command there is byte-identical"). Nothing in PR CI executes release.yml, so that dispatch was its only dry-run proxy. Both lines are now the same command and a test keeps them that way. The register itself said "Recommended hardening — identified, NOT done" over four rows this branch had already done, and called them "an owner decision, not a drive-by". Rewritten with per-row status and the reason the owner gate is retired for them (they are PR-visible now). What is NOT closed is stated plainly: these pin only the top of each install, sigstore's ~30 transitive deps still float, and §3's own data (bandit==1.9.4 pinned and still flagged) says no pin moves the Scorecard finding — option B remains the only thing that does, and remains an owner call. New guards: security.yml's unpinned installs are a REGISTERED set (pip/uv/pip-audit — contents:read, schedule-only, nothing anyone installs) so a new one there still reds; the SBOM twin-identity check; and a PR-time canary that constraints.lock still carries exactly one `packaging==` line, because release.yml derives that pin at run time and `exit 1`s without it — on the tag push. RECEIPTS. Guard mutation-tested against a TEMP COPY of the workflows (repo never modified), one regression at a time — control 0 failures, no crashes: sigstore == -> >= 2 blanket_scan + named_backstop build == -> >= (1 of the 2 installs) 2 blanket_scan + named_backstop[build] sigstore install deleted 2 blanket_scan + named_backstop new unpinned named install 1 blanket_scan new https://…whl install 1 blanket_scan (scored 0 before this commit) new git+ install 1 blanket_scan packaging pin -> bare 2 blanket_scan + named_backstop[packaging] TAB / pip3 / flag-before-subcommand 2 each (was an IndexError crash) SBOM twin drift 1 sbom_twin security.yml gains an unpinned target 1 security_registry --upgrade-deps / lost --require-hashes 2 / 1 constraints.lock loses packaging== 1 packaging_canary Both workflows re-parse as YAML; permissions, triggers and job lists are byte-identical to origin/main (checked, not assumed). 195 passed / 10 skipped across all 14 test modules that read .github/. No messagefoundry/ change. * ci(release): pin the console job's toolchain, which the new guard caught The console publish job landed while this branch was open and installs pip, build and packaging unpinned. The guard added earlier on this branch rejects exactly that, so updating the branch against main turned all three test legs red -- the guard catching a real regression written after it was, which is the best evidence it works that this branch could have produced. pip and build take the same explicit pins the sibling build jobs use. packaging is derived from constraints.lock the way relsmoke and harnesssmoke already do, so one lock bump moves every release-path packaging install together instead of letting them drift apart. Also adds the check the pin guard structurally cannot make. Getting here, an edit put a literal 0x01 byte in release.yml -- a sed backreference written into a non-raw Python string -- and the file became unparseable YAML that GitHub Actions could never have run. The pin guard passed green through it, because it greps lines and never parses the document. So the file is now verified by parsing it, and its permissions blocks and triggers are compared against origin/main as parsed structures rather than eyeballed: jobs release, release-webconsole, release-harness; permissions identical; triggers identical. Verification: pin guard 15 passed; full suite 9331 passed, 818 skipped, 1 failed. That failure is tests/test_gate_installed_parity.py, which is PRE-EXISTING and unrelated -- it reproduces identically on clean main, this branch touches neither the gate source nor that test, and the test is LOCAL-MACHINE only (it skips on CI, where no installed gate exists). It reports that this workstation's installed ~/.claude/hooks copy is older than the source that PR #36 merged; re-running install-gate.ps1 is the fix, and that is deliberately an owner action because it activates the EnterWorktree rule for every session on the box.
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.
Ports the session-rescue work off the dormant
salvage/worktree-session-rescuebranch, where it had sat unmerged since 2026-07-24 while the failure it fixes stayed live.sessions.ps1indexes sessions across every login on the box and rehomes a stranded one back to the primary's slug. Read-only by default — a bare run only lists — and-Rehomeis the sole destructive path, refusing a session that still looks live and honouring-WhatIf.The gate rule ships INERT, deliberately
The live hook is a copy at
~/.claude/hooks/worktree_gate.ps1;install-gate.ps1is what overwrites it. Merging this changes nothing until that script is re-run.That split is the point. Rule 2 already denies a fan-out from the primary, and this rule denies relocating into a worktree — so with both live, a primary-resident session has no in-session path to a subagent at all and must be started in a worktree. That may well be right, but it is a workflow change deserving its own decision, and the rescue tool should not wait behind it.
Worth weighing when you do decide: the rule was designed when a stranded session was unrecoverable. This same PR ships the cure, so the benefit shrinks to preventing a recoverable annoyance while the cost stays fixed.
docs/WORKTREES.mdrecords both sides for whoever runsinstall-gate.ps1next.Notes on the port
main's docs never documented the existing rule 3b, so that list was already drifting from the code.)git cherry-pickis refused by the worktree gate. Four files whose base was byte-identical tomainwere taken wholesale; the two that diverged were applied by unique anchor.install-gate.ps1reddenstest_every_tool_the_gate_handles_is_registered_by_the_installer; restoring it goes green. The source commit notes a rule once shipped dead, so the guard seemed worth proving.Gate tests: 72 passed. Full suite: 9040 passed, 816 skipped, 1 failed — that failure is the pre-existing
test_anon_paritybug, fixed independently in #35.