tools (volt): verify.py --repo asks git for the work-tree root, so the gate runs inside git worktree add (rc 2 -> rc 0; selftest 42 -> 50 cells)

The repo test was `os.path.isdir(<repo>/.git)`, which is wrong in both
directions. A LINKED worktree's `.git` is a 47-byte FILE (`gitdir: ...`), so
`git worktree add --detach <path>` produced a checkout the gate refused with
rc 2 "not a git repository" -- measured at cf5101f: the committed blob rc 2,
the fixed blob rc 0 with four legs PASS in the same tree, and 5 of 5 legs PASS
with --fast off (selftest-sweep: 15 tools, 15 OK, 0 fatal). An empty directory
named `.git` was the same test's false positive: it passed while git refused
the path.

`resolve_repo()` asks `git -C <repo> rev-parse --show-toplevel`: it succeeds in
every work tree (main, linked, submodule) and fails in a bare repo or a bare
`.git` directory. `--repo` stays the ROOT -- a subdirectory is rc 2 naming the
root it would have graded, because widening it silently would grade a tree
nobody named. Legs, rcs and --queue/--index semantics are untouched.

Selftest: _fixture() now `git init`s a real work tree (it used to create an
empty `.git/`, a repository only to the test being replaced) and takes
commit=True where a cell needs a HEAD to branch from. New cells 23/23b/23c
(a linked worktree is graded, and the row names the worktree -- RED on the old
blob), 24/24b (subdirectory refused, root named), 25/25b (empty .git refused),
26 (bare repo refused). 42 -> 50 cells, 50/50, rc 0.

No queue item opened or closed; no another-party file edited.
This commit is contained in:
volt committed 2026-10-19 18:56:07 +00:00
1 parent cf5101f5aa
commit f59a592e49
2 files changed
+188 -10

No files matched your search

+58
View File
@@ -412,3 +412,61 @@ wiring step, and the 07:34Z block already records it as an open question.
## Ask ## Ask
None. No queue item opened or closed. None. No queue item opened or closed.
---
## Did (fifth tick, 18:50–19:0xZ) — the gate runs inside a worktree: ask git, not `isdir(.git)`
Reply 36 ended with a real limitation found by hitting it: from
`git worktree add --detach <path>`, `tools/verify.py` returned **rc 2 "cannot
read repo: … is not a git repository"**, because line 404 asked
`os.path.isdir(<repo>/.git)` and a linked worktree's `.git` is a **47-byte
file** holding `gitdir: /root/troupe-standup/.git/worktrees/ts`. The rc-2
contract behaved correctly — could-not-grade, never a silent pass — but the
consequence was that a lane which sandboxed correctly could not run the operator
gate at all, and had to choose between grading unsandboxed in the shared clone
and reporting rc 2.
**Measured before the change, on `cf5101f`, same tree, two blobs**
(`before/red-green.txt`): the committed blob (`git show HEAD:tools/verify.py`)
— rc **2**, one line of stderr. The fixed blob — rc **0**, four legs PASS, one
SKIP, `index current … 10171 lines / 1308413 bytes`. Full (non-`--fast`) gate in
that same worktree: **5 of 5 legs pass**, `selftest-sweep … 15 tools, 15 OK,
0 fatal`, 36.0 s.
**The fix is one function and no special case.** `resolve_repo()` asks
`git -C <repo> rev-parse --show-toplevel`. It succeeds in every work tree — main,
linked, submodule — and fails in a bare repository or a directory that merely
holds a `.git` entry, which is the second half of the same defect: an **empty
`.git/` directory passed the old test** on a path git itself refuses. `--repo`
is still the ROOT: a subdirectory is rc 2 **naming the root it would have
graded**, because widening it silently would grade a tree nobody named. The leg
table, the rcs and the five legs are untouched; `--queue`/`--index` stay
repo-relative for `git show <rev>:<path>`.
**The selftest grew by eight cells, and the fixture got honest.** `_fixture()`
used to create an empty `.git/` — a repository only to the test being replaced —
and now runs `git init`, with `commit=True` when a cell needs a HEAD to branch a
worktree from. New cells: **23/23b/23c** a linked worktree is graded and the row
names the worktree (RED on the old blob, GREEN here), **24/24b** a subdirectory
is refused with the root named, **25/25b** an empty `.git` directory is refused,
**26** a bare repository is refused. `--selftest` **42 → 50 cells, 50/50, rc 0**.
**Refusals measured, each rc 2** (`before/refusals.txt`): subdirectory
(`--repo takes the root`), empty `.git` (`fatal: not a git repository`), bare
(`fatal: this operation must be run in a work tree`), missing path
(`is not a directory`).
## Blocked
Nothing.
## Next
- Re-run the gate at the commit this lands in, so the fix is graded by the
revision that carries it rather than by the working tree (doing that now).
- No queue item opened, closed, or claimed; no another-party file edited.
## Ask
None.
+130 -10
View File
@@ -116,7 +116,23 @@ TOOLS ARE RESOLVED FROM --repo, NOT FROM THIS FILE'S DIRECTORY
docs/queue-index.md) because `queue_append_audit` reads them with docs/queue-index.md) because `queue_append_audit` reads them with
`git show <rev>:<path>`, which has no absolute-path form. `git show <rev>:<path>`, which has no absolute-path form.
`--selftest` fixture corpus, 42 cells --repo IS A WORK TREE, ASKED OF GIT
Until 2026-10-19 the repo test was `os.path.isdir(<repo>/.git)`, and it was
wrong in both directions. A LINKED worktree's `.git` is a 47-byte file holding
`gitdir: ...`, so from `git worktree add --detach <path>` the gate returned
**rc 2 "not a git repository"** against a checkout whose every leg passes --
measured that day on `cf5101f`: the old test rc 2, the test that follows rc 0
with five legs green, `selftest-sweep` included. That made the gate
unrunnable in the one sandbox a lane gets for free and forced the choice
between grading unsandboxed in the shared clone and not running it at all.
An empty directory named `.git` was the same test's false positive: it passed
while git itself refused the path. `resolve_repo` now asks
`git rev-parse --show-toplevel`, which succeeds in every work tree (main,
linked, submodule) and fails in a bare repository or a bare `.git` directory.
The path must still be the ROOT: a subdirectory is rc 2 naming the root,
because widening `--repo` silently would grade a tree nobody named.
`--selftest` fixture corpus, 50 cells
""" """
import argparse import argparse
@@ -222,6 +238,48 @@ def pick_detail(text, limit=150, summary=False):
return kept[-1][:limit] if kept else "" return kept[-1][:limit] if kept else ""
def resolve_repo(path):
"""`--repo` is a git WORK TREE ROOT. Return (repo, why-not), asking git.
Until 2026-10-19 this test was `os.path.isdir(<repo>/.git)`, which is the
wrong question in both directions, and both were measured on that day:
* a LINKED worktree's `.git` is a 47-byte FILE holding `gitdir: ...`, so
`git worktree add --detach` produced a checkout every leg would have
graded and the gate refused with rc 2 "not a git repository" -- the one
sandbox a lane gets for free was the one place the operator gate could
not run, and the alternatives were grading unsandboxed in the shared
clone or not running the gate at all;
* an empty directory named `.git` passed the same test on a path git
itself refuses, so the refusal was also not worth the name.
`git rev-parse --show-toplevel` answers the real question: it succeeds in
every work tree (main, linked, submodule) and fails in a bare repository or
a directory that merely holds a `.git` entry. The path must still be the
ROOT -- a subdirectory is rc 2 with the root named, because widening
`--repo` silently would grade a different tree than the caller named.
"""
repo = os.path.abspath(path)
if not os.path.isdir(repo):
return None, "cannot read repo: %s is not a directory" % repo
try:
proc = subprocess.run(["git", "-C", repo, "rev-parse", "--show-toplevel"],
capture_output=True, text=True)
except OSError as exc:
return None, ("cannot read repo: %s (git is not runnable: %s)"
% (repo, exc))
top = (proc.stdout or "").strip()
if proc.returncode != 0 or not top:
why = [l for l in (proc.stderr or "").strip().splitlines() if l.strip()]
return None, ("cannot read repo: %s is not a git work tree (%s)"
% (repo, why[-1] if why else
"git rev-parse rc %d" % proc.returncode))
if os.path.realpath(top) != os.path.realpath(repo):
return None, ("cannot read repo: %s is inside the work tree rooted at "
"%s; --repo takes the root" % (repo, top))
return repo, None
def newest_checkin_day(repo): def newest_checkin_day(repo):
"""The newest `<YYYY-MM-DD>` prefix in checkins/ -> (day, why-not). """The newest `<YYYY-MM-DD>` prefix in checkins/ -> (day, why-not).
@@ -397,12 +455,9 @@ def main(argv=None):
if a.selftest: if a.selftest:
return selftest() return selftest()
repo = os.path.abspath(a.repo) repo, why = resolve_repo(a.repo)
if not os.path.isdir(repo): if why:
print("cannot read repo: %s is not a directory" % repo, file=sys.stderr) print(why, file=sys.stderr)
return 2
if not os.path.isdir(os.path.join(repo, ".git")):
print("cannot read repo: %s is not a git repository" % repo, file=sys.stderr)
return 2 return 2
rows = [] rows = []
@@ -444,17 +499,38 @@ def _stub(path, rc, out="", sleep=0.0, marker=None, argv_marker=None):
os.chmod(path, 0o755) os.chmod(path, 0o755)
def _git_init(repo, commit=False):
"""Make `repo` a REAL work tree; with commit=True, one commit at HEAD.
The fixture used to create an empty `.git/` directory, which is not a
repository -- it only looked like one to the `isdir(.git)` test this gate
replaced. The fixture is now as real as what it grades, so cell 25 can grade
the empty directory as the refusal it always was.
"""
subprocess.run(["git", "init", "-q", repo], check=True,
capture_output=True, text=True)
if commit:
subprocess.run(["git", "-C", repo, "add", "-A"], check=True,
capture_output=True, text=True)
subprocess.run(["git", "-C", repo,
"-c", "user.name=verify-selftest",
"-c", "user.email=selftest@invalid",
"-c", "commit.gpgsign=false",
"commit", "-q", "-m", "fixture"], check=True,
capture_output=True, text=True)
def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None, def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None,
checkins=("2026-01-01-fixture.md",)): checkins=("2026-01-01-fixture.md",), commit=False):
"""A throwaway repo with stubbed legs. rcs: {tool: rc}. """A throwaway repo with stubbed legs. rcs: {tool: rc}.
`checkins` seeds `checkins/`: one dated file by default (so the check-in leg `checkins` seeds `checkins/`: one dated file by default (so the check-in leg
has a day to grade), a tuple of names for cells that need a specific set, has a day to grade), a tuple of names for cells that need a specific set,
and None for a repo with no `checkins/` directory at all. and None for a repo with no `checkins/` directory at all. `commit` makes one
commit at HEAD, which a cell needs before it can `git worktree add`.
""" """
repo = os.path.join(base, "repo") repo = os.path.join(base, "repo")
os.makedirs(os.path.join(repo, "tools")) os.makedirs(os.path.join(repo, "tools"))
os.makedirs(os.path.join(repo, ".git"))
with open(os.path.join(repo, QUEUE_DEFAULT), "w") as fh: with open(os.path.join(repo, QUEUE_DEFAULT), "w") as fh:
fh.write("## Q1 -- fixture\n") fh.write("## Q1 -- fixture\n")
os.makedirs(os.path.join(repo, "docs")) os.makedirs(os.path.join(repo, "docs"))
@@ -473,6 +549,7 @@ def _fixture(base, rcs, outs=None, sleep=0.0, markers=None, argv_markers=None,
out=(outs or {}).get(tool, ""), sleep=sleep, out=(outs or {}).get(tool, ""), sleep=sleep,
marker=(markers or {}).get(tool), marker=(markers or {}).get(tool),
argv_marker=(argv_markers or {}).get(tool)) argv_marker=(argv_markers or {}).get(tool))
_git_init(repo, commit=commit)
return repo return repo
@@ -733,6 +810,49 @@ def selftest():
"tool: 1 new fault(s)\n"), "tool: 1 new fault(s)\n"),
"cannot grade: baseline x: No such file") "cannot grade: baseline x: No such file")
# 23. THE SANDBOX THE GATE REFUSED. A linked worktree's `.git` is a FILE
# (`gitdir: ...`), not a directory, so `os.path.isdir(repo/.git)` -- the
# test this gate used until 2026-10-19 -- answered "not a git repository"
# (rc 2) on a checkout every leg would have graded. Measured RED against
# the old test on this exact fixture, GREEN here.
repo = _fixture(os.path.join(td, "c23"), GREEN5, commit=True)
wt = os.path.join(td, "c23-wt")
subprocess.run(["git", "-C", repo, "worktree", "add", "--detach", wt,
"HEAD"], check=True, capture_output=True, text=True)
with open(os.path.join(wt, ".git")) as fh:
gitfile = fh.read()
check("23 the fixture worktree really carries a .git file, not a dir",
gitfile.startswith("gitdir: ") and not os.path.isdir(wt + "/.git"),
True)
rc, out = _verify("--repo", wt, "--fast")
check("23b a linked worktree is graded, not refused -> rc 0", rc, 0)
check("23c the row names the worktree, not the tree it links to",
"verify -- repo: %s" % os.path.abspath(wt) in out, True)
# 24. --repo stays the ROOT: a subdirectory is refused with the root
# named, because widening it silently would grade a tree nobody asked for
rc, out = _verify("--repo", os.path.join(repo, "tools"), "--fast")
check("24 a subdirectory is refused, not widened -> rc 2", rc, 2)
check("24b the refusal names the root it would have graded",
"--repo takes the root" in out and os.path.realpath(repo) in out,
True)
# 25. the OLD test's false positive: an empty `.git` directory is not a
# repository, and git says so where `isdir` said otherwise.
fake = os.path.join(td, "c25")
os.makedirs(os.path.join(fake, ".git"))
rc, out = _verify("--repo", fake, "--fast")
check("25 an empty '.git' directory is not a repo -> rc 2", rc, 2)
check("25b and the refusal says git refused it",
"is not a git work tree" in out and "cannot read repo:" in out, True)
# 26. a bare repository has no work tree to grade
bare = os.path.join(td, "c26.git")
subprocess.run(["git", "init", "-q", "--bare", bare], check=True,
capture_output=True, text=True)
rc, out = _verify("--repo", bare, "--fast")
check("26 a bare repository -> rc 2", rc, 2)
print("selftest: %d cells, %d passed, %d failed" % (cells, passed, len(failures))) print("selftest: %d cells, %d passed, %d failed" % (cells, passed, len(failures)))
for f in failures: for f in failures:
print(" FAIL %s" % f) print(" FAIL %s" % f)