Skip to content

feat(worktree): session-rescue tool (sessions.ps1) + an inert EnterWorktree rule - #36

Merged
wshallwshall merged 2 commits into
mainfrom
wtree
Jul 29, 2026
Merged

feat(worktree): session-rescue tool (sessions.ps1) + an inert EnterWorktree rule#36
wshallwshall merged 2 commits into
mainfrom
wtree

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Ports the session-rescue work off the dormant salvage/worktree-session-rescue 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. 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 ships INERT, deliberately

The live hook is a copy at ~/.claude/hooks/worktree_gate.ps1; install-gate.ps1 is 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.md records both sides for whoever runs install-gate.ps1 next.

Notes on the port

  • Numbered rule 3 here, not 5 as on the source branch — this list has two entries, not four. (Separately: main's docs never documented the existing rule 3b, so that list was already drifting from the code.)
  • Reconstructed, not cherry-pickedgit cherry-pick is refused by the worktree gate. Four files whose base was byte-identical to main were taken wholesale; the two that diverged were applied by unique anchor.
  • The tripwire was exercised, not trusted: pulling the matcher out of install-gate.ps1 reddens test_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.
  • The leak gate caught an inherited defect in the ported tool — a real account name in an explanatory comment. Fixed to a neutral example.

Gate tests: 72 passed. Full suite: 9040 passed, 816 skipped, 1 failed — that failure is the pre-existing test_anon_parity bug, fixed independently in #35.

…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
wshallwshall enabled auto-merge (squash) July 29, 2026 03:01
@wshallwshall
wshallwshall merged commit 1de93b4 into main Jul 29, 2026
32 checks passed
@wshallwshall
wshallwshall deleted the wtree branch July 29, 2026 03:17
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant