- BUILD.md: an R5 section covering N1 to N3, the fixes, the tests, the RED and GREEN counts, the 57-mutant table, the 13-shape live probe, the false BUSY and miss checks, and the known limits after R5. N3 corrects the R4 bind-mount note: a same-filesystem bind mount keeps st_dev, so a detector needs mount metadata such as /proc/self/mountinfo. No code changed for N3. Earlier sections are untouched. - docs/TOOLS.md: the busy check's flat form, span rule, exit 2 on text it can't classify, the literal listing, and 153 cases. - BUILD-LOG.md and docs/SESSIONS.md: one appended entry each.
This commit is contained in:
1 parent
afc176de02
commit
794f4c4a24
4 files changed
+419
-6
No files matched your search
@@ -5856,3 +5856,25 @@ sourced() that a GREEN-time case now pins; GREEN 105 passed, 0 failed (20:02:40Z
|
||||
of 30 killed, runner exit 0 (20:02:56Z to 20:06:58Z, base green). No 64-hex test pin names a changed path.
|
||||
Not pushed. Gaps: lexical false BUSY and missed runtime-built sourced paths, and an undetected bind-mounted
|
||||
hooks directory; details in agents/darkwing/work/queue-211/BUILD.md, section "R4".
|
||||
|
||||
## 2026-11-02T23:53:07Z: row 211 (#1709, #426) R5, busy check listing, quoting and spacing
|
||||
|
||||
Before: row 211 R4 (fd1aa8cc8 on `t3/row211-push-guard-r4-fix`, over 09a847eda). The GPT review of R4
|
||||
(row211-r4-gpt.md) found that gone() parsed `ls -f`, whose names follow `QUOTING_STYLE`, so under
|
||||
`shell-always` or `c` a failed read of a listed pid became CLEAR (N1). It also found that seven of 13 live
|
||||
shell-snapshot wrappers that source a test, with no child, gave CLEAR: a quoted path with spaces, a quoted
|
||||
`source`, `;` with no blank, a quoted snapshot, a tab and two spaces (N2). And the R4 bind-mount note named
|
||||
st_dev, which a same-filesystem bind mount keeps (N3).
|
||||
|
||||
After: two R5 commits by the lead's implementation sub-agent, code and tests, then records. gone() lists with
|
||||
`ls -f -1 --quoting-style=literal`, the script exports `LC_ALL=C`, and a failed listing is still never gone.
|
||||
A command line holding quotes, backslashes, shell operators or uneven whitespace gets a flat form (awk). The
|
||||
snapshot rule reads the flat form, the general rules read it and then the raw text, and a wrapper's text goes
|
||||
through an awk word walk. In the walk, every word of each `source` or `.` span counts, and quoted text is read
|
||||
again. Text the walk can't classify (over 32 levels, an awk failure) is exit 2. test-git-guard.sh: RED 119
|
||||
passed, 34 failed (23:32:37Z to 23:33:12Z); GREEN 153 passed, 0 failed (23:33:23Z to 23:33:35Z). Mutants 57
|
||||
of 57 killed, runner exit 0 (23:33:53Z to 23:46:34Z, base green). Live probes of the 13 shapes
|
||||
(23:46:56Z to 23:47:18Z): R4 CLEAR on 7 to 13, R5 BUSY on all 13. No 64-hex test pin names a changed path.
|
||||
Not pushed. Gaps: new lexical false BUSY (span arguments, redirects, `find .`, parentheses), still-missed
|
||||
runtime-built and ANSI-C sourced paths, and an undetected bind-mounted hooks directory; details in
|
||||
agents/darkwing/work/queue-211/BUILD.md, section "R5".
|
||||
@@ -571,3 +571,384 @@ The runner cuts each first failing case at 100 characters, which is why three of
|
||||
output lines. Chain on the exit code, never on the lines (unchanged advice).
|
||||
- The scan sees only what `/proc` lists to it. Under `hidepid=2`, another user's processes aren't listed and
|
||||
can't make it BUSY. This host's `/proc` has no hidepid option.
|
||||
|
||||
## R5, the GPT review of R4 (2026-11-02)
|
||||
|
||||
The lead's implementation sub-agent (Claude Opus 5.5, a child of Sage's T3 c5c1e8ba) wrote this round from
|
||||
the brief `/mnt/storage/scratch/mosaic-lead/briefs/row211-r5-fix.md` and the review
|
||||
`/mnt/storage/scratch/mosaic-lead/reviews/row211-r4-gpt.md` (N1 to N3, the 13 live shapes in Check 2, and
|
||||
Check 6). Base 09a847eda, branch `t3/row211-push-guard-r5-fix` from fd1aa8cc8, then one R5 code commit and
|
||||
one record commit. Only `slot-busy.sh`, the suite, the mutant runner and the record files change; the hook
|
||||
and the installer are untouched. The suite goes from 105 cases to 153 and the mutants from 30 to 57. The
|
||||
sections above stay as written; where R5 changes them, it says so here.
|
||||
|
||||
### N1: a listing that reads the same in any environment
|
||||
|
||||
Was: `gone()` ran `ls -f -- ROOT` and looked for the pid among the names. GNU ls reads `QUOTING_STYLE`. With
|
||||
`shell-always` or `c` it prints a present pid as `'4194430'` or `"4194430"`, the name check misses it, and
|
||||
the pid reads as gone. A failed cmdline read and a failed ancestor read then both ended in CLEAR.
|
||||
|
||||
Fix: `ls -f -1 --quoting-style=literal -- ROOT`. The flag overrides the variable, and `-1` asks for one name
|
||||
per line. R2's rule is unchanged: a failed or denied listing returns 1, never gone. The script also exports
|
||||
`LC_ALL=C` before it runs any tool, for the locale finding below.
|
||||
|
||||
I checked every other tool the script runs or parses, on this host (GNU ls 9.11, GNU Awk 5.4.1, bash
|
||||
5.3.15):
|
||||
|
||||
- ls. `shell-always` and `shell-escape-always` print `'N'`, `c` prints `"N"`, and `locale` prints the
|
||||
locale's quote marks around N. `escape`, `shell`, `shell-escape` and `literal` print a digits-only name
|
||||
bare, so the brief's `escape` control passes on R4 too and stays as a control. `LS_COLORS`, `CLICOLOR`,
|
||||
`CLICOLOR_FORCE`, `COLUMNS`, `TABSIZE` and `POSIXLY_CORRECT` don't change the output to a pipe. To a pipe,
|
||||
`-1` changes nothing either, so no test can pin it; it states the format. BSD and BusyBox ls reject
|
||||
`--quoting-style`, so on such a host every listing fails and every failed read is exit 2.
|
||||
- awk. `AWKPATH`, `GAWK_READ_TIMEOUT=1`, `POSIXLY_CORRECT` and a UTF-8 `LC_ALL` left the status read's output
|
||||
as it was. `GAWK_PERSIST_FILE` naming a missing file makes gawk exit 2 with a fatal error. Every awk call
|
||||
in the script checks its exit status, so that ends in exit 2, never CLEAR.
|
||||
- tr. It maps bytes; `POSIXLY_CORRECT` and a UTF-8 `LC_ALL` gave the same bytes, 0xff included.
|
||||
- readlink only feeds the cwd field of a BUSY line, and it prints the link as it is. `sed` runs only for
|
||||
`--help`.
|
||||
- bash itself. bash doesn't take `IFS` from the environment. `GLOBIGNORE` had no effect on the pid glob.
|
||||
`SHELLOPTS=noglob` (exported) turns the glob into its literal, which fails the numeric test, so nothing is
|
||||
seen and the script exits 2, "no processes". Exported functions, `BASH_ENV` and `PATH` let a caller put
|
||||
its own code in place of `ls`, `awk` or `tr`; the suite's shims rely on exactly that. The check can't
|
||||
defend against its own caller, and that stays a stated limit.
|
||||
- Locale. Under a UTF-8 locale, bash's `[^ /]*` in `RE_TEST` stops at a byte that isn't valid UTF-8, so
|
||||
`bash scripts/test-<0xff>x.sh` didn't match. `export LC_ALL=C` makes every regex and every tool read bytes.
|
||||
This host has `C.utf8`, so that test isn't vacuous here.
|
||||
|
||||
Tests, each RED on R4 except the `escape` pair: a loop over `QUOTING_STYLE` = `shell-always`, `c`, `escape`,
|
||||
`shell-escape-always` and `locale`, each with "QUOTING_STYLE=X: a failing cmdline read with the pid still
|
||||
listed: exit 2, no CLEAR" (the `failtr` shim) and "QUOTING_STYLE=X: a failing ancestor status read with the
|
||||
pid still listed: exit 2, no CLEAR" (`awkn fail`, which now passes extra environment through `env`). "under
|
||||
LC_ALL=C.UTF-8, a test script name holding the byte 0xff: exit 1" pins the locale line.
|
||||
|
||||
Mutants: `ls-env-quoting` (the `--quoting-style=literal` flag removed) and `locale-kept` (the `LC_ALL=C` line
|
||||
removed). `denied-listing-gone` keeps its intent at the new line. All killed.
|
||||
|
||||
### N2: quoting, spacing and operators no longer hide a sourced test
|
||||
|
||||
Was: three regular expressions read the text with a fixed idea of spacing. `RE_SNAPSHOT` needed single
|
||||
spaces and a space after `.sh`, `RE_SOURCED` cut a quoted path at its first blank and didn't allow a quote
|
||||
around `source`, and the general rules needed a blank after `.sh`. Seven of the review's 13 live wrappers
|
||||
gave CLEAR.
|
||||
|
||||
Fix, in three parts.
|
||||
|
||||
1. The flat form. When a command line holds a quote, a backslash, one of `; & | ( ) { } < >`, a backtick,
|
||||
any whitespace other than a single space, or two spaces in a row, `flat()` rewrites it in awk: quotes
|
||||
and backslashes removed, and each run of whitespace and those operators made one space. A command line
|
||||
with none of them is its own flat form, so most of the host's command lines skip the awk call (550 of
|
||||
661 on this host). `flat()` prints an `F` before the text; a failed awk or output without the `F` is
|
||||
exit 2.
|
||||
2. The rules on both forms. `RE_SNAPSHOT` now runs on the flat form only, so a tab, two spaces, a quoted
|
||||
snapshot path or `;` with no blank all reduce to the shape it expects. For any other process the general
|
||||
rules run on the flat form first, then on the raw form. The flat form finds `bash scripts/test-x.sh;echo
|
||||
done`, `docker compose` and `docker<TAB>compose`. The raw form keeps every R4 match, such as a script
|
||||
named `gate;1.sh` passed as one argument, which the flat form splits.
|
||||
3. The walk over a wrapper's text. `sourced()` is now an awk program that splits the wrapper's raw text
|
||||
into words the way the shell does. A blank, a newline or one of `; & | ( ) < >` and backtick ends a word.
|
||||
`'...'` and `"..."` are parts of a word, with the quotes removed. A backslash keeps the next character,
|
||||
and `$'...'` and `$"..."` read as quotes. A span starts after a word that is `source` or `.`, quoted or
|
||||
not, and runs to the next `;`, `&`, `|` or newline outside parentheses and backticks. Every word in the
|
||||
span is checked, not only the first, against the gate rule, the `mutant` rule and the test-script rule,
|
||||
each anchored at the word's end. So a quoted path with spaces is one word that still ends in
|
||||
`/scripts/test-x.sh`, and `source 2>/dev/null T`, `source $(cd scripts && pwd)/test-x.sh` and the
|
||||
backtick form match. Quoted text that holds a blank, an operator, a quote or a backslash is walked again
|
||||
as shell text, which covers `eval '...'`, nested `bash -c` and the harness's `'\''` form. A span still
|
||||
open at the end of quoted text stays open after the word. The first `shell-snapshots/snapshot-*.sh` word
|
||||
in a span is the harness's own snapshot and is skipped whatever its name, and so is a later word equal
|
||||
to it.
|
||||
|
||||
What the walk can't classify:
|
||||
|
||||
- An unbalanced quote isn't an error. The quote runs to the end of the text, and that text is walked again,
|
||||
so a `source` span after it still counts and gives BUSY. An error there would turn every wrapper with a
|
||||
stray apostrophe (`don't` in a heredoc, a commit message) into exit 2 and stall every gate on the host,
|
||||
while reading on keeps the one property that matters: a named test, gate or mutant file in a source
|
||||
argument is never CLEAR. The cost is a false BUSY when the quoted text only mentions a test.
|
||||
- Quoting deeper than 32 walk levels is exit 2, "cannot classify the command line of PID". Each level of
|
||||
escaped double quotes costs the walk about two levels, so 16 levels still read and 17 don't. Each level
|
||||
walks its quoted text again, so the limit also bounds the time on a text that is megabytes long.
|
||||
- A failed awk, or output that is neither a rule nor "none", is exit 2 with the same message. The scan goes
|
||||
on, so other BUSY lines still print, and the final line says the host isn't known to be idle.
|
||||
|
||||
The first version of the flat form was bash, `${s//x/y}` per character class. A fake proc root holding one
|
||||
2 MB command line full of quotes then took 36 s to 135 s to scan, since each replacement costs time per
|
||||
match. A busy check that takes minutes stalls every gate that waits on it. With the awk version the same
|
||||
root takes 238 ms to 478 ms as a plain process and 320 ms to 1387 ms as a wrapper, over 12 fill patterns
|
||||
(quotes, operators, blanks, backslashes, an unclosed quote, repeated `source` and `.` words). I checked the
|
||||
awk output against the bash version's on 27 inputs. On the real `/proc` (975 pids) R5 took 1317 ms to
|
||||
1349 ms against R4's 1168 ms to 1172 ms, both CLEAR at the time.
|
||||
|
||||
Tests (all RED on R4 except the controls named here):
|
||||
|
||||
- Rule cases on the wrapper shape, "sourced test script", RED on R4: `source 2>/dev/null T`, `source $(cd
|
||||
scripts && pwd)/test-x.sh`, the backtick form, `source "/seat/a;b (c)/scripts/test-x.sh"`, `eval 'source
|
||||
"/seat/a;b/..."'`, an `eval "..."` with escaped quotes around a path with spaces, the harness's
|
||||
`eval 'source '\''scripts/test-x.sh'\'''` with `pwd -P >|`, and a heredoc holding `don't` before
|
||||
`source "it's dir/scripts/test-x.sh"`. Two more pass on R4 and stay as controls:
|
||||
`eval $'source scripts/test-x.sh'`, and an unclosed `"` before `source /seat/scripts/test-x.sh`.
|
||||
- General rule cases: "test script" for `bash -c "bash scripts/test-x.sh;echo done"`, "docker compose" for
|
||||
two spaces and for a tab between `docker` and `compose`, and "gate" for `bash "/seat/gate;1.sh"` (a
|
||||
control for the raw form; it passes on R4).
|
||||
- CLEAR cases: a wrapper whose own snapshot is named `snapshot-bash-3-gate.sh`, bare and quoted (controls
|
||||
that pass on R4), and the same snapshot sourced twice (RED on R4, which read the second as a gate).
|
||||
- Depth: "a sourced test under 16 escaped quoting levels (131094 bytes): exit 1, sourced test script" and
|
||||
"quoting 17 escaped levels deep (262166 bytes): exit 2, cannot classify, no CLEAR".
|
||||
- awk failure: a shim that fails or prints nothing for one program only, keyed on the walk's `function
|
||||
walk` and the flat form's `gsub`: "the wrapper walk's awk exits 1", "the wrapper walk's awk prints
|
||||
nothing", "the flat form's awk exits 1" and "the flat form's awk prints nothing", each "exit 2, cannot
|
||||
classify, no CLEAR".
|
||||
- Live wrappers, one per shape in the review's table: a real `bash -c` in `$SANDBOX/wrap` whose sourced
|
||||
file writes a ready marker and blocks in `read -t 60 -u 3` on a fifo. Each case checks the marker, an
|
||||
empty children file, exit 1, no CLEAR and a BUSY line naming the wrapper's pid, the rule and its cwd,
|
||||
through a proc root that links only that wrapper. Shapes 1 to 6 pass on R4 and must still pass; shapes 7
|
||||
to 13 fail on R4. Live shape 14 is a plain wrapper doing the same blocking read inline, which must stay
|
||||
CLEAR. The suite kills and reaps each wrapper before the next. R4's external `node --test` child control
|
||||
(F2, F5) is unchanged and still passes: exit 1 on the child's line, no line for its wrapper.
|
||||
|
||||
Mutants, all killed: `quotes-kept`, `operators-kept`, `snapshot-single-space`, `flat-skipped`,
|
||||
`flat-double-blank-missed`, `special-tab-missed`, `flat-failure-passes`, `flat-sentinel-unchecked`,
|
||||
`raw-rules-dropped`, `flat-rules-dropped`, `source-arg-first-word`, `no-requote`, `quoted-span-reset`,
|
||||
`span-ends-in-parens`, `backtick-ignored`, `sq-kept`, `dq-kept`, `dollar-quote-glued`, `backslash-kept`,
|
||||
`snapshot-path-checked`, `snapshot-repeat-checked`, `depth-unbounded`, `awk-failure-none`,
|
||||
`lexer-garbage-none` and `classify-error-passes`. Each edits one line once; the table below gives the site
|
||||
by name.
|
||||
|
||||
Sites R5 moved, same intent: `snapshot-matched` deletes the new snapshot line, `no-node-rule` and
|
||||
`no-compose-rule` drop the rule from `rules()`, `snapshot-substring` turns the flat-form match into a
|
||||
substring test, `denied-listing-gone` edits the new `ls` line, `sourced-test-hidden` makes the snapshot
|
||||
branch return 1, `sourced-first-only` ends the walk at the end of the first span, and `dot-source-missed`
|
||||
drops `.` from the span start.
|
||||
|
||||
New false BUSY. Each one only delays a gate, and each was a choice in favor of never giving CLEAR while a
|
||||
named file runs. I checked each example on a fake proc root holding that one process, with R4 and R5
|
||||
(the claims table under "R5 runs").
|
||||
|
||||
New in R5 (CLEAR on R4, BUSY on R5):
|
||||
|
||||
- Every word in a span counts, not only the sourced file. So the file's arguments count
|
||||
(`source env.sh scripts/test-x.sh`), and so do redirection targets (`source x 2>notes-mutant.log`)
|
||||
and the words inside `$(...)` and backticks.
|
||||
- A `.` word starts a span, so `find . -name 'test-*.sh'` is BUSY.
|
||||
- A span doesn't end at `;`, `&` or `|` inside parentheses, `$(...)` or backticks, so
|
||||
`( source x; cat scripts/test-x.sh )` is BUSY. That keeps `source $(cd scripts && pwd)/test-x.sh` one
|
||||
span. An unbalanced `(` keeps the span open to the end.
|
||||
- A span open at the end of quoted text stays open: `echo 'x; source' scripts/test-x.sh` is BUSY.
|
||||
- The general rules now see the flat form in every process. So a shell's `-c` text that names a test,
|
||||
gate or mutant file next to a quote or an operator is BUSY, where R4 needed a blank after `.sh`:
|
||||
`bash -c 'cat scripts/test-x.sh|wc -l'`.
|
||||
|
||||
Kept from R4 (BUSY on both): `grep -rn x . scripts/test-x.sh`, `git add . gate.sh`,
|
||||
`grep -n source scripts/test-x.sh`, `echo source scripts/test-x.sh`, a commit message
|
||||
`git commit -m "fix; source scripts/test-x.sh"` (also with the path in single quotes), a `#` comment line
|
||||
naming one, the text after an unbalanced quote, and a second, different snapshot path whose name matches the
|
||||
gate rule. R5 still reads quoted text again as shell text and doesn't know comments, so these stay BUSY.
|
||||
|
||||
Two more, by reasoning, not tested:
|
||||
|
||||
- Waiters can block each other. Take two wrappers that each run the check and whose text also names a
|
||||
test in a source span. Each sees the other as BUSY until one gives up. R4 had the same risk with fewer
|
||||
shapes.
|
||||
- Text quoted more than 32 levels deep is exit 2 (that part is tested), which also holds a gate.
|
||||
|
||||
### N3: a correction to the R4 limit on bind mounts
|
||||
|
||||
R4's known limits say a mount-point test, "such as the hooks directory's st_dev against its parent's",
|
||||
would catch a bind-mounted hooks directory. That is wrong for the case it names. A different st_dev finds
|
||||
a mount of a different filesystem. A bind mount of a directory from the same filesystem keeps the device
|
||||
number, so the comparison passes it. A detector would have to read mount metadata, such as
|
||||
`/proc/self/mountinfo`, and look for a mount point at the hooks directory or between it and the
|
||||
repository. None is built; the limit stands. The R4 text stays as written; this entry supersedes it. No
|
||||
code changed for N3.
|
||||
|
||||
### What didn't change
|
||||
|
||||
`scripts/git-hooks/pre-push` and `scripts/install-git-hooks.sh` are untouched, and their cases pass
|
||||
unchanged in GREEN. The ancestor walk, the read loop, the gone rule (a successful literal listing that
|
||||
lacks the pid) and the exit codes are as R4 left them, apart from the new "cannot classify" error.
|
||||
`docs/TOOLS.md` now gives 153 cases, the flat form, the span rule, the exit 2 on text it can't classify, and
|
||||
the literal listing.
|
||||
|
||||
### R5 runs
|
||||
|
||||
`TMPDIR` was a fresh `mktemp -d /mnt/storage/scratch/tmp/XXXXXX`. Every run was under `timeout -k 10 1800`,
|
||||
one at a time, with output to files there. The suite runs were in the foreground. The mutant runner took
|
||||
12 minutes 41 seconds, longer than the tool's 10-minute foreground limit. So it ran as a background job of
|
||||
the tool, under the same `timeout`, and nothing else ran until it ended.
|
||||
|
||||
- RED, all new tests in place and R4's busy check, 23:32:37Z to 23:33:12Z: 119 passed, 34 failed. For this
|
||||
one run the checkout's `scripts/slot-busy.sh` was fd1aa8cc8's copy (`git diff --quiet fd1aa8cc8` on it
|
||||
passed), then R5's was put back (sha256 `8571e27d...` before and after). The hook and the installer are
|
||||
the same blobs in R4 and R5. All 105 R4 tests pass there. Of the 48 new tests, 34 fail and 14 pass. The 14
|
||||
are the controls named above: `bash "/seat/gate;1.sh"`, `eval $'source scripts/test-x.sh'`, the unclosed
|
||||
`"` before a source span, the two `snapshot-bash-3-gate.sh` wrappers, the `QUOTING_STYLE=escape` pair,
|
||||
live shapes 1 to 6 and live shape 14. Two earlier RED runs at 148 cases gave 117/31 and 119/29. In the
|
||||
first, a fake pid that one of my new tests left in a shared fake root made two other tests fail. I fixed
|
||||
that in the test before the second run. The last five cases added after that all fail on R4.
|
||||
- GREEN, 23:33:23Z to 23:33:35Z: 153 passed, 0 failed. The three code files haven't changed since.
|
||||
- R4's own suite, in a scratch tree of fd1aa8cc8's four scripts, 23:47:46Z to 23:47:53Z: 105 passed,
|
||||
0 failed. Its 105 labels all appear in GREEN, and GREEN's other 48 are the new tests above.
|
||||
- Mutants, 23:33:53Z to 23:46:34Z: base green (153/0), runner exit 0, 57 of 57 killed, none survived, no
|
||||
NO MATCH, no COPY FAILED, no timeout. Every kill exited 1. A DRY run before it matched all 57 sites
|
||||
once.
|
||||
|
||||
| Mutant | FAIL | First failing case |
|
||||
|---|---|---|
|
||||
| ancestor-dropped | 8 | push --force of a rewritten branch is refused |
|
||||
| unknown-object-passes | 2 | a push over a remote tip missing locally is refused, saying fetch first |
|
||||
| delete-passes | 13 | the install into a clean clone exits 0 |
|
||||
| install-over-hookspath | 2 | it says core.hooksPath is set |
|
||||
| busy-exits-0 | 52 | a node --test process: exit 1 |
|
||||
| differing-hook-replaced | 3 | the install refuses a different installed pre-push (exit 2) |
|
||||
| replace-in-place | 1 | the replacement was a rename: the old file keeps its bytes |
|
||||
| symlink-followed | 1 | the install refuses a symlinked pre-push, even with --force-install |
|
||||
| ancestor-error-passes | 1 | a failed ancestor check (an unknown local object) is refused |
|
||||
| snapshot-matched | 44 | no matching process: exit 0 and CLEAR |
|
||||
| ancestors-matched | 1 | this suite, an ancestor, has no BUSY line |
|
||||
| no-node-rule | 10 | a node --test process: exit 1 |
|
||||
| no-compose-rule | 4 | busy on: docker compose -p x up -d (docker compose) |
|
||||
| unreadable-read-clear | 13 | a pid directory with no cmdline: exit 2 |
|
||||
| vanished-is-error | 3 | a process that vanishes during its read isn't an error: exit 0 and CLEAR |
|
||||
| read-errors-ignored | 18 | quoting 17 escaped levels deep (262166 bytes): exit 2, cannot classify, no CLEAR |
|
||||
| self-read-unchecked | 1 | its own process unreadable (awk exits 1): exit 2, no CLEAR |
|
||||
| snapshot-substring | 2 | busy on: node --test /seat/tests/shell-snapshots/snapshot-guard.test.mjs (node --test) |
|
||||
| tag-replace-passes | 4 | push --force of a new tag object on the same commit is refused |
|
||||
| hooks-dir-symlink-followed | 3 | the install refuses a symlinked hooks directory (exit 2) |
|
||||
| short-sha-accepted | 2 | a remote object name of 0 is refused, not read as a new ref |
|
||||
| ancestor-read-unchecked | 7 | an ancestor's status read that fails after its own read passed: exit 2, no CLEAR |
|
||||
| exited-ancestor-error | 1 | an ancestor that has exited (its pid gone from /proc) ends the walk: exit 0 and CLEAR |
|
||||
| denied-lookup-vanished | 2 | a lookup denied after the listing (proc root mode 000): exit 2, no CLEAR |
|
||||
| denied-listing-gone | 2 | a lookup denied after the listing (proc root mode 000): exit 2, no CLEAR |
|
||||
| dir-check-skips | 1 | a proc root that turns unreadable after one read hides no later process: exit 2, no CLEAR |
|
||||
| sourced-test-hidden | 36 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source /seat |
|
||||
| sourced-first-only | 34 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source /seat |
|
||||
| dot-source-missed | 6 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && . /seat/scri |
|
||||
| installer-fixed-tmp | 2 | the install writes nothing through a link planted at a pid-named temporary path |
|
||||
| ls-env-quoting | 8 | QUOTING_STYLE=shell-always: a failing cmdline read with the pid still listed: exit 2, no CLEAR |
|
||||
| locale-kept | 1 | under LC_ALL=C.UTF-8, a test script name holding the byte 0xff: exit 1 |
|
||||
| quotes-kept | 1 | live shape 11, source "S" && source "T", no child: exit 1, sourced test script |
|
||||
| operators-kept | 3 | busy on: /bin/bash -c bash scripts/test-x.sh;echo done (test script) |
|
||||
| snapshot-single-space | 2 | busy on: /bin/sh -c docker compose -p x up (docker compose) |
|
||||
| flat-skipped | 10 | busy on: /bin/bash -c bash scripts/test-x.sh;echo done (test script) |
|
||||
| flat-double-blank-missed | 1 | busy on: /bin/sh -c docker compose -p x up (docker compose) |
|
||||
| special-tab-missed | 1 | busy on: `/bin/sh -c docker<TAB>compose -p x up` (docker compose) |
|
||||
| flat-failure-passes | 2 | the flat form's awk exits 1: exit 2, cannot classify, no CLEAR |
|
||||
| flat-sentinel-unchecked | 1 | the flat form's awk prints nothing: exit 2, cannot classify, no CLEAR |
|
||||
| raw-rules-dropped | 1 | busy on: bash /seat/gate;1.sh (gate) |
|
||||
| flat-rules-dropped | 3 | busy on: /bin/bash -c bash scripts/test-x.sh;echo done (test script) |
|
||||
| source-arg-first-word | 6 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source 2>/de |
|
||||
| no-requote | 10 | busy on: /usr/bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-2-ab.sh 2>/dev/n |
|
||||
| quoted-span-reset | 2 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source "/sea |
|
||||
| span-ends-in-parens | 2 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source $(cd |
|
||||
| backtick-ignored | 1 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source `cd s |
|
||||
| sq-kept | 6 | busy on: /usr/bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-2-ab.sh 2>/dev/n |
|
||||
| dq-kept | 13 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source "scri |
|
||||
| dollar-quote-glued | 1 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && eval $'sourc |
|
||||
| backslash-kept | 3 | busy on: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && eval "source |
|
||||
| snapshot-path-checked | 3 | not busy: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && echo h |
|
||||
| snapshot-repeat-checked | 1 | not busy: /bin/bash -c source /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && . /hom |
|
||||
| depth-unbounded | 1 | quoting 17 escaped levels deep (262166 bytes): exit 2, cannot classify, no CLEAR |
|
||||
| awk-failure-none | 1 | the wrapper walk's awk exits 1: exit 2, cannot classify, no CLEAR |
|
||||
| lexer-garbage-none | 2 | quoting 17 escaped levels deep (262166 bytes): exit 2, cannot classify, no CLEAR |
|
||||
| classify-error-passes | 5 | quoting 17 escaped levels deep (262166 bytes): exit 2, cannot classify, no CLEAR |
|
||||
|
||||
The runner cuts each line at 100 characters, which is why some first failing cases end mid-word.
|
||||
|
||||
Live probes, 23:46:56Z to 23:47:18Z, outside the suite (`probe.sh` in TMPDIR, exit 0). Each shape is one
|
||||
real `bash -c` started in `$P/wrap`, whose sourced file writes a ready marker and blocks in
|
||||
`read -t 60 -u 3` on a fifo. With the marker present and the children file read, R4's and R5's
|
||||
`slot-busy.sh` each scanned a proc root that links only that wrapper. Then R5 scanned the real `/proc`,
|
||||
and the table counts its BUSY lines for that pid. Each wrapper was killed and reaped before the next, and no
|
||||
"still alive" warning printed. `S` is an empty snapshot under `$P/shell-snapshots`, `SF` one that runs
|
||||
`false`, and `T` is `$P/scripts/test-x.sh`. Shape 2's path is relative to `$P/wrap`.
|
||||
|
||||
| Shape | Wrapper text | Pid | Marker, children | R4 | R5 | BUSY lines on the real /proc (R5) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | `source S && source T` | 732550 | ready, none | exit 1, BUSY 732550 sourced test script | exit 1, BUSY 732550 sourced test script | 1 |
|
||||
| 2 | `source S && .<TAB><TAB>'scripts/test-x.sh'` | 737522 | ready, none | exit 1, BUSY 737522 sourced test script | exit 1, BUSY 737522 sourced test script | 1 |
|
||||
| 3 | `source S ; source T ; :` | 742569 | ready, none | exit 1, BUSY 742569 sourced test script | exit 1, BUSY 742569 sourced test script | 1 |
|
||||
| 4 | `source SF \|\| source T` | 747729 | ready, none | exit 1, BUSY 747729 sourced test script | exit 1, BUSY 747729 sourced test script | 1 |
|
||||
| 5 | `source S && . "./gate.sh"` | 755860 | ready, none | exit 1, BUSY 755860 sourced gate | exit 1, BUSY 755860 sourced gate | 1 |
|
||||
| 6 | `source S && source ./mutants.sh` | 760031 | ready, none | exit 1, BUSY 760031 sourced mutant | exit 1, BUSY 760031 sourced mutant | 1 |
|
||||
| 7 | `source S && source ".../dir with spaces/scripts/test-x.sh"` | 763715 | ready, none | exit 0, CLEAR | exit 1, BUSY 763715 sourced test script | 1 |
|
||||
| 8 | `source S && "source" T` | 767367 | ready, none | exit 0, CLEAR | exit 1, BUSY 767367 sourced test script | 1 |
|
||||
| 9 | `source S;source T;:` | 771113 | ready, none | exit 0, CLEAR | exit 1, BUSY 771113 sourced test script | 1 |
|
||||
| 10 | `source S;. T;:` | 774899 | ready, none | exit 0, CLEAR | exit 1, BUSY 774899 sourced test script | 1 |
|
||||
| 11 | `source "S" && source "T"` | 778649 | ready, none | exit 0, CLEAR | exit 1, BUSY 778649 sourced test script | 1 |
|
||||
| 12 | `source<TAB>S && source "T"` | 782425 | ready, none | exit 0, CLEAR | exit 1, BUSY 782425 sourced test script | 1 |
|
||||
| 13 | `source<SPACE><SPACE>S && source "T"` | 786085 | ready, none | exit 0, CLEAR | exit 1, BUSY 786085 sourced test script | 1 |
|
||||
|
||||
Claims, 23:47:24Z to 23:48:44Z (`claims.sh` and `claims3.sh` in TMPDIR). Each row is one process in a fake
|
||||
proc root, scanned by R4 and R5. Unless the row says otherwise, the argv is
|
||||
`/bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && TEXT"`.
|
||||
|
||||
| TEXT | R4 | R5 |
|
||||
|---|---|---|
|
||||
| `source env.sh scripts/test-x.sh` | exit 0, CLEAR | exit 1, sourced test script |
|
||||
| `source x 2>notes-mutant.log` | exit 0, CLEAR | exit 1, sourced mutant |
|
||||
| `find . -name 'test-*.sh'` | exit 0, CLEAR | exit 1, sourced test script |
|
||||
| `( source x; cat scripts/test-x.sh )` | exit 0, CLEAR | exit 1, sourced test script |
|
||||
| `echo 'x; source' scripts/test-x.sh` | exit 0, CLEAR | exit 1, sourced test script |
|
||||
| argv `/bin/bash -c 'cat scripts/test-x.sh\|wc -l'`, no snapshot | exit 0, CLEAR | exit 1, test script |
|
||||
| `grep -rn x . scripts/test-x.sh` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `git add . gate.sh` | exit 1, sourced gate | exit 1, sourced gate |
|
||||
| `grep -n source scripts/test-x.sh` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `echo source scripts/test-x.sh` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `git commit -m "fix; source scripts/test-x.sh"` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `git commit -m "fix; source 'scripts/test-x.sh'"` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `echo hi`, then a line `# source scripts/test-x.sh` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `echo "it's && source /seat/scripts/test-x.sh` | exit 1, sourced test script | exit 1, sourced test script |
|
||||
| `source /home/u/.claude/shell-snapshots/snapshot-bash-9-gate.sh && echo hi` | exit 1, sourced gate | exit 1, sourced gate |
|
||||
| `source $'scripts/test\x2dx.sh'` | exit 0, CLEAR | exit 0, CLEAR |
|
||||
| `f=scripts/test-x.sh; source "$f"` | exit 0, CLEAR | exit 0, CLEAR |
|
||||
| argv `/bin/bash -c 'source "/home/a b/.claude/shell-snapshots/snapshot-bash-1.sh" && source scripts/test-x.sh'` | exit 1, test script | exit 1, test script |
|
||||
| the same with `&& node --test x.test.mjs; :` in place of the second source | exit 1, node --test | exit 1, node --test |
|
||||
| `echo hi && ls -la` | exit 0, CLEAR | exit 0, CLEAR |
|
||||
|
||||
The ANSI-C and variable rows are the misses under "Known limits after R5". The two rows with a blank in the
|
||||
snapshot path show that such a wrapper isn't skipped and goes through the general rules. Run as
|
||||
`env SHELLOPTS=noglob bash slot-busy.sh`, both versions exit 2 with "no processes", on a fake root holding
|
||||
`sleep 1` and on the real `/proc` (23:47:40Z). A first try set `SHELLOPTS=noglob` as a prefix
|
||||
assignment. bash refused it ("readonly variable") and ran the check without it, so that row is the `env`
|
||||
run.
|
||||
|
||||
- Real host, 23:50:13Z to 23:50:17Z: `bash slot-busy.sh` on the real `/proc`, R4, R5, R4, R5. Each exited
|
||||
0 with CLEAR. R4 took 1157 ms and 1183 ms, R5 1352 ms and 1275 ms, over about 965 pids. The earlier
|
||||
timing runs above gave R5 145 ms to 181 ms more than R4.
|
||||
- Pins: `git grep -nE '[0-9a-f]{64}' -- 'packages/*/tests/*' 'scripts/test-*.sh'` gives 33 lines (the
|
||||
positive control). None names a path this round changes, and none holds the old or new sha256 or blob id
|
||||
of the three changed code files. `git grep` for any of those twelve ids over the whole tree finds nothing.
|
||||
No test reads `docs/TOOLS.md`, `BUILD-LOG.md`, `docs/SESSIONS.md` or this file. `gate-lane-auth.test.mjs`
|
||||
leaves `docs`, `agents` and `BUILD-LOG.md` out of its copy, and `stack-release.test.mjs` only names
|
||||
TOOLS.md in an expected message.
|
||||
|
||||
### Known limits after R5
|
||||
|
||||
- `git push --no-verify` skips the hook. Gitea branch protection is the guard that holds (unchanged).
|
||||
- The busy rules read command lines as text. The walk is a model of shell word splitting, not bash's
|
||||
parser: it doesn't know comments, `case` patterns, arithmetic or here-strings. The false BUSY cases above
|
||||
are the price of reading every quoted and spanned word.
|
||||
- Still missed: a test sourced by a path built at run time (a variable, a glob, command substitution
|
||||
output), by an ANSI-C escape (`$'scripts/test\x2dx.sh'`, which the walk reads as plain quotes), by a
|
||||
nested file, a function or an alias, or by a shell reading its commands from stdin or a terminal. The
|
||||
command line doesn't show any of those, and the check may not read another seat's files (the review's
|
||||
Check 6, item 2).
|
||||
- A snapshot path with a blank in it, from a home directory with a space, doesn't match `RE_SNAPSHOT`, so
|
||||
that wrapper is never skipped. Its text goes through the general rules, which can only add BUSY lines.
|
||||
- `SPECIAL` must list every character `flat()` changes, or a command line holding only that character keeps
|
||||
its raw form. The tab and two spaces are pinned by tests and mutants; CR, VT and FF are listed but not
|
||||
pinned.
|
||||
- `--quoting-style` needs GNU ls. BSD or BusyBox ls fails every listing, which fails closed (exit 2 on any
|
||||
failed read) but turns every exit race into an error.
|
||||
- `-1` is a no-op for a pipe, so no test pins it.
|
||||
- Exported functions, `BASH_ENV` and `PATH` are the caller's to set; with them a caller can replace the tools
|
||||
the check runs.
|
||||
- A scan costs 145 ms to 181 ms more than R4's on this host (975 pids), and a single 2 MB wrapper text took
|
||||
up to 1.4 s.
|
||||
- A bind-mounted hooks directory isn't detected (corrected by N3 above).
|
||||
- The installer has a check-then-use window after its path checks (unchanged).
|
||||
- A BUSY line holds the command line as it is, so a wrapper whose `-c` text has newlines prints over several
|
||||
output lines. Chain on the exit code, never on the lines (unchanged advice).
|
||||
- The scan sees only what `/proc` lists to it. Under `hidepid=2`, another user's processes aren't listed and
|
||||
can't make it BUSY. This host's `/proc` has no hidepid option (unchanged).
|
||||
@@ -1882,3 +1882,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
|
||||
2026-11-02 | Sage implementation sub-agent (Claude Opus 5.5, child of T3 c5c1e8ba) | row 211 (#1709/#426) R3 from the GPT review, on t3/row211-push-guard-r3-fix over 09a847eda (picks 5734c1a80, c8f2cb70c) | F1-F5 and the object-name length check fixed with tests; suite RED 67/18, GREEN 85/0; 21 of 21 mutants killed; one local commit, not pushed
|
||||
2026-11-02 | Sage implementation sub-agent (Claude Opus 5.5, child of T3 c5c1e8ba) | row 211 (#1709/#426) R4 from the GPT review of R3, on t3/row211-push-guard-r4-fix over 09a847eda (picks bcd74ec33, 3a121885c, 7d03f212d) | R1-R3, runner exit status and installer mktemp fixed with tests; suite RED 90/14, GREEN 105/0; 30 of 30 mutants killed, runner exit 0; one local commit, not pushed
|
||||
2026-11-02 | Sage implementation sub-agent (Claude Opus 5.5, child of T3 c5c1e8ba) | row 211 (#1709/#426) R5 from the GPT review of R4, on t3/row211-push-guard-r5-fix over 09a847eda (from fd1aa8cc8) | N1-N3 fixed (literal ls listing under LC_ALL=C, flat form and awk word walk for sourced tests, bind-mount note corrected) with tests; suite RED 119/34, GREEN 153/0; 57 of 57 mutants killed, runner exit 0; 13 of 13 live shapes BUSY; two local commits, not pushed
|
||||
+15
-6
@@ -166,7 +166,7 @@ resolved through the CLI.
|
||||
| `scripts/test-conductor.sh` | Auto-apply selftests (sandboxed) | 17 cases |
|
||||
| `scripts/test-auth.sh` | Auth checkpoint selftests (no Docker) | 13 cases |
|
||||
| `scripts/test-land-work.sh` | Land-work selftests (sandboxed, fake Docker) | 84 cases |
|
||||
| `scripts/test-git-guard.sh` | Push guard, its installer and `slot-busy.sh` (scratch bare remote, fake `/proc`) | 105 cases |
|
||||
| `scripts/test-git-guard.sh` | Push guard, its installer and `slot-busy.sh` (scratch bare remote, fake `/proc`) | 153 cases |
|
||||
| `scripts/install-git-hooks.sh [--repo DIR] [--force-install]` | Install the push guard as `pre-push` in the common `.git/hooks` | Refuses a set `core.hooksPath`, a symlinked hooks directory or a different installed hook; exit 0 ok · 1 failed · 2 refused · 4 usage |
|
||||
| `scripts/slot-busy.sh` | Busy check: one `BUSY` line per gate or test process, or `CLEAR` | Exit 0 CLEAR · 1 BUSY · 2 error, including a process or an ancestor it can't read that isn't confirmed gone; chain on the exit code |
|
||||
| `scripts/gitea-api.sh <METHOD> <path> [body]` | Gitea API helper; reads the JSON credential file or a raw per-seat token file | Token never on argv/stdout |
|
||||
@@ -199,11 +199,20 @@ Never `bash scripts/slot-busy.sh | tail -1 && gate`: `tail` exits 0, so the
|
||||
gate runs while BUSY. It matches `node ... --test`, a `*gate*.sh`, `mutant`, a
|
||||
`test-*.sh` and `docker compose` command lines, skips itself, its ancestors
|
||||
and agent shell-snapshot wrappers (a shell whose `-c` text starts by sourcing
|
||||
`.../shell-snapshots/snapshot-*.sh`). A wrapper whose text also sources
|
||||
(`source` or `.`) a gate, test or mutant script is BUSY, because that script
|
||||
runs in the wrapper's own shell. It reads only `/proc/<pid>/cmdline`, the cwd
|
||||
link, its own and its ancestors' `status` and, after a failed read, the proc
|
||||
root's listing. A process or an ancestor it can't read makes it exit 2, never
|
||||
`.../shell-snapshots/snapshot-*.sh`). Each rule runs on the command line as it
|
||||
is and on a flat form with the quotes and backslashes removed and every run of
|
||||
blanks, newlines and the operators `; & | ( ) { } < >` and backtick turned into
|
||||
one space, so quoting and spacing don't hide a match. A wrapper is BUSY when
|
||||
any word after a `source` or `.`, up to the next `;`, `&`, `|` or newline
|
||||
outside parentheses and backticks, names a gate, test or mutant script,
|
||||
because that script runs in the wrapper's own shell. Quoted text, an `eval`
|
||||
string for one, is read the same way. That rule can call a wrapper BUSY that
|
||||
runs no test; row 211's BUILD.md (R5) lists the cases. A command line it can't
|
||||
classify, quoting more than 32 levels deep for one, makes it exit 2. It reads
|
||||
only `/proc/<pid>/cmdline`, the cwd link, its own and its ancestors' `status`
|
||||
and, after a failed read, the proc root's listing, which it takes from GNU
|
||||
`ls --quoting-style=literal` under `LC_ALL=C` so the caller's environment
|
||||
can't change it. A process or an ancestor it can't read makes it exit 2, never
|
||||
CLEAR, unless a listing of the proc root succeeds and no longer has that pid.
|
||||
|
||||
## Seat launch with registration (`scripts/mosaic`)
|
||||
|
||||
Reference in new issue
Block a user