🧭 git-validator: two notions of "where am I"

Three defects in one hook, all downstream of a single mismatch. Found by being denied on a branch that was not main.
worktree session judged as main #3411 path 3: dead in worktrees 7557 hook tests 2 of 4 new tests fail on old code

The mismatch

ctx.branch      = getCachedBranch(getProjectDir())   lib/context.ts:23
                                   └─ reads the ENV = PRIMARY checkout

denial slug     = repoSlugFromCwd(input.project_dir)  git-validator.ts
state read slug = repoSlugFromCwd(input.project_dir)
state WRITE slug= repoSlugFromCwd(getProjectDir())   heartbeat-publisher:139

CC sends the WORKTREE as project_dir. So the hook pairs a worktree
DIRECTORY with the TRUNK's branch, and reads state nobody wrote there.

Probe, not argument

Two payloads differing only in project_dir, driven through the built alpha.24 bundle:

DENY   branch_in_message=main   project_dir = PRIMARY  (on main)
DENY   branch_in_message=main   project_dir = WORKTREE (on fix/3450-precompact-guard-auto)
                                             ^ nothing was on main

Byte-identical denials for two different directories is what isolated it. The worktree-awareness of #2363 and the shell_cwd path of #3411 both sit downstream of this, so both were compensating for a branch read from the wrong place.

The dead path, visible on disk

Writer and reader disagreed on the state key, and repoSlugFromCwd is only basename(). Both files existed for the same session:

~/.claude/state/orchestkit/orchestkit/<sid>.json         written, holds shell_cwd
~/.claude/state/orchestkit/precompact-guard/<sid>.json   opened by the reader

So shell_cwd was always null for a worktree session: the #3411 mechanism was inert in precisely the scenario it was written for.

The message that described a mechanism that no longer exists

"This guard reads the COMMAND STRING, not the shell's working directory, so a cd you ran earlier is invisible to it."

True before #3411, false after. Worse, it collapsed two situations an operator cannot otherwise tell apart. It now branches, and in the lost-track case names the cause: shell_cwd is cleared rather than guessed by any cd it cannot read statically (lib/shell-cwd.ts:75,91).

The sting: the repo's own worktree helper prints WT=$(...) && cd "$WT". Following the documented workflow is exactly what clears the record, and nothing said so.

Why it stayed green

git-validator-worktree.test.ts wrote its fixtures with a local writeShellCwdState() keyed by the test's own project dir. It validated the reader against state keyed the way the reader happened to read it, never the way the writer writes it. That is the same half-contract the #3411 changelog entry criticised in this very file, one layer down. The suite now mocks getProjectDir so writer and reader agree on one directory.

Verification