Skip to content

feat(doctor): ask agy which model it runs; validate permissions.allow - #56

Merged
yuting0624 merged 13 commits into
masterfrom
feat/doctor-probe-and-allow-rules
Aug 10, 2026
Merged

feat(doctor): ask agy which model it runs; validate permissions.allow#56
yuting0624 merged 13 commits into
masterfrom
feat/doctor-probe-and-allow-rules

Conversation

@yuting0624

Copy link
Copy Markdown
Owner

Follow-ups deferred from #54, now that agy 1.1.11 is out. Three items were listed there as "not in this PR"; this settles all three, one of them by deciding not to do it.

1. Stop inferring --model, ask

#54 warns when agy is below 1.1.10, where --model was ignored in headless -p. That warning is the best a version comparison can do — and a version comparison is a proxy. It is right about the releases we know about and silent about every other way the flag can fail to land.

1.1.11 answers the read-only slash commands in print mode without starting an agent turn. So doctor asks:

✓ --model takes effect (asked for 'Gemini 3.5 Flash (High)', agy reports 'gemini-3.5-flash-high')

usage.total_tokens: 0 — no quota, no conversation left behind. It reads the tab-separated reply's slug and matches it against a tier configured as a display name: the same either-direction comparison agy models needed in 0.20.x.

Gated at 1.1.11 deliberately. Below that the slash command is not recognised, falls through as literal prompt text, and the model answers as though it had run — probing there would spend a real turn and believe the answer it invented. The stub agy logs every invocation, so "never probes below 1.1.11" is asserted against the log, not described in a comment.

An empty answer draws no conclusion either way. An older build than the version claims, a hang, or a plan that refuses the probe is not evidence that routing is broken.

2. permissions.allow validation

We recommend such a rule in eight places as the narrow alternative to --yolo, and the recommendation ships a placeholder: write_file(<dir>).

A rule agy cannot parse announces itself in neither direction, and which way it fails depends on the version:

agy consequence of an unparseable entry
≥ 1.1.11 matches nothing — the grant is absent, the write is soft-denied (exit 15), and the rule is sitting right there in the file
< 1.1.11 matches every command, silently auto-approving anything the agent ran — broader than the --yolo it was chosen instead of

doctor flags the entry, names the reason, and reports the consequence for your version rather than both.

The zero-command-word test follows upstream's own examples — command(time) (a shell reserved word that prefixes a command without being one), a comment-only entry, an empty compound () — plus the unsubstituted <...> placeholder, which is ours.

Rules it cannot judge are left alone: unbalanced quotes are agy's parser's business, and write_file(...) is a different matcher from command(...). A false positive sends someone to edit a rule that was always fine, so the well-formed case is pinned as hard as the broken ones. Checked against 12 synthetic configs including the maintainer's real rule set.

3. Verified against 1.1.11, no change needed

  • all three tiers resolve: flashgemini-3.5-flash-high, flash-lo-low, progemini-3.1-pro-high
  • the exit-14 classifier still fires on an unknown model
  • 1.1.11 reworded model-loading errors, but exit 15 is intact — its message still contains auto-denied and permissions.allow verbatim, two independent anchors the classifier already matches

/usage and /quota were considered for doctor and dropped on evidence. They return zero bytes here. That is not a bug: 1.1.11's own notes say credits do not apply to accounts signed in through a Google Cloud project or ADC. /model, /effort and /skills all return data on the same setup — which is what made §1 possible.

Tests

224 → 234. Every new assertion was shown capable of failing:

  • 6 fail against the unfixed doctor (tests file kept, scripts/doctor.sh alone reverted — reverting both proves nothing, which is how the first attempt at this check read "0 failures")
  • 4 are negative properties of the new code and cannot fail against master by construction, so they were confirmed by mutation: remove the version gate, remove the empty-answer branch, widen the tokenizer, remove the version case — each must turn its test red

Mutation found a real defect in one of them. The two verdicts read takes effect and does NOT take effect, so a pattern copied from one branch silently stopped guarding the other: the empty-answer test passed with the branch it guards removed. The first mutation attempt also produced a false alarm by not matching the file at all, so the harness now asserts the mutation actually applied before drawing any conclusion.

0.22.4 warned when agy was below 1.1.10, where --model was ignored in
headless -p. That is the best a version comparison can do, and a version
comparison is a proxy: right about the releases we know about, silent
about every other way the flag can fail to land.

agy 1.1.11 answers the read-only slash commands in print mode without
starting an agent turn, so doctor requests a tier model with -p /model
and reports which one comes back. Zero tokens, no quota, no conversation
left behind. Gated at 1.1.11: below it the command falls through as
prompt text and the model answers as though it had run, so probing would
spend a real turn and then trust its own invention. The stub agy logs
every invocation, so "never probes below 1.1.11" is asserted against the
log rather than described in a comment.

Separately, doctor now validates permissions.allow. The plugin
recommends such a rule in eight places as the narrow alternative to
--yolo, and ships a placeholder, write_file(<dir>). A rule agy cannot
parse is silent in both directions and fails opposite ways either side
of 1.1.11: from 1.1.11 it matches nothing, so the grant is absent and
the write is soft-denied with the rule sitting visibly in the file;
before 1.1.11 an entry tokenizing to zero command words matched EVERY
command and auto-approved anything the agent ran, broader than the
--yolo it was chosen instead of. doctor names the entry, the reason, and
the consequence for the version in front of it.

The zero-command-word test follows upstream's own examples, plus our own
placeholder. Rules it cannot judge are left alone: unbalanced quotes are
agy's parser's business, write_file(...) is a different matcher from
command(...). A false positive sends someone to edit a rule that was
always fine, so the well-formed case is pinned as hard as the broken
ones.

Verified against 1.1.11 with no change needed: all three tiers resolve,
the exit-14 classifier still fires, and the reworded model-loading
errors leave exit 15 intact (its message still carries "auto-denied" and
"permissions.allow" verbatim). /usage and /quota were considered for
doctor and dropped: they return zero bytes on this account type, which
1.1.11's notes explain rather than a bug.

10 new assertions, all shown capable of failing. 6 fail against the
unfixed doctor; the other 4 are negative properties of the new code and
were confirmed by mutation. That found a real defect in one of them:
the two verdicts read "takes effect" and "does NOT take effect", so a
pattern copied from one branch silently stopped guarding the other, and
the empty-answer test passed with the branch it guards removed.

224 -> 234 tests.
@quorum-code-review

quorum-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Quorum review

Reviewing 6fdb0a6 with gemini-3.6-flash, claude-sonnet-5 — both read the diff independently, so this takes a few minutes.

This comment will be replaced by the result.

Comment thread scripts/doctor.sh Outdated
t = e.strip()
if not t:
bad.append(("(empty string)", "empty entry")); continue
if "<" in t or ">" in t:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor false-positive risk: "<" in t or ">" in t flags any occurrence of these characters anywhere in the entry as an "unsubstituted placeholder", not just <...> placeholder syntax. A well-formed command(...) rule that happens to use a shell redirect or comparison (e.g. command(sort < file.txt)) would be misreported as a placeholder rather than left alone like other well-formed rules. Given the "well-formed case is pinned as hard as the broken ones" bar this PR sets for itself, this one path isn't covered by a test — worth a quick check on whether that shape is realistic for permissions.allow entries, and if so a placeholder-specific pattern (e.g. requiring <...> to actually bracket something) would avoid it.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (doctor.sh, tests, README/TROUBLESHOOTING/SKILL.md, CHANGELOG). This PR does not touch hooks/validate-delegate-bash.sh, agy-delegate.sh, or agy-job.sh, so the security gate and the exit-code/signal contract are unaffected.

Shell logic (scripts/doctor.sh): traced the new -p /model probe gate, the bidirectional slug/display-name match, and bad_allow_rules() Python. All check out, including the version-gate arithmetic (ver_lt on 1.1.10/1.1.11) and the empty-answer no-verdict branch the PR says was mutation-tested. Left two review comments inline:

  1. Doc drift on docs/POC-PLAYBOOK.md line 105 - that bullet is a verbatim duplicate of the README.md Two write grants block (same wording, same Plan implementation delegation doesn't work, because agy can't write files #10 link) that this PR updated in README/TROUBLESHOOTING/SKILL but not here, so the playbook still presents write_file() without the new placeholder / version-dependent-failure warning. Could not inline it since the file is not part of this diff - GitHub rejects review comments on unchanged files.
  2. Minor false-positive risk in bad_allow_rules() (doctor.sh line 133) - the placeholder check matches '<' or '>' anywhere in the entry, not just <...> syntax, so a rule using a literal shell redirect/comparison would be misflagged as an unsubstituted placeholder. Untested edge case; only worth fixing if that shape is realistic for these rules.

Cost discipline: the -p /model probe reads one line via head -1 | cut -f1 - no bulk output reaches the conductor. Good.

Verified claims: the exit 15 still contains auto-denied and permissions.allow verbatim claim checks out against the classifier in agy-delegate.sh line 494 (unchanged by this PR). Version bump is consistent between .claude-plugin/plugin.json and skills/antigravity/SKILL.md (both 0.22.5).

No test-passes-for-wrong-reason or silent-no-op issues found in the new assertions - traced through the stub agy scripts and confirmed they exercise the paths they claim to (probe not run below 1.1.11, probe run at 1.1.11, empty-answer branch, well-formed-rules produce no warning, and the version-dependent consequence message).

Comment thread scripts/doctor.sh Outdated
…finding

Both reviewers on #56 caught the first cut printing the pre-1.1.11
"matches EVERY command and auto-approves anything" consequence for every
flagged entry. That history belongs to one class only: a command(...)
rule naming no command, which is what agy 1.1.11 fixed. A mistyped
write_file(<dir>) never had it. Telling someone holding one that their
config auto-approves everything is a security claim about a situation
they are not in.

bad_allow_rules now emits a class alongside the reason. Every class
still reports that the grant is absent, since that is true of all of
them; only zerowords adds the version-dependent sentence.

Also from review: the placeholder test now matches the <...> SHAPE
rather than a bare angle bracket, so command(echo hi > /tmp/f) is not
misread as a template nobody filled in.

And the write-grant block in POC-PLAYBOOK.md was a verbatim copy of the
README one, so the placeholder warning landed in three files and missed
the fourth. Updated, framed for that document: either failure mode makes
an arm's write behaviour a second uncontrolled variable, which is what
its own section 5 says a cost claim cannot carry.

3 more assertions, each confirmed by mutation. The first mutation
reproduces the reviewers' finding exactly. 234 -> 237.
Comment thread scripts/doctor.sh Outdated
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review

Focused on scripts/doctor.sh (the only executable change) plus the accompanying tests/docs. No changes touched hooks/validate-delegate-bash.sh or the exit-code contract in agy-delegate.sh/agy-job.sh, so those are out of scope here.

Traced through both new checks and found them sound:

  • 2c (--model probe via -p /model): correctly gated behind ver_lt "$AGY_VER" 1.1.11 so it never fires on a version where the slash command would fall through as a literal prompt (which would burn a real turn and trust a fabricated answer). FLASH is in scope when 2c runs (set earlier in the same if [ -n "$MODELS" ]). The nested case-in-elif for bidirectional slug/display-name matching (doctor.sh:302) is valid POSIX/bash-3.2 syntax and mirrors the existing model_present logic. Empty-answer path correctly draws no conclusion instead of asserting failure.
  • 3b (bad_allow_rules): walked the classification logic (placeholder-shape regex, command(...) zero-word tokenization, PREFIX reserved-word stripping) against the test cases plus a few not in the suite (command(if time), command(# comment), multi-paren nesting) — all classify correctly. The zerowords vs unparseable split correctly confines the "matches every command" security claim to the one class that actually carries that history (addressing the "attached to every finding" issue the changelog says both reviewers caught on the first cut) — verified write_file(<dir>) never gets that claim, on either side of the 1.1.11 gate.
  • Cross-checked the new exit 15 / auto-denied / permissions.allow claims in the changelog and doctor output against the actual strings in agy-delegate.sh:494,498 — they match verbatim, no drift.
  • The "recommends a rule in eight places" claim added to this changelog entry is accurate: exactly 8 non-changelog occurrences of write_file(<dir>) in the repo post-diff, and the new prose in README/POC-PLAYBOOK/TROUBLESHOOTING/SKILL.md annotates those existing lines rather than adding new ones.
  • plugin.json / SKILL.md version (0.22.5) are in sync; no stale 0.22.4 references outside CHANGELOG history.

Not independently verifiable here: the "Verified against agy 1.1.11" changelog claims (tier resolution, exit-14 classifier, exit-15 message stability) and the shape of the real -p /model reply — I couldn't execute anything against a real agy binary in this environment. The stub-based test suite is honest about testing the wrapper's logic, not the CLI's actual behavior, and the changelog appropriately separates "tested as a fact" (against the stub) from "verified against agy 1.1.11" (asserted, unmeasured here).

Minor, not blocking: a permissions.allow entry containing an embedded literal newline (JSON \n in the string) would break the TAB/newline-per-record protocol between the python helper and the bash while read loop in doctor.sh:332–338, since nothing strips internal newlines before printing. Vanishingly unlikely for a real permission rule string; not worth guarding given the "rules it cannot judge are left alone" design already covers other odd shapes.

No high-confidence bugs found. Nice work confining the security-claim wording to the class it's actually true for.

…e test construct

Review finding: <[^<>]*> false-positives on a rule with TWO literal
redirects, command(sort < in > out), where everything between the
brackets is a filename. A placeholder is now the shape AND
placeholder-shaped content — one unbroken token, no whitespace — which
still catches <dir> and <path/to/repo>.

The bigger find came from chasing the flake that appeared while testing
that. `printf '%s' "$x" | grep -q PAT` is the shape fixed in 0.21.1 and
explained in a comment in this same file: grep -q exits at the first
match, closes the pipe, the writer takes SIGPIPE (141), and pipefail
marks the pipeline failed — the assertion reads "not found" while the
text is right there. The window is whatever the writer still has to emit
AFTER the matched line, so appending output below a match revives it.
This release appended output below a match: the 1.1.9 version-gate
assertion failed 1-4 times in 8 concurrent runs on this branch and never
on master.

Diagnosed rather than guessed. Instrumenting the test made it vanish, so
the trace went inside doctor: mktemp never failed and AGY_VER was never
empty, which left only the assertion misreading output that had in fact
been printed.

All 25 occurrences now use a case-based has(): same test, no second
process, no pipe. The five `sed ... | grep -q` source scans give grep a
process substitution, so sed's death is not pipefail's business. Those
five failed SILENTLY — a false negative there reads as "the property
holds" — and they guard the delegate wrapper's --help probe, agy_guard's
pipe rule, and the sort -V dependency.

24 consecutive clean runs at the concurrency that reproduced it. Each
converted source scan re-checked by mutation. 237 -> 239.
Comment thread tests/run-tests.sh
Comment thread tests/run-tests.sh
Comment thread tests/run-tests.sh
echo "FAIL: \$out is empty at the envelope check — the assertion below proves nothing"; FAIL=$((FAIL+1));
else echo "ok: \$out still holds the json_ok reply at the envelope check"; PASS=$((PASS+1)); fi
if printf '%s' "$out" | grep -q 'conversation_id'; then
if has 'conversation_id' "$out"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has() is called before it's defined — this assertion always "passes", for the wrong reason.

has() is defined at line 692, but this call executes earlier at top-level (bash doesn't hoist function definitions). At line 211, has is not yet a known command, so this errors with has: command not found (exit 127) and the if takes the else branch unconditionally — printing "ok: json envelope does not leak to stdout" regardless of whether conversation_id is actually present in $out.

This is exactly the "test that passes for the wrong reason" class this PR's own CHANGELOG entry calls out (negative assertion that passes for free while PASS still increments). It defeats the json-envelope-leak regression check this line exists to guard.

Same bug at line 368 (has 'NOT write to your workspace without it' "$out"), which similarly always reports "ok" regardless of the actual warning text.

Fix: move the has() definition (line 692) above its first use, e.g. near the top of the file next to the other helpers (check, etc.).

Comment thread tests/run-tests.sh
out=$(STUB_MODE=args "$DELEGATE" "implement the parser module" 2>&1)
check "write warning names the permissions.allow route" 0 0 "permissions.allow" "$out"
if printf '%s' "$out" | grep -q 'NOT write to your workspace without it'; then
if has 'NOT write to your workspace without it' "$out"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same bug as line 211: has() isn't defined until line 692, so this call fails with has: command not found and unconditionally takes the else branch — always printing "ok: warning no longer claims --yolo is the only write grant", even if the old "NOT write to your workspace without it" text came back. See the comment on line 211 for the fix (move the has() definition above its first use).

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (doctor.sh --model probe + permissions.allow validation, doc updates, and the run-tests.sh SIGPIPE/pipefail cleanup). No changes to hooks/validate-delegate-bash.sh, agy-delegate.sh, or agy-job.sh in this PR, so the security gate and exit-code contract are out of scope here.

One high-confidence bug, left as inline comments: in tests/run-tests.sh, the has() helper (added to fix the grep -q/pipefail SIGPIPE race) is defined at line 692, but two pre-existing assertions at lines 211 and 368 call it earlier in top-level execution. Bash doesn't hoist function definitions, so both calls hit has: command not found (exit 127) and unconditionally take the else branch — meaning both "ok" results print regardless of the actual content being checked. This is the "test that passes for the wrong reason" class the PR's own CHANGELOG explicitly calls out (and ostensibly fixed for 25 sites) — these two slipped through because the refactor touched pre-existing lines that sit above the new has() definition. Fix: move the has() definition above line 211.

Everything else checked out:

  • The permissions.allow zero-command-word / placeholder detection logic (PLACEHOLDER regex, PREFIX set, command-vs-write_file scoping) is correct against all the test cases in the diff, including the two narrowing rounds already applied from prior review (redirect-pair false positive, class-scoped consequence message).
  • The --model probe (agy_guard 20 --model "$FLASH" -p /model) matches the existing -p PROMPT invocation convention used in agy-delegate.sh, is correctly gated on ver_lt "$AGY_VER" 1.1.11, and correctly declines to draw a conclusion on an empty answer.
  • The sed ... | grep -qgrep -q ... <(sed ...) conversions correctly eliminate the pipefail/SIGPIPE race (process substitution isolates sed's exit status from the pipeline's).
  • No bash 4+-isms introduced in doctor.sh (no ${var,,}, mapfile, declare -A), consistent with existing macOS bash 3.2 support.
  • Version references (1.1.11 gating, 0.22.4→0.22.5 in plugin.json and SKILL.md) are consistent across CHANGELOG, README, POC-PLAYBOOK, TROUBLESHOOTING, and SKILL.md.

I did not independently verify the claims about real agy 1.1.11 behavior (slash-command replies, /usage returning zero bytes on ADC accounts, exit-14/15 classifier stability) — those are asserted in the PR description as manually verified against a live binary, which I have no way to reproduce here.

Both reviewers, independently: has() was defined beside the doctor tests
at line 692, and two of the conversions sat at 211 and 368. bash does
not hoist, so those two calls were `command not found`, exit 127, else
taken unconditionally, "ok" printed regardless of what was being
checked. The previous commit's own message claimed to be removing
exactly this class of defect.

One of the two was the "json envelope leaked to stdout" assertion, which
had already been voided once before by a different accident. Mutation
confirms it is live again: leaking the envelope now fails it.

Helpers moved to the top of the file. tests/check-helper-order.py runs
first and fails the suite when any function is called above its
definition, naming both lines.

bash 4's command_not_found_handle was tried for this and removed. macOS
ships bash 3.2, where merely defining it is a silent no-op — a guard
that reads as protection and provides none, which is the defect this
whole PR is about. Verified by putting the original bug back: the static
check catches it, the handler did not.

239 -> 240.
Comment thread tests/check-helper-order.py Outdated
# A CALL is the bare name in command position. Excluded: the definition itself
# (`name()`), any prose about it (a comment line), and any other name that merely
# ends with these characters.
call = re.compile(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Call-site regex misses common bash contexts and ignores quoting

The new hoisting guard scans raw text for a fixed set of anchors that must immediately precede a function name to count as a "call": start-of-line, ;, &, |, if , then , else , while , until , do , !, $(. This has two concrete gaps that undermine the exact guarantee the tool exists to provide (catching a helper called above its own definition, which the CHANGELOG for this PR says caused command not found/exit 127/silently-taken else branches):

  1. False negative: elif is not covered, because \bif\s cannot match inside elif (no word boundary between el and if). A call written as elif has "$x"; then above has() { ... } would not be detected. Worse, since the outer loop does break on the first regex match found scanning top-to-bottom, if a later, correctly-anchored call to the same function exists further down (even after the definition), the scan stops there and the earlier unmatched elif-based call is never reported at all.
  2. False positive: the script only excludes whole-line comments (line.lstrip().startswith('#')); it has no awareness of shell string literals. A test description such as check "...; has been broken" ... sitting above has()'s definition would trip the ;-anchor purely from the string's punctuation, flagging a bug that doesn't exist.

Right now the file happens not to trigger either case, so the suite passes, but the tool is materially weaker than its stated purpose ("the check is static ... and works on any shell" / "it names the call site and the definition line"), and a future ordinary edit (e.g. adding an elif has ... branch above a helper) would silently defeat the guard.

Second opinion from gemini-3.6-flash

claude-sonnet-5 raised this. gemini-3.6-flash was then asked to judge it — without being shown the reasoning, the severity, or who reported it, so that it would assess the code rather than agree with a colleague. Its answer:

The regular expression used to match function call sites misses several standard bash contexts (such as elif, backticks `fn`, subshells (fn), and group blocks { fn; }) and ignores shell quoting, causing strings like echo "foo; fn" to be incorrectly identified as function calls.

reliability · id e0b3f364669cd497

Comment thread skills/antigravity/SKILL.md Outdated
ways — from 1.1.11 it matches nothing (the grant is absent, exit 15 with the rule
visibly present in the file), and before 1.1.11 an entry tokenizing to zero command
words (`command(time)`, comment-only, `()`) matched EVERY command. If a user reports a
rule that "should" work, have them run `agy-doctor` before changing anything else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caveat (placeholder must be substituted, malformed rule fails silently in opposite ways either side of 1.1.11, run agy-doctor) lands here, in README.md, POC-PLAYBOOK.md and TROUBLESHOOTING.md — but not in the two places that arguably matter most:

  • scripts/agy-delegate.sh:497, the exit-15 message shown at the moment a write is soft-denied (the exact symptom this whole feature diagnoses). It still just says "add a write_file(<dir>) rule" with no mention that an already-present rule could be the cause, and no pointer to agy-doctor. The parallel prose in docs/TROUBLESHOOTING.md for this exact same exit-15 case was updated to say "suspect the rule before suspecting agy" and to run agy-doctor — the runtime string wasn't kept in sync.
  • agents/antigravity-delegate.md:88 and commands/delegate.md:15, the operational instructions actually consulted when constructing a delegation, still show the bare write_file(<dir>) recommendation with no placeholder/version caveat at all.

Given "doc drift across more than the file in the diff" is a recurring failure class here, worth deciding whether these should get at least a short pointer (e.g. "malformed rule? run agy-doctor") rather than silently diverging from the docs that were updated.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (doctor.sh model-probe + permissions.allow validation, run-tests.sh SIGPIPE/ordering fixes, check-helper-order.py, docs). CI/shellcheck/plugin-validate aren't repeated here.

Shell logic — looks correct:

  • bad_allow_rules() (doctor.sh:113-175): traced the placeholder regex, the prefix-word stripping, and the zerowords vs unparseable classing against all the cases in the new tests (bare <dir>, <path/to/repo>, single/paired literal redirects, command(time), bare (), unbalanced quotes). All match the described behavior; no false positive/negative found beyond what's already tested.
  • The --model probe (doctor.sh:292-313): version gate, empty-answer handling, and the two-way substring match via nested case all check out. agy_guard's file-based capture means the outer | head -1 | cut -f1 pipe can't reintroduce the SIGPIPE/pipefail issue this same PR fixes elsewhere, since the result is only used in a substitution, never if-tested.
  • tests/run-tests.sh<(...) process-substitution fix for the sed | grep -q SIGPIPE-under-pipefail race: correct — the substituted producer isn't part of the pipeline pipefail evaluates, so an early-exiting grep -q no longer poisons the exit status via the killed sed.
  • Verified by inspection (sandbox blocked executing the checker) that has(), ver_doctor(), probe_doctor(), and allow_doctor() are all defined before every call site in run-tests.sh — the exact class of bug the last two commits fixed. check-helper-order.py's regex doesn't special-case elif/case/for before a call, so it wouldn't catch that shape if introduced later, but no current call site relies on it.
  • No changes to hooks/validate-delegate-bash.sh, agy-delegate.sh's exit-code contract, or agy-job.sh — nothing new to scrutinize there.

Doc drift (left as an inline comment on skills/antigravity/SKILL.md:226, summarized here): the new placeholder/version caveat for write_file(<dir>) was added to README.md, POC-PLAYBOOK.md, TROUBLESHOOTING.md and one SKILL.md section, but two higher-value spots were missed:

  • scripts/agy-delegate.sh:497 — the actual exit-15 runtime message shown when a write is soft-denied (the exact symptom this feature diagnoses) still doesn't mention that the rule itself could be the cause, or point at agy-doctor, even though TROUBLESHOOTING.md's prose for this same case now does.
  • agents/antigravity-delegate.md:88 / commands/delegate.md:15 — the operational instructions that actually get consulted when building a delegation call still show the bare, uncaveated recommendation.

Version/count claims — checked: .claude-plugin/plugin.json and skills/antigravity/SKILL.md both say 0.22.5 (in sync), and the "eight places" the PR description claims for the write_file(<dir>) recommendation checks out exactly against a repo-wide grep.

Nothing else stood out — the mutation-testing discipline in the commit history is visible in the diff (e.g. the has()/helper-ordering fix, the placeholder-shape narrowing) and held up under a second look.

… rule

Review: the checker's regex matched command position with one big
alternation of contexts and missed elif, a call inside a case branch,
and a brace group. Confirmed against the old pattern — all three
returned False. A checker that misses a shape is a false negative, which
is the defect it exists to prevent, so it now splits the line into
command segments and compares each segment's first word. No list of
contexts to keep complete.

The checker has its own tests now, including a no-false-positive case
for a mention inside a string. It promptly flagged its own test data,
which is fair: a fixture written across real lines is not
distinguishable from code. Fixtures are single-line.

Also review: the placeholder caveat landed in four documents and missed
the one place someone actually lands when a write is soft-denied. The
exit-15 message recommended permissions.allow without allowing that the
rule itself may be the problem, which is exactly what this release added
a check for. It now says to run agy-doctor when a rule is already
present. Same caveat in agents/antigravity-delegate.md and
commands/delegate.md, the two files consulted while building a
delegation call.

240 -> 245.

# Anything that ends one command and begins another. `(` and `)` cover subshells and
# the `pattern)` opening a case branch; backtick and `$(` cover substitution.
SEGMENT = re.compile(r"\|\||&&|\$\(|[;&|()`{}]")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Segment splitter treats bare {/} as delimiters, mis-parsing ${name}

The new SEGMENT regex splits on the single characters { and } unconditionally, without checking whether they are preceded by $ (i.e. part of a parameter expansion like ${name} rather than a brace group). For a line containing ${hlp} where hlp is the name of a helper function defined later in the file, the split produces the isolated segment hlp, which calls() then reports as a call in command position — even though it is only a variable reference inside ${...}. This is a false positive of exactly the kind this rewrite was supposed to eliminate (the PR explicitly added a 'no false positive' test for a quoted mention, but missed the ${...} shape). It doesn't currently fire because no such variable name collides with a helper name in tests/run-tests.sh, but the very next edit that introduces ${name} matching an existing helper name before its definition will make the CI helper-order check fail spuriously.

Second opinion from gemini-3.6-flash

claude-sonnet-5 raised this. gemini-3.6-flash was then asked to judge it — without being shown the reasoning, the severity, or who reported it, so that it would assess the code rather than agree with a colleague. Its answer:

In tests/check-helper-order.py, line 20 includes { and } inside the character set [;&|(){}]ofSEGMENT. When SEGMENT.split(line)runs on a line containing a parameter expansion like${fn}(e.g.echo ${fn}), re.splitmatches{and}as segment boundaries and splits the line into["echo $", "fn", ""]. calls(line, "fn")evaluates the segment"fn", finds "fn"in command position as the first word of that segment, and returnsTrue`, mis-parsing the parameter expansion as a function call.

correctness · id c003c638bf5c4211

Comment thread agents/antigravity-delegate.md Outdated
`--yolo`, which approves every tool. You cannot see that file, so `--yolo` stays the
`--yolo`, which approves every tool. If they say a rule is in place and the write is
still soft-denied, have them run `agy-doctor` before anything else: an entry agy cannot
parse grants nothing, and before agy 1.1.11 it granted everything. You cannot see that file, so `--yolo` stays the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This restates the pre-1.1.11 "granted everything" consequence for any unparseable permissions.allow entry, not just the command(...)-naming-no-command (zerowords) class. scripts/doctor.sh's bad_allow_rules deliberately does not do this — per its own comment and the commit history on this PR ("Both reviewers on #56 caught the first cut printing the pre-1.1.11 … consequence for every flagged entry … A mistyped write_file(<dir>) never had it"), only a command(...) rule tokenizing to zero words carries that history. write_file(<dir>) — the exact placeholder shipped throughout the docs — is a different matcher and was never shown to match-everything pre-1.1.11.

README.md, docs/TROUBLESHOOTING.md, docs/POC-PLAYBOOK.md and skills/antigravity/SKILL.md all scope this correctly ("an entry that tokenized to zero command words … matched every command"), but this file still makes the generic claim. Since this is the guidance the delegate subagent itself reads before telling a user their rule "granted everything," it reintroduces the exact overreach the linked commit says was fixed.

Same issue at scripts/agy-delegate.sh:497 (the exit-15 message users actually see).

Comment thread scripts/agy-delegate.sh Outdated
shopt -u nocasematch
[ -s "$ERR" ] && cat "$ERR" >&2
echo "agy-delegate: agy soft-denied a tool that needs permission (headless can't prompt) — no work was done. For a FILE WRITE, the narrower fix is a permissions.allow rule covering the target in ~/.gemini/antigravity-cli/settings.json — write_file(<dir>) matches recursively beneath <dir> — which needs no flag; --yolo also works but auto-approves ALL tools. Other tools (web / Vertex AI Search / terminal) need --yolo unless a rule covers them. agy's own message above names the specific permission it wanted. (agy >= 1.1.3)" >&2
echo "agy-delegate: agy soft-denied a tool that needs permission (headless can't prompt) — no work was done. For a FILE WRITE, the narrower fix is a permissions.allow rule covering the target in ~/.gemini/antigravity-cli/settings.json — write_file(<dir>) matches recursively beneath <dir> — which needs no flag; --yolo also works but auto-approves ALL tools. Other tools (web / Vertex AI Search / terminal) need --yolo unless a rule covers them. agy's own message above names the specific permission it wanted. If a rule is ALREADY in that file and you are still reading this, suspect the rule: run agy-doctor, because an entry agy cannot parse grants nothing (and before agy 1.1.11 granted everything). (agy >= 1.1.3)" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same over-generalization as agents/antigravity-delegate.md:92: "an entry agy cannot parse … before agy 1.1.11 granted everything" applies the match-everything history to any unparseable entry, but doctor.sh's bad_allow_rules only attributes it to the zerowords class (a command(...) rule naming no command). A mistyped write_file(<dir>) — the placeholder this very message tells the user to use — is a different matcher and, per the doctor.sh comment, "never had it." This is the exact scoping bug the commit history says both reviewers already caught and fixed in doctor.sh's own output; it just didn't get propagated to this string, which is what a user actually sees at the point of an exit-15 failure.

Comment thread scripts/agy-delegate.sh Outdated
shopt -u nocasematch
[ -s "$ERR" ] && cat "$ERR" >&2
echo "agy-delegate: agy soft-denied a tool that needs permission (headless can't prompt) — no work was done. For a FILE WRITE, the narrower fix is a permissions.allow rule covering the target in ~/.gemini/antigravity-cli/settings.json — write_file(<dir>) matches recursively beneath <dir> — which needs no flag; --yolo also works but auto-approves ALL tools. Other tools (web / Vertex AI Search / terminal) need --yolo unless a rule covers them. agy's own message above names the specific permission it wanted. (agy >= 1.1.3)" >&2
echo "agy-delegate: agy soft-denied a tool that needs permission (headless can't prompt) — no work was done. For a FILE WRITE, the narrower fix is a permissions.allow rule covering the target in ~/.gemini/antigravity-cli/settings.json — write_file(<dir>) matches recursively beneath <dir> — which needs no flag; --yolo also works but auto-approves ALL tools. Other tools (web / Vertex AI Search / terminal) need --yolo unless a rule covers them. agy's own message above names the specific permission it wanted. If a rule is ALREADY in that file and you are still reading this, suspect the rule: run agy-doctor, because an entry agy cannot parse grants nothing (and before agy 1.1.11 granted everything). (agy >= 1.1.3)" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition here, but it lands in only one of the two write-grant messages in this file. The proactive nudge at line 256 ("this looks like a write task and --yolo is not set...") recommends the exact same write_file(<dir>) rule and is fired before the run — arguably the more natural place to plant "if a rule is already there and it's still not working, run agy-doctor" — but it wasn't touched.

The CHANGELOG entry for this release explicitly scopes the "suspect the rule" caveat to three places (this exit-15 message, agents/antigravity-delegate.md, commands/delegate.md), so this looks like an intentional-but-incomplete propagation rather than an oversight — worth either adding the same one-liner to the line-256 nudge, or confirming the omission is deliberate (e.g. because that message fires pre-emptively and doctor can't have run yet).

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (doctor.sh's new --model probe + permissions.allow validation, the associated test/doc changes, and the six follow-up commits already fixing prior review findings on this same PR).

Verified by manual trace (sandbox here couldn't execute python3/heredocs, so this is careful reading, not execution — CI's test run is the actual verification):

  • PLACEHOLDER regex (<[A-Za-z0-9_./\-]+>) correctly rejects command(sort < in > out) (space right after < breaks the match) and command(echo hi > /tmp/f) (no < at all), while still catching write_file(<dir>) and write_file(<path/to/repo>) — matches all the cases pinned in run-tests.sh.
  • The elif case ... esac ; then construct in doctor.sh's new §2c (bidirectional slug/display-name match) is unusual but syntactically valid bash — nested case/esac closes correctly before the outer one.
  • zerowords vs unparseable classification (empty ()/command() → zerowords; empty write_file() → unparseable; comment-only command(# ...) → zerowords via shlex comment handling) matches upstream's stated examples and the new tests.
  • No changes to the exit-code contract (2/3/10-15, AGY_SIGNAL/AGY_USAGE) — only message text in agy-delegate.sh changed, and hooks/validate-delegate-bash.sh (the security gate) isn't touched by this PR at all.
  • plugin.json and SKILL.md version fields are both bumped to 0.22.5, consistently.

One finding, left inline: the exit-15 soft-deny message in scripts/agy-delegate.sh (line 497) got the new "if a rule is already there, run agy-doctor" caveat, but the proactive write-task nudge earlier in the same file (line 256, fired before the run when --yolo is unset) recommends the identical write_file(<dir>) rule and didn't get it. The CHANGELOG scopes this release's fix to exactly three places (the exit-15 message + two .md files), so the line-256 message looks like a missed instance of the same "recommended in N places, fix N-1 of them" pattern this PR spent several commits closing elsewhere (worth either propagating the one-liner there too, or confirming the gap is deliberate).

Everything else — the version-gated --model probe, the placeholder/redirect edge cases, the helper-order static checker, and the sed | grep -q → process-substitution SIGPIPE fix — looks correct and is exercised by the new tests. No other doc-drift or exit-code inconsistencies found across README/SKILL.md/TROUBLESHOOTING/POC-PLAYBOOK/agents/commands.

Review: the exit-15 message got the 'run agy-doctor if a rule is already
there' caveat, but the proactive nudge at line 256 recommends the same
write_file(<dir>) rule and did not — the same 'fix N-1 of N places'
pattern this PR spent commits closing.

It gets the short half only. That message fires on every write-looking
task without --yolo, so it says what prevents the typo at the moment
someone would make it: <dir> is a placeholder, agy-doctor will say
whether yours parses. The diagnosis stays where the failure is.
Comment thread README.md Outdated
Comment on lines +205 to +211
**`<dir>` is a placeholder — substitute a real path.** A rule agy cannot parse says
nothing in either direction: from **agy 1.1.11** it matches nothing, so the grant you
think you have is absent and the write is soft-denied for no visible reason; **before
1.1.11** an entry that tokenized to zero command words (upstream's examples:
`command(time)`, a comment-only entry, `()`) matched **every** command and silently
auto-approved anything the agent ran — broader than the `--yolo` it was chosen instead
of. `agy-doctor` checks your entries and names which failure applies to your version.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is about the write_file(<dir>) placeholder specifically, but the "before 1.1.11 ... matched every command" consequence it describes is scoped to command(...)-shaped rules with zero words — per scripts/doctor.sh's own bad_allow_rules() (and the PR's second commit message: "A mistyped write_file(<dir>) never had it. Telling someone holding one that their config auto-approves everything is a security claim about a situation they are not in.").

bad_allow_rules() classifies an unsubstituted <dir> placeholder as "unparseable", not "zerowords" — only "zerowords" (the command(time)/comment-only/() examples) carries the match-everything history, and doctor.sh prints that sentence only when a zerowords entry is present. As written, this bullet tells a reader whose write_file(<dir>) was never substituted that pre-1.1.11 it "matched every command and silently auto-approved anything the agent ran" — which, per the code's own distinction, isn't true for that matcher.

Same overclaim is copy-pasted into docs/TROUBLESHOOTING.md:113-118, docs/POC-PLAYBOOK.md:106-111, skills/antigravity/SKILL.md:220-225, and (shorter form) agents/antigravity-delegate.md:89-92. commands/delegate.md avoids it and reads correctly.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the full diff (7 commits, doctor.sh probe + permissions.allow validation, plus the test-suite pipefail/hoisting fixes). Scope check: hooks/validate-delegate-bash.sh (the security gate) is untouched by this PR, and no exit-code/signal contract lines in agy-delegate.sh/agy-job.sh changed — only doctor.sh's diagnostic output and message copy. Version is bumped consistently in .claude-plugin/plugin.json and skills/antigravity/SKILL.md (0.22.5 in both).

scripts/doctor.sh's new --model probe (2c) and bad_allow_rules() are solid — the --model/display-name matching mirrors the existing model_present() comparison, the 1.1.11 version gate is correct (! ver_lt), the empty-answer branch draws no conclusion, and the zerowords vs unparseable classification correctly confines the "matches every command" history to command(...)-shaped rules only, as the commit 2 message says it should.

One finding, left as an inline comment on README.md:205-211: the doc prose in five places (README.md, docs/TROUBLESHOOTING.md, docs/POC-PLAYBOOK.md, skills/antigravity/SKILL.md, agents/antigravity-delegate.md) still makes the pre-1.1.11 "matched every command and silently auto-approved anything" claim while talking specifically about the write_file(<dir>) placeholder — the exact overclaim commit 2 fixed in doctor.sh's own output (and that both reviewers caught there). Per bad_allow_rules(), an unsubstituted write_file(<dir>) is classified unparseable, not zerowords, and never carried that history. The code fix didn't make it back into the docs it was copy-pasted from. commands/delegate.md is the one doc that avoids the overclaim.

Everything else — shell quoting/portability in the new doctor.sh code, the ZEROWORDS heredoc loop avoiding the subshell-variable-loss bug, the PLACEHOLDER regex's redirect-pair false-positive fix, check-helper-order.py's segment-based detection — looks correct and consistent with what the commit messages claim was fixed.

…thing

Review: the overclaim removed from doctor's output in commit 2 was still
in the prose it was copied from. Five documents put the placeholder
caveat next to the pre-1.1.11 match-everything history, which reads as
though a mistyped write_file(<dir>) carried it. Per bad_allow_rules it
does not: a placeholder is unparseable, not zerowords, and never had
that history.

Fixing the output and leaving the sentence that produced it just moves
the error somewhere a reviewer will not look again. All five now say the
placeholder grants nothing on any version, and name command(...)
explicitly for the part that is version-sensitive.
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff against origin/master (scripts/doctor.sh, scripts/agy-delegate.sh, tests/run-tests.sh, tests/check-helper-order.py, and the doc files). Focused on shell-logic correctness, the exit-code/signal contract, and doc/version consistency, since CI already covers shellcheck/tests/plugin-validate.

No blocking issues found. Specifics checked and no problems found:

  • bad_allow_rules() in doctor.sh: the PLACEHOLDER regex correctly rejects single (echo hi > /tmp/f) and paired (sort < in > out) literal redirects while still catching /<path/to/repo>-style placeholders -- traced by hand, matches the new test cases in run-tests.sh. zerowords vs unparseable classing is correctly scoped so the 'matched EVERY command' (pre-1.1.11) claim only prints for command(...)-shaped zero-word rules, never for a mistyped write_file() -- the exact bug flagged in earlier review rounds stays fixed.
  • The --model probe (2c in doctor.sh) is correctly nested inside the 'agy authenticated' block so FLASH/AGY_VER are always bound under set -uo pipefail; the 1.1.11 version gate and the empty-answer no-op branch both check out against the new probe_doctor tests.
  • Exit codes 2/3/10/11/12/13/14/15 are untouched by this PR (only messages/docs changed around exit 15) -- no drift between agy-delegate.sh, agy-job.sh, SKILL.md, TROUBLESHOOTING.md.
  • Doc claims cross-checked across all five places recommending write_file() (README, POC-PLAYBOOK, TROUBLESHOOTING, SKILL.md, agy-delegate.sh runtime strings) plus the two operational files (agents/antigravity-delegate.md, commands/delegate.md) -- all consistently say the placeholder grants nothing on any version and scope the 'matched everything' history to command(...) only. plugin.json and SKILL.md versions match (0.22.5).
  • check-helper-order.py's segment-splitting approach correctly handles elif/case-branch/brace-group call sites that the earlier context-alternation regex missed (verified against its own fixtures in run-tests.sh).
  • No changes to hooks/validate-delegate-bash.sh in this diff, so no new bypass surface there.

One non-blocking observation (pre-existing, not introduced by this PR, so not inline-commentable): run-tests.sh still has roughly 20 remaining positive-match 'printf | grep -q' assertions outside the 25 this PR converted to has() -- e.g. the tier-model-present checks around line 711-714 that read doctor.sh's full multi-section output, structurally the same shape as the 1.1.9 version-gate assertion this PR proved was flaky under load. Not confirmed to actually flake here, but it's the same construct class the PR spent several commits eliminating elsewhere in the same file -- worth a follow-up sweep if the concurrency issue resurfaces.

Nice work tightening the scope of the security-relevant claims across commits -- the class/reason separation in bad_allow_rules and the doc caveats are careful and consistent.

Review flagged these as a follow-up on the grounds that they are
positive matches, where SIGPIPE gives a noisy false FAIL. A scan says
otherwise: 17 are negative assertions. SIGPIPE is only possible when
grep -q matches and exits early, which for a negative assertion is
precisely the moment it is meant to fail — so they cannot flake in CI,
because they only break when they have a real bug to report.

grep ... <<<"$x" instead: a here-string has no writer process to kill,
the pipeline is one command, and -F/-i/-E/BRE semantics are untouched,
which a rewrite to case would not have preserved. Verified by mutation
on the plain and the -qE alternation forms.

The first pass truncated a grep pattern at a ) inside it and produced
valid shell that searched for the wrong string, hanging a polling loop.
Caught because the suite stopped completing.
Comment thread tests/run-tests.sh
}
# upstream's own example of a rule that tokenizes to zero command words: `time` is a
# shell reserved word that prefixes a command without being one.
allow_out="$(allow_doctor 1.1.11 '"command(time)"')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad_allow_rules() in scripts/doctor.sh (lines 154-159) explicitly handles a bare command() and any empty-body rule ("empty rule body", "zerowords" if name in ("", "command") else "unparseable"), and both the in-code comment and the CHANGELOG entry say the zero-command-word test set "follows upstream's own examples — command(time) ..., a comment-only entry, an empty compound ()". But this test block only exercises command(time), write_file(<dir>), command(echo hi > /tmp/f), command(sort < in > out), write_file(<path/to/repo>), plus a well-formed set. There's no allow_doctor case for a bare "command()"/"()" or a comment-only entry ("command(# nope)").

I traced the code manually and both cases do classify correctly (zerowords), so this isn't a live bug — but it is an untested path that the PR's own description claims is covered ("follows upstream's own examples"), which is exactly the kind of claim-vs-diff gap worth pinning down given this PR's own multiple rounds of catching exactly this class of gap.

Comment thread scripts/doctor.sh
# and so is a PAIR of them, command(sort < in > out), where everything between the two
# brackets is a filename rather than a template. Requiring an unbroken token separates
# <dir> and <path/to/repo> from both.
PLACEHOLDER = re.compile(r"<[A-Za-z0-9_./\-]+>")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor false-positive risk: PLACEHOLDER = re.compile(r"<[A-Za-z0-9_./\-]+>") is checked against the entire rule string, not just inside a command(...)/write_file(...) argument. It correctly avoids the two tested redirect shapes (echo hi > /tmp/f, sort < in > out) because those have whitespace or two unmatched brackets, but a legitimate single-token angle-bracket literal with no whitespace — e.g. command(grep -F '<TAG>' file.txt) — would still match the placeholder shape and get misclassified as "unsubstituted placeholder" even though it's real, working content. Not tested either direction. Given the PR's own stated bar ("a false positive here sends someone to edit a rule that was always fine"), this seems like a gap worth at least a comment acknowledging the trade-off, if not a test.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (docs/changelog claims cross-checked against code, not just read). Overall this is careful, well-tested work — the two-directional permissions.allow classification (zerowords vs unparseable), the 1.1.11 version gate on the --model probe, the <...> placeholder detection that correctly excludes literal redirects, and the check-helper-order.py hoisting guard all check out under manual trace. The bash constructs are unusual in places (elif case ... in ...; then, IFS="$(printf '\t')") but are valid on both bash 3.2 and modern bash, and I didn't find a portability break.

Two minor things worth a look (posted inline):

  1. Untested paths that the PR/CHANGELOG claims are covered. bad_allow_rules() handles a bare command()/() and a comment-only entry as zerowords (matches the code comments and the CHANGELOG's "follows upstream's own examples — command(time), a comment-only entry, an empty compound ()"), but tests/run-tests.sh's allow_doctor cases never exercise either shape. I traced the code by hand and both classify correctly, so it's not a live bug — just a claim the diff doesn't fully back with a test, which is the specific failure class this PR itself repeatedly caught and fixed elsewhere.

  2. PLACEHOLDER regex is a shape match, not a placeholder match. It correctly avoids the two tested redirect cases (echo hi > /tmp/f, sort < in > out), but a legitimate single-token angle-bracket literal with no whitespace (e.g. command(grep -F '<TAG>' file.txt)) would still false-positive as "unsubstituted placeholder." Given the PR is explicit that a false positive here is worse than a miss, this edge is untested in either direction.

Neither blocks — both are narrow, and the second is speculative (no evidence such a rule exists in practice). Didn't find anything in the exit-code/signal contract, the doc version numbers (0.22.5 matches in both plugin.json and SKILL.md), or the permissions.allow doc propagation (checked all ~10 mentions across README/SKILL/agents/commands/docs — the terse ones correctly defer to a detailed section that does carry the caveat) that looked wrong. hooks/validate-delegate-bash.sh (the security gate) is untouched by this PR, so no extra scrutiny needed there this round.

I could not execute the test suite in this sandbox (bash execution required approval that wasn't available), so the above is from static/manual trace, not a live run — CI's own run of tests/run-tests.sh is the authoritative check here.

Review: three shapes the CHANGELOG named as covered were exercised by
nothing (command(), a bare (), a comment-only rule), and write_file()
was described as not carrying the match-everything history with no test
saying so. All four pinned.

Also review: the placeholder test was a shape match, so a command rule
holding a real angle-bracketed literal would be misread. It now applies
only outside command(...), where shell syntax lives and where the
placeholder is never recommended. The cost is a placeholder inside a
command rule going unflagged — a miss, which this file prefers to a
false positive that sends someone to edit a working rule.

Writing that comment disabled the entire validator. A single quote
inside python3 -c '...' closes the shell string; bash parses the rest as
arguments and redirections, which stays valid shell, so bash -n and
shellcheck both pass and python runs a truncated program. With stderr on
/dev/null the caller reads 'nothing to report' — every negative
allow-rule test still passed, and only the positive ones caught it.

check-embedded-python.py now fails the suite on either signature: a body
ending on a comment line, or one that no longer compiles. Both verified
by mutation. The first attempt looked for the closing quote at the start
of a line and false-positived on nudge-delegation.sh, where it is at the
end of one.

245 -> 252.
@quorum-code-review

quorum-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Quorum review

Reviewing only what changed since 984b41d. Findings in files this range does not touch are carried over untouched — they were not re-examined, so they are neither re-reported nor treated as fixed.

gemini-3.6-flash, claude-sonnet-5 each read the diff without seeing the other's output (gemini-3.6-flash 0 and claude-sonnet-5 1), which merged to 1 distinct finding(s). 0 of those were reported by both models independently.
The remaining findings were each judged by the model that did not report them: 0 confirmed, 0 uncertain, 1 refuted.

Beyond the diff, the models made 20 read-only lookup(s) into the checkout at 11c06e4 and opened scripts/doctor.sh, tests/run-tests.sh.

Refuted by the other model (1)
  • New 'no reasonless finding' test cannot fail; it passes for free (tests/run-tests.sh:954) — The added test checks allow_out for lines matching ^ +[^ ]+ — *$ (a finding header with no reason text following the em-dash). If a reasonless finding line is present in the output, grep succeeds and executes the FAIL: branch, incrementing $FAIL. The test is a valid regression check that will fail if doctor output emits a finding without a reason.

No longer reported

Not raised by the last two reviews of these files. Two, rather than one, because models are not perfectly repeatable and a single scan disagreeing with the previous one is ordinary — closing on the first miss made findings flap. Still not a guarantee of a fix: they will reappear if a later review finds them again.

  • permissions.allow TSV report breaks if a rule string contains a tab/newline (scripts/doctor.sh)

Usage
Model Region Calls Input Cached input Output
gemini-3.6-flash global 16 143,822 44,069 600
claude-sonnet-5 global 6 100,967 11,724 16,157

Reviewed 1831e03 · quorum-review 1.7.0 · models gemini-3.6-flash, claude-sonnet-5 · 216s

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (doctor's --model probe, permissions.allow validation, and the test-suite hardening). The bulk of this PR is already the product of several review rounds against itself (visible in the commit log), and it shows — the bad_allow_rules classification logic, the placeholder-shape regex, the SIGPIPE-safe here-string conversions, and the helper-order static checker all held up under scrutiny (traced through the zerowords/unparseable split, the <...> placeholder boundary cases, set -uo pipefail interaction with the new agy_guard | head | cut probe pipeline, and the 1.1.11 version gating — no set -e, so pipefail there is inert by design, consistent with the rest of the file).

One real finding, a doc-drift regression of exactly the kind this PR spent several commits fixing elsewhere:

scripts/agy-delegate.sh:497 (the exit-15 message users actually see at runtime) still makes the overclaim that this PR's own doctor.sh, CHANGELOG, and all 5 markdown docs explicitly disclaim. The message reads:

"...run agy-doctor, because an entry agy cannot parse grants nothing (and before agy 1.1.11 granted everything)."

That parenthetical attaches "granted everything before 1.1.11" to any unparseable entry — but per bad_allow_rules() in scripts/doctor.sh, that history belongs only to the zerowords class (a command(...) rule naming no command). A mistyped write_file(<dir>) — the exact placeholder named earlier in this same message — is unparseable, not zerowords, and per the CHANGELOG's own words "never had it." agents/antigravity-delegate.md:92-93 and skills/antigravity/SKILL.md:222-225 both carry the corrected, scoped version of this same caveat ("do not attach that history to a mistyped write_file()"), and the CHANGELOG describes a dedicated fixup commit that swept "five documents" to remove exactly this conflation. scripts/agy-delegate.sh is a .sh file, not a "document," and appears to have been missed by that sweep — it still has the pre-fixup wording from the very first commit of this PR. No test pins the exact string, so this is a docs-only fix (scope the "(and before agy 1.1.11 granted everything)" parenthetical to the command(...)-naming-no-command case, as the other two files now do).

Nothing else rose to reportable confidence — the rest of the diff looks correct and consistent.

Comment thread scripts/agy-delegate.sh Outdated
shopt -u nocasematch
[ -s "$ERR" ] && cat "$ERR" >&2
echo "agy-delegate: agy soft-denied a tool that needs permission (headless can't prompt) — no work was done. For a FILE WRITE, the narrower fix is a permissions.allow rule covering the target in ~/.gemini/antigravity-cli/settings.json — write_file(<dir>) matches recursively beneath <dir> — which needs no flag; --yolo also works but auto-approves ALL tools. Other tools (web / Vertex AI Search / terminal) need --yolo unless a rule covers them. agy's own message above names the specific permission it wanted. (agy >= 1.1.3)" >&2
echo "agy-delegate: agy soft-denied a tool that needs permission (headless can't prompt) — no work was done. For a FILE WRITE, the narrower fix is a permissions.allow rule covering the target in ~/.gemini/antigravity-cli/settings.json — write_file(<dir>) matches recursively beneath <dir> — which needs no flag; --yolo also works but auto-approves ALL tools. Other tools (web / Vertex AI Search / terminal) need --yolo unless a rule covers them. agy's own message above names the specific permission it wanted. If a rule is ALREADY in that file and you are still reading this, suspect the rule: run agy-doctor, because an entry agy cannot parse grants nothing (and before agy 1.1.11 granted everything). (agy >= 1.1.3)" >&2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exit-15 message still says an unparseable permissions.allow entry "grants nothing (and before agy 1.1.11 granted everything)" — attaching the pre-1.1.11 "matched EVERY command" history to any parse failure, including the write_file(<dir>) placeholder named earlier in the same sentence.

Per bad_allow_rules() in scripts/doctor.sh (added in this PR), that history belongs only to the zerowords class (command(...) naming no command); a mistyped write_file() is unparseable and per the CHANGELOG "never had it." agents/antigravity-delegate.md:92-93 and skills/antigravity/SKILL.md:222-225 both carry the corrected, scoped wording ("do not attach that history to a mistyped write_file()"), and this PR's own fixup commit swept "five documents" to remove exactly this conflation — this .sh file (not a "document") looks like it was missed and still has the pre-fixup phrasing from the first commit.

No test pins this exact string, so scoping the parenthetical to the command(...)-naming-no-command case (like the two docs above) should be safe.

Comment thread scripts/doctor.sh Outdated
Review: the permissions.allow report is tab-separated and a rule is
user-supplied JSON. An entry containing a tab shifted every field after
it, and the field that moves is the class — the one that decides whether
the security consequence prints. A newline split one finding across two
lines; the reader dropped the orphan but the count still counted it, so
the header promised more entries than it named. Class goes first now and
the entry is escaped.

Review: the exit-15 message still said 'an entry agy cannot parse grants
nothing (and before agy 1.1.11 granted everything)', handing the
command-rule history to the write_file(<dir>) placeholder it names two
sentences earlier. The sweep that fixed this in five .md files did not
look at .sh. Scoped, and pinned by a test.

Three assertions, each verified against the unfixed code. The first
newline test asserted an empty reason and mutation showed the reader
already guards that, so it was pinning nothing; it now compares the
header's count against the findings it lists.

252 -> 255.
Comment thread tests/run-tests.sh Outdated
else echo "FAIL: a tab in the rule text dropped the zero-command-word consequence"; FAIL=$((FAIL+1)); fi
# A newline splits one finding across two lines. The reader drops the orphan, so nothing
# looks wrong on the findings themselves — but the COUNT is taken from the line count, so
# the header promises more entries than it goes on to name. Assert the two agree; the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New newline-escaping test can't distinguish fixed from fully-reverted doctor.sh

The test builds a single allow-rule entry containing an embedded literal newline (write_file(<dir>)\nwrite_file(/x)), runs doctor, and asserts the header count (claimed) equals the number of printed finding lines (listed). Tracing through the original (pre-PR) bad_allow_rules/reader pair — i.e. reverting both the field order (rule why cls) and the esc() call together, which is what "the unfixed doctor" means for a plain revert of scripts/doctor.sh — the embedded newline splits Python's single print() call into two physical stdout lines. Under the OLD field order, the first (orphan) line has no tabs, so its entire content lands in $rule (the first read variable), which is non-empty, so the [ -n "$rule" ] || continue guard does NOT skip it: the orphan is still printed as its own finding. This makes grep -c . over BAD_RULES (2 lines) equal the number of printed " — " lines (2), so claimed == listed holds and the test reports "ok" even against the fully-reverted, buggy doctor.sh. The test only becomes red when the field order is fixed (cls why rule) but esc() is removed in isolation — because then the orphan's bare text lands in $cls instead of $rule, making $rule empty and triggering the continue that actually drops it. So this assertion validates a narrower mutation (missing esc() while the field-order fix is present) rather than the count-mismatch bug as it exists in the unmodified base code, weakening its value as a regression guard for a plain revert of doctor.sh.

Second opinion from gemini-3.6-flash

claude-sonnet-5 raised this. gemini-3.6-flash was then asked to judge it — without being shown the reasoning, the severity, or who reported it, so that it would assess the code rather than agree with a colleague. Its answer:

When doctor.sh does not escape newlines, an entry containing a newline splits into two lines in BAD_RULES. The header counts lines with grep -c . and claims 2 entries, while the output loop formats both lines with , resulting in listed=2. Because claimed and listed are both 2, [ "${claimed:-0}" = "$listed" ] evaluates to true and the test passes even on unescaped/reverted code.

reliability · id adac3e8ad0bb77b1

Comment thread scripts/doctor.sh Outdated
}

# Report `permissions.allow` entries agy cannot use as written. Prints one
# TAB-separated `<entry>\t<reason>\t<class>` line each and returns 0 when any were found.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this docstring says the emitted line shape is <entry>\t<reason>\t<class>, but the actual print (line ~194) emits "%s\t%s\t%s" % (cls, why, esc(t)) — class first, then reason, then entry. The reader (while IFS=$'\t' read -r cls why rule, line ~356) correctly matches the real order. Worth updating the comment to <class>\t<reason>\t<entry> so it doesn't mislead the next person who touches this format (which the PR itself flags as security-sensitive: a shifted field silently drops the security consequence).

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (docs, scripts/doctor.sh's new bad_allow_rules/--model probe, scripts/agy-delegate.sh message tweaks, and the test-suite hardening in tests/). hooks/validate-delegate-bash.sh (the security gate) is untouched by this PR.

Verified, not just read:

  • bad_allow_rules()'s classification logic traced by hand against all the test fixtures (command(time), (), command(), comment-only, write_file(), the placeholder shapes, the redirect-pair false-positive guard, the tab/newline-escaping fix) — the class/reason/entry protocol and the esc() escaping correctly prevent the field-shift and count-inflation bugs the CHANGELOG describes, and the tab/newline regression tests do fail against the pre-escaping shape (confirmed by tracing what the reader does with an unescaped embedded newline).
  • The 1.1.11 --model probe gate (ver_lt boundary, norm_model either-direction match, the empty-answer no-verdict branch) is correctly wired and the probe_doctor stub test harness exercises the right argv shape.
  • The 8 write_file(<dir>) recommendation sites (README, SKILL.md, POC-PLAYBOOK, TROUBLESHOOTING, commands/delegate.md, agents/antigravity-delegate.md, and both messages in agy-delegate.sh) now consistently state the placeholder-grants-nothing / command(...)-is-the-version-sensitive-one distinction — no drift found across them.
  • check-helper-order.py's segment-splitting correctly handles the elif/case-branch/brace-group cases its own tests claim, and doesn't false-positive on the quoted-mention case or on comment lines.
  • Exit-15 / AGY_SIGNAL/AGY_USAGE contract is untouched by this PR (only the stderr prose around it changed) — no drift between agy-delegate.sh and the docs.
  • plugin.json / SKILL.md version bump (0.22.4 → 0.22.5) is in sync.

One nit (posted inline on scripts/doctor.sh:96): the bad_allow_rules() docstring says the emitted format is <entry>\t<reason>\t<class>, but the actual code prints class-first (<class>\t<reason>\t<entry>), matching the reader. Harmless since the reader already uses the real order, but worth fixing since this is the exact kind of field that, per the function's own comment, is security-sensitive if misread.

Nothing else stood out. This is an unusually well-tested PR for a docs/diagnostics change — the new tests are traceable to specific fixed defects (SIGPIPE false-negative, hoisting false-negative, quote-truncation false-negative) rather than being decorative.

Reviewers: the newline test could not tell the fixed doctor from a fully
reverted one. Correct, and the reason is a mistake I made one commit
earlier — I dropped the empty-reason assertion after mutating only the
escaping, concluded it pinned nothing, and never mutated the ordering.

The two orderings fail differently and one assertion cannot see both.
Entry last: the orphan line carries no rule text, the reader drops it,
and only the count is wrong. Entry first: the orphan keeps rule text and
prints as a finding with no reason at all.

Both assertions are in now, each verified against the mutation the other
misses. Also fixed the bad_allow_rules docstring, which still described
the old entry-first field order that the reader no longer uses — the
exact field the function's own comment calls security-sensitive if
misread.

255 -> 256.
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewed the diff (doctor.sh model-probe + permissions.allow validator, the two new static checkers, the delegate.sh message tweaks, and the doc sweep). This sits on top of an already extensive self-review cycle visible in the commit history (helper-order bug, SIGPIPE-under-pipefail, TSV field-order, placeholder-shape false positives, embedded-python truncation) — traced through each fix rather than re-deriving it, and they hold up.

bad_allow_rules (doctor.sh): walked the classifier by hand against every shape the tests exercise — command(time), bare (), comment-only, write_file(), the

/<path/to/repo>/ placeholders, and the command(echo hi > /tmp/f) / command(sort < in > out) / command(grep -F file.txt) negative cases. The class/reason/entry split and TSV escaping are correct — a rule containing a literal tab or newline can no longer shift the class field or split into an orphan line.

ver_lt: real per-segment numeric comparison, so 1.1.9 vs 1.1.11 resolves correctly on the third segment; consistent across all three call sites (--tier gate, --model probe gate, zero-words version-sensitivity gate), all guarded by the same unparseable-version pattern.

--model probe (2c): gated on agy >= 1.1.11 before ever invoking -p /model; an empty reply produces no verdict either way, matching the changelog's 'not evidence of breakage' claim. The bidirectional substring match is consistent with the pre-existing model_present helper.

Test-suite <<< / <() conversions: here-strings and process substitution are both plain bash builtins, fine for the bash-3.2-compatible target these scripts already assume.

Doc sweep: grepped write_file(

) across every .md/.sh that recommends it (README, SKILL.md, POC-PLAYBOOK, TROUBLESHOOTING, agents/antigravity-delegate.md, commands/delegate.md, both agy-delegate.sh nudges) — all 8 sites carry the placeholder caveat now, and none still attach the pre-1.1.11 match-everything history to a mistyped write_file() (that history stays scoped to command(...) naming no command). plugin.json and SKILL.md both read 0.22.5.

One caveat: the claims about real agy 1.1.11 CLI behavior (-p /model as a zero-token, no-turn slash command; falling through as literal prompt text below 1.1.11) rest on the PR's own testing against a live agy binary, which I don't have access to here. The description is explicit this was checked rather than assumed, so I'm not flagging it as a defect — just noting I verified the shell/python logic and test coverage, not the upstream CLI itself.

No findings to raise.

@yuting0624
yuting0624 merged commit adc33d6 into master Aug 10, 2026
7 checks passed
@yuting0624
yuting0624 deleted the feat/doctor-probe-and-allow-rules branch August 10, 2026 12:50
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