Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 88 additions & 4 deletions .claude/hooks/guard-governed-enqueue.selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,96 @@ expect allow 'a PR reporting no changed files is not a governed answer' \
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_EMPTY"

echo "== a generated-exception row on a repo with no checkout to recompute against =="
# objectstack-ai/cloud has no sibling checkout here, so the register cannot
# recompute the row's provenance on the RIGHT tree. Judging one repo's paths
# against another's files would be worse than not answering: fail open, say so.
# With no checkout of the target repo the register cannot recompute the row's
# provenance on the RIGHT tree, and judging one repo's paths against another's
# files would be worse than not answering: fail open, say so.
#
# ⚠️ THIS CASE'S PREMISE IS INJECTED, AND THE INJECTION IS THE REPAIR. It used to
# rest on a fact about the BOX — "objectstack-ai/cloud has no sibling checkout
# here" — which is a property of the container, not of the hook, and not true
# everywhere: on a box that does carry a sibling `cloud` checkout the guard
# resolved it, recomputed the predicate on it and BLOCKED, so this matrix read
# `54 passed, 1 failed` there and was green in CI only because the runner
# mounts no sibling. A case whose verdict depends on what else happens to sit
# next to the checkout is not hermetic, whatever the step comment says. It now
# points the lookup at a directory it created itself and therefore knows is
# empty, so the "cannot resolve" premise is one this file OWNS on every box.
# ONE spelling of the path, read by the fixture the hook is handed AND by the
# register leg below. Two spellings drift: change the fixture alone and the
# agreement leg goes on asking about the old path, which is agreement with a
# question nobody asked.
CROSS_REPO_PATH=skills/objectstack-data/references/_index.md
F_CROSS_REGEN="$(fixture cross-repo-regen "$(files_of "$CROSS_REPO_PATH")" "$NO_REVIEWS")"
NO_SIBLING_ROOT="$root/no-sibling-here" # under $root: the existing trap removes it
mkdir -p "$NO_SIBLING_ROOT"
#
# ⚠️ AND IT DOES NOT REACH THE "no checkout … is available" FAIL-OPEN — measured,
# because the comment that used to sit here said it did. With nothing resolved
# the register is asked WITHOUT `--root`, so it answers about THIS tree, where
# this path is byte-exact against its own generator and therefore LIFTED: the
# hook leaves at the cleared-predicate `exit 0` with EMPTY stderr, several
# branches above that fail-open. Pinning a warning here would pin a sentence
# nothing prints. What this case does hold is the property the card is about —
# the verdict must not depend on what else is mounted beside the checkout — and
# the resolved-sibling case below is its other half: same fixture, same payload,
# only the injected root differs.
expect allow 'an exception-row path in a repo this container cannot resolve' \
"$(mcp $AUTO 999 objectstack-ai cloud)" \
"OS_GOVERNED_ENQUEUE_FIXTURE=$(fixture cross-repo-regen "$(files_of skills/objectstack-data/references/_index.md)" "$NO_REVIEWS")"
"OS_GOVERNED_ENQUEUE_FIXTURE=$F_CROSS_REGEN" "OS_GOVERNED_ENQUEUE_SIBLING_ROOT=$NO_SIBLING_ROOT"

echo "== ...and a sibling checkout that DOES resolve is audited on its own tree =="
# The other half of that same branch, and the reason the case above needs an
# injection rather than a rename: when a checkout of the target repo IS
# reachable, resolving it and recomputing the predicate there is the DESIGNED
# behaviour — the answer then reflects the DIFF instead of the environment,
# which is the whole point of passing `--root`. Pin only the fail-open and the
# guard stays green after it stops looking for siblings at all.
#
# The sibling is BUILT here rather than borrowed from the box: `git init` plus an
# `origin` naming objectstack-ai/cloud is the entire admission requirement, since
# the hook compares origin slugs and reads nothing else. Measured on a container
# carrying the real read-only /home/user/cloud checkout: this throwaway and that
# checkout hand the register the SAME verdict with the SAME reason — governed,
# `pureRegeneration: false`, "the generator declared no output set … fail closed"
# — because the `gen:skill-refs` toolchain cannot run on either tree. The
# throwaway reproduces the real sibling, so no further tree is needed.
#
# ⛔ The origin URL is the BARE form on purpose — do not "tidy" a `.git` suffix
# onto it. The hook's slug reader keeps that suffix (its path character class
# owns the dot and swallows it, leaving `cloud.git`), so the `.git` spelling
# resolves NOTHING and this case would silently become a second copy of the one
# above. Measured here; filed separately as its own defect, since the same
# reader also derives the slug for a bare `gh pr merge <n>`.
#
# ⭐ Asserted as AGREEMENT with the register, for the reason the pure-regeneration
# case above learned the hard way: `skills/**` leaving the governed fence, or this
# exception row being retired, would flip the verdict for a reason the hook had
# nothing to do with, and a verdict copied from the register makes this matrix a
# second register. Here and in CI today that branch is `block`.
SIBLING_ROOT="$root/sibling-parent" # under $root: the existing trap removes it
mkdir -p "$SIBLING_ROOT/cloud"
git -C "$SIBLING_ROOT/cloud" init -q >/dev/null 2>&1
git -C "$SIBLING_ROOT/cloud" remote add origin https://github.com/objectstack-ai/cloud >/dev/null 2>&1
node "$repo_root/scripts/pm/check-governed-merges.mjs" --test --root "$SIBLING_ROOT/cloud" \
"$CROSS_REPO_PATH" >/dev/null 2>&1
sibling_rc=$?
if [ "$sibling_rc" -eq 0 ]; then
sibling_want=allow
sibling_branch='LIFTED on the sibling tree — the hook must answer the same way'
else
sibling_want=block
sibling_branch="GOVERNED on the sibling tree (exit $sibling_rc, fail-closed: the generator cannot run there) — the refusal must stand"
fi
printf ' .. register verdict on the RESOLVED sibling: %s\n' "$sibling_branch"
expect "$sibling_want" 'a sibling checkout that resolves is audited, never waved through' \
"$(mcp $AUTO 999 objectstack-ai cloud)" \
"OS_GOVERNED_ENQUEUE_FIXTURE=$F_CROSS_REGEN" "OS_GOVERNED_ENQUEUE_SIBLING_ROOT=$SIBLING_ROOT"
# ⛔ No companion assertion that the fail-open text is ABSENT here — it was
# written, and measured to be a phantom: when the sibling does NOT resolve the
# hook does not print that warning either (it leaves at the cleared-predicate
# exit with empty stderr, per the case above), so the assertion passed in both
# worlds and discriminated nothing. The verdict row above is the discriminator,
# and it is the one that goes red when the sibling stops being resolved.

echo "== the deliberate exception switch =="
expect allow 'OS_ALLOW_GOVERNED_ENQUEUE=1 on the blocking case' \
Expand Down
41 changes: 39 additions & 2 deletions .claude/hooks/guard-governed-enqueue.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,34 @@
# change where DATA comes from and nothing else; the open escape hatch above is
# the way to actually skip the guard.
#
# OS_GOVERNED_ENQUEUE_SIBLING_ROOT=<dir> is the third, and it moves WHERE a
# sibling checkout is looked for — never WHAT is accepted as one. The admission
# rule stays the origin-slug comparison further down: a directory becomes the
# tree this guard audits only when its own `origin` declares the target
# `owner/repo`, so a value pointed anywhere wrong resolves NOTHING. It cannot
# widen the audit, and it cannot soften one either — a sibling that does resolve
# is judged exactly as it is today.
#
# unset → `$(dirname "$repo_root")`, the parent of this checkout. Today's
# behaviour, to the byte.
# empty → the same as unset. An empty value is an accident (`export VAR=`,
# or `VAR="$SOMETHING_UNSET"`), and the safe reading of an accident
# is "no override" — never "look nowhere", which would silently
# drop a real audit.
# a directory carrying no matching checkout, one that does not exist included
# → nothing resolves, and the run takes the existing "no checkout of
# the target repo is available" fail-open below, with its existing
# warning. That is the branch a box WITHOUT the sibling has always
# taken; this variable opens no new way out.
#
# Why it exists: the self-test's cross-repo case asserts that fail-open, and its
# premise used to be a fact about the BOX ("objectstack-ai/cloud has no sibling
# checkout here") rather than about the hook. On a container that does carry a
# sibling `cloud` checkout the guard resolved it, recomputed the predicate on it
# and blocked — 54 passed / 1 failed, green in CI only because CI carries no
# sibling. The matrix now sets this variable and owns its own premise, and the
# resolved-sibling BLOCK is pinned beside it as the deliberate behaviour it is.
#
# Self-test (no network, no build): .claude/hooks/guard-governed-enqueue.selftest.sh

set -uo pipefail
Expand Down Expand Up @@ -447,11 +475,20 @@ done < "$work/files.txt"

slug_of() { git -C "$1" remote get-url origin 2>/dev/null | sed -n 's#.*github\.com[:/]\([A-Za-z0-9._-]*/[A-Za-z0-9._-]*\)\(\.git\)\{0,1\}/*$#\1#p'; }

# WHERE a sibling is looked for is injectable (OS_GOVERNED_ENQUEUE_SIBLING_ROOT,
# header); WHAT is accepted as one is not. The slug comparison below is the whole
# admission rule and is untouched by it, so the variable can only move the
# search — a sibling that resolves is audited exactly as before, and a root
# holding no matching checkout resolves nothing and falls through to the
# "no checkout available" fail-open, the branch a box without the sibling
# already takes. Unset or empty ⇒ the parent of this checkout, as always.
sibling_root="${OS_GOVERNED_ENQUEUE_SIBLING_ROOT:-$(dirname "$repo_root")}"

target_root=""
if [ "$(slug_of "$repo_root")" = "$owner/$repo" ]; then
target_root="$repo_root"
elif [ "$(slug_of "$(dirname "$repo_root")/$repo")" = "$owner/$repo" ]; then
target_root="$(dirname "$repo_root")/$repo"
elif [ "$(slug_of "$sibling_root/$repo")" = "$owner/$repo" ]; then
target_root="$sibling_root/$repo"
fi

test_args=(--test --json)
Expand Down
Loading