Skip to content

fix(procedures): make a bad query loud, and bound the scout's toolset - #23

Merged
drewdrewthis merged 2 commits into
mainfrom
fix/scout-cost-and-query-correctness
Aug 7, 2026
Merged

fix(procedures): make a bad query loud, and bound the scout's toolset#23
drewdrewthis merged 2 commits into
mainfrom
fix/scout-cost-and-query-correctness

Conversation

@drewdrewthis

@drewdrewthis drewdrewthis commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What

Two silent-wrong-answer defects in query-records.sh, plus a tools: allowlist on procedure-scout.

Closes #21.

Why

This came out of pricing /how-do-i and /am-i-done against local agent transcripts. The headline cost finding is not in this PR — it is the tier leak in #22 (~66% of the priced cost of /how-do-i, no flow change). What is here is the correctness half: the scout's query tool could return a confidently wrong answer, fast, with exit 0.

For a discovery tool that is the worst failure shape — the same reasoning behind the existing exit 3 scan-error path, which already says "NOT 'no matches'" out loud.

Changes

scripts/query-records.sh + scripts/lib/record-match.awk + scripts/lib/record-rarity.awk

  1. 2-char terms were unmatchable — --keyword pr could never work. The 3-char token floor was applied in three places (query tokenizer, record-match.awk:159, record-rarity.awk:75), so it dropped short tokens from the query and from every record's keywords. pr occupies 135 keyword slots in the live corpus, ci 29, gh 16; pr-review tokenizes to {pr, review} and lost its pr half too. The scout could not find PR procedures. The floor is now a min_tok parameter threaded through all three sites: the PULL path passes 2, every other caller defaults to 3, so the PUSH router is unchanged. Same split the file already makes with gate=0 — an explicit --keyword is a term the caller chose, not prompt noise. record-rarity and record-match must receive the same value or df/idf is computed over a different token set than the one scored.
  2. Repeated value-taking flag silently kept the last. --keyword a --keyword b returned b-only results at full speed, no warning. Now exit 2, with the working idiom in the message — several terms belong in ONE --keyword string, where they tokenize into a ranked OR union. Applied to all seven value-taking flags; --full is exempt (idempotent, takes no value).
  3. All-tokens-dropped returned a silent empty. A --keyword whose tokens all fell under the 3-char floor produced an empty token set, empty stdout, exit 0 — indistinguishable from "searched, found nothing" when no search had run. Hits real terms: ci, pr, gh, db. Now exit 2, naming the floor and the rejected input.
  4. The floor is the named MIN_TOKEN_LEN rather than an inlined 3, and the exit-code contract is documented in the header: 0 = ran, empty means genuine miss · 2 = usage error, never searched · 3 = scan broke.

agents/work-reviewer.md — added tools: Read, Grep, Glob. Its Boundaries say "Never re-run the work, read the diff, or verify independently", and it was measured doing 1.2 Bash calls per invocation — boundary violations. Withholding Bash closes the re-run vector in the harness rather than in prose.

agents/procedure-scout.md — added tools: Bash, Read, Grep, Glob. There was no tools: key at all, so a subagent briefed read-only inherited the full toolset. That is the mechanism behind three logged incidents of read-only forks writing to GitHub.

Allowlist checked against the agent's own steps: every command in Steps 2–4 (query-records.sh, the grep -rn keyword fallback, the awk 'FNR==1{...}' batch read, the grep -icE/grep -iE | tail sweep) runs under Bash. Read/Grep/Glob are read-only spares. Nothing in the documented flow needs a tool outside the list, so this cannot silently degrade a lookup.

This narrows the surface; it does not close the hole. Bash still reaches gh, git and rm. The Invariants section now says so explicitly, so nobody reads the allowlist as enforcement. A real closure needs Bash-command-level filtering, which is out of scope here.

Human verification

proven — 6 tests added, and the suite is not vacuous. 3 fail on the parent commit and pass here; 3 are regression guards that must pass on both sides.

Against origin/main (4cb4071):

not ok 35 repeating --keyword is refused instead of silently keeping the last
not ok 36 repeating a non-keyword flag is also refused
ok  37 repeating --full is allowed (idempotent, takes no value)
not ok 38 a keyword with no token of 3+ chars exits non-zero, not a silent miss
ok  39 a genuine miss still exits 0 with empty stdout
ok  40 multiple terms in ONE --keyword string still union

On this branch, all 6 pass. Full suite, measured in two separate worktrees:

BASELINE on origin/main -> PASS: 145   FAIL: 0
THIS BRANCH             -> PASS: 154   FAIL: 0

proven — behaviour, run against a throwaway fixture corpus (QUERY_RECORDS_ROOT) so the transcript carries no real record content:

$ bash scripts/query-records.sh --keyword quokkadec --keyword fixpattern
query-records: --keyword given more than once — each flag may appear at most once.
query-records: to search several terms, put them in ONE string: --keyword "term1 term2" (ranked OR union).
exit=2

$ bash scripts/query-records.sh --keyword "a b"
query-records: --keyword "a b" has no token of 2+ characters — nothing to search.
query-records: this is NOT 'no matches'. Tokens shorter than 2 characters are dropped; use a longer term.
exit=2

$ bash scripts/query-records.sh --keyword pr | head -4    # against the LIVE corpus
references/research/boxd-pr-preview-workflow-2026-04-21.md — Per-PR Boxd preview VMs — feasibility investigation
references/procedures/review-qa/pr-review/PROCEDURE.md — Steps
references/procedures/review-qa/review/PROCEDURE.md — The reviewers and personas below all apply the shared review methodology ...
references/solutions/2026-06-04-gh-pr-edit-projects-classic-failure.md — `gh pr edit --body` fails on projects-classic repos → REST PATCH bypass

$ bash scripts/query-records.sh --keyword "quokkadec fixpattern"     # the correct idiom
references/decisions/sample-decision.md — Sample decision record
references/solutions/sample-solution.md — Sample solution record
exit=0

$ bash scripts/query-records.sh --keyword zzzznonexistentquux        # genuine miss, unchanged
exit=0

missing — the procedure-scout.md change has no automated coverage. Nothing under hooks/tests asserts an agent file's frontmatter, so the 151-pass figure speaks only to the script half. The allowlist-vs-steps check above is a paper review, not a test. I have also not observed a scout dispatched under the allowlist — tools: is applied by the harness at dispatch, and nothing in this repo exercises it.

missing — the repo-local reviewer agents (principles-reviewer, hygiene-reviewer, security-reviewer, test-reviewer) were not run; this session is under a standing instruction not to dispatch subagents unprompted. CONTRIBUTING asks for all four on a script change, so this is a known policy gap on this PR, not a waiver.

What I dropped, and why

The original plan also collapsed the scout's Read fan-out into query-records.sh --full. Dropped — the premise was stale.

The transcripts I measured end 2026-08-07T10:58:46Z; PR #9 merged at 11:45:59Z. So the 3.8 Reads/invocation I measured is entirely pre-#9 behaviour, and #9 already batches those into one awk 'FNR==1{...}' call. Worse, Step 3 now deliberately says not to use --full on a broad survey, for a reason that still holds — dumping before selecting wastes context on records a gloss would have excluded. Overriding a rule that had just landed, using numbers taken before it landed, would have been a regression dressed as an optimisation.

Everything measured about the scout's cost predates #9 and should be re-measured before anyone acts on it.

Vendoring note

query-records.sh is declared vendored from the codex repo, but the copies have already diverged substantially — the upstream copy lacks --limit, --full, --rel-ratio and --k-floor. procedure-scout.md has no upstream copy any more.

These changes are bug fixes, not hosting adaptations, so they are not marked # PLUGIN ADAPTATION — that marker is for divergence required by the plugin's hosting. Whether they should also land upstream is the open question in #19; this PR does not decide it.

Review round 2 (f77e3b2)

CodeRabbit's PLUGIN ADAPTATION finding was correct and is addressed at all five diverging sites. I had argued in this body that these were bug fixes and therefore exempt — wrong; the rule is about behavioral divergence from upstream, which they are regardless of motive.

Re-reviewing also surfaced that the first pass shipped a worse bug than it fixed (the pr case above). Turning a silent empty into a hard exit 2 made a real gap louder while leaving the lookup equally impossible. That is now fixed properly; 4 tests cover it and all 4 fail on the parent commit.

On the am-i-done fork cost — what this PR can and cannot reach

/am-i-done is 69% fork-boot overhead (~30,145 cache-write tokens per invocation, for 2.8 turns of work). Measured composition of that boot context:

Component ~tokens share of boot Owner
work-reviewer.md + am-i-done/SKILL.md 1,848 6.1% this repo
host CLAUDE.md import chain + memory index 7,934 26.3% host codex
harness system prompt + tool schemas ~20,363 ~67.6% harness / tools:

Only 6.1% of it is plugin-owned. The tools: allowlists here attack the schema share; the import-chain share is host-side and filed separately (orchard-codex#303). No change confined to this repo can move the 69% materially, and claiming otherwise would be the same overreach as the pr fix.

query-records.sh had two silent-wrong-answer shapes. Both exited 0 with
plausible output, so a caller could not tell a broken query from a genuine
miss — the worst failure mode for a discovery tool, and the same one the
existing `exit 3` scan-error path was added to prevent.

1. Repeating a value-taking flag silently kept the last occurrence.
   `--keyword recall --keyword gitflow` returned gitflow-only results at full
   speed with no warning. Now exit 2, with the working idiom in the message
   (several terms belong in ONE --keyword string — they union).

2. A --keyword whose tokens all fell under the 3-char floor produced an empty
   token set, empty stdout and exit 0 — indistinguishable from "searched,
   found nothing", when no search had run at all. Hits real terms: ci, pr,
   gh, db. Now exit 2 naming the floor and the dropped input.

The token floor is now the named MIN_TOKEN_LEN rather than an inlined 3, and
the exit-code contract is documented in the header so a caller can tell
"found nothing" (0) from "never searched" (2) from "scan broke" (3).

procedure-scout carried no `tools:` key at all, so a subagent briefed
read-only inherited the full toolset — the mechanism behind three logged
incidents of read-only forks writing to GitHub. Allowlist Bash/Read/Grep/Glob
and state plainly in Invariants that Bash still reaches gh/git/rm, so
read-only remains a rule the agent keeps rather than one the harness enforces.

Tests: 6 added. 3 fail on the parent commit and pass here (repeat-flag x2,
short-token); 3 are regression guards that must pass both sides (--full stays
idempotent, a genuine miss stays exit 0 + empty, one-string multi-term still
unions). Suite 145 -> 151, 0 failures.

Closes #21

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drewdrewthis drewdrewthis self-assigned this Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@drewdrewthis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 225539ee-750c-46bb-aa1f-c04e32ae4a30

📥 Commits

Reviewing files that changed from the base of the PR and between 2608616 and f77e3b2.

📒 Files selected for processing (5)
  • plugins/procedures/agents/work-reviewer.md
  • plugins/procedures/hooks/tests/query-records.bats
  • plugins/procedures/scripts/lib/record-match.awk
  • plugins/procedures/scripts/lib/record-rarity.awk
  • plugins/procedures/scripts/query-records.sh
📝 Walkthrough

Walkthrough

The PR validates repeated query flags, distinguishes invalid keyword input from genuine misses, adds regression tests, and updates the procedure scout’s tool allowlist and read-only rule.

Changes

Query validation

Layer / File(s) Summary
Query contract and validation
plugins/procedures/scripts/query-records.sh
The script documents flag and exit-code rules, rejects repeated value-taking flags, allows repeated --full, and rejects keyword queries with no valid tokens.
Query regression coverage
plugins/procedures/hooks/tests/query-records.bats
Tests cover repeated flags, short-token errors, genuine unmatched queries, and multi-term keyword unions.

Scout tool policy

Layer / File(s) Summary
Scout read-only policy
plugins/procedures/agents/procedure-scout.md
The scout declares Bash, Read, Grep, and Glob tools. Its read-only rule identifies mutation risks from Bash commands such as gh, git, and rm.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The query changes satisfy AC-1 through AC-5, but no change reconciles or deletes ~/.claude/scripts/query-records.sh required by AC-6 [#21]. Reconcile or delete the duplicate ~/.claude/scripts/query-records.sh, then add evidence that AC-6 is satisfied.
Out of Scope Changes check ⚠️ Warning The scout tool allowlist is unrelated to the query-records defects and requirements in issue #21. Move the scout toolset change to a separate PR, or link an issue that explicitly requires the allowlist.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both main changes: stricter query validation and a bounded toolset for the procedure scout.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/scout-cost-and-query-correctness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@drewdrewthis
drewdrewthis marked this pull request as ready for review August 7, 2026 12:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/procedures/scripts/query-records.sh`:
- Around line 110-139: Add a literal “# PLUGIN ADAPTATION: <why>” comment
immediately before each affected divergence: the repeated-flag validation in
plugins/procedures/scripts/query-records.sh lines 110-139, the all-short-token
error path in plugins/procedures/scripts/query-records.sh lines 204-213, and the
regression-test block in plugins/procedures/hooks/tests/query-records.bats lines
575-615. Explain the plugin-specific reason for each adaptation without changing
the behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5fb43316-dbb0-46dc-8a47-36f920ad64fb

📥 Commits

Reviewing files that changed from the base of the PR and between 4cb4071 and 2608616.

📒 Files selected for processing (3)
  • plugins/procedures/agents/procedure-scout.md
  • plugins/procedures/hooks/tests/query-records.bats
  • plugins/procedures/scripts/query-records.sh

Comment thread plugins/procedures/scripts/query-records.sh
…d work-reviewer

Review found the first pass shipped a worse bug than the one it fixed.

`--keyword pr` could never work. The 3-char token floor was applied in three
places — the query tokenizer, record-match.awk:159 and record-rarity.awk:75 —
so `pr` (135 keyword slots in the live corpus), `ci` (29) and `gh` (16) were
unmatchable from both sides. `pr-review` tokenizes to {pr, review} and the
`pr` half was dropped off the record too. The previous commit turned that
silent empty into a hard exit 2, which made a real gap louder while leaving
the lookup just as impossible: the scout could not find PR procedures at all.

The floor is now a `min_tok` parameter threaded through all three sites. The
PULL path passes 2; every other caller defaults to 3, so the PUSH router is
untouched. This is the same split the file already makes with `gate=0` — an
explicit --keyword is a term the caller chose, not prompt noise to filter.
record-rarity and record-match must be given the same value or df/idf is
computed over a different token set than the one scored; both now are.

Marked the three diverging sections `# PLUGIN ADAPTATION` per the repo's
vendoring rule (CodeRabbit, PR #23).

work-reviewer had no `tools:` key, so an agent whose Boundaries say "never
re-run the work, read the diff, or verify independently" could do all three.
Measured: 1.2 Bash calls per invocation — boundary violations. Allowlist
Read/Grep/Glob and withhold Bash, closing the re-run vector in the harness
rather than in prose.

Tests: 3 added (2-char match, 2-char inside a hyphenated keyword, 1-char
still dropped); the short-token error test retargeted to a 1-char input. All
4 fail on the parent commit. Suite 151 -> 154, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drewdrewthis

Copy link
Copy Markdown
Owner Author

Addressed in f77e3b2.

PLUGIN ADAPTATION markers — added at all three sites CodeRabbit named: the repeated-flag validation, the all-short-token error path, and the regression-test block. Two more went in with this push (record-match.awk / record-rarity.awk min_tok). I had argued in the PR body that these were bug fixes rather than hosting adaptations and therefore exempt; that was wrong — the rule is about behavioral divergence from upstream, which these are regardless of motive.

A worse bug found while re-reviewing. The first pass made --keyword pr exit 2. That is not a fix — the scout needs to find PR procedures, and pr occupies 135 keyword slots in the live corpus (ci 29, gh 16). The 3-char floor was applied in three places, so those terms were unmatchable from both the query side and the record side; pr-review tokenizes to {pr, review} and lost its pr half too. Turning a silent empty into a loud error left the lookup equally impossible.

The floor is now a min_tok parameter through all three sites. PULL passes 2; every other caller defaults to 3, so the PUSH router is unchanged. Same split this file already makes with gate=0.

work-reviewer now carries tools: Read, Grep, Glob. Its Boundaries say "never re-run the work, read the diff, or verify independently" — it was measured doing 1.2 Bash calls per invocation. Withholding Bash closes that in the harness instead of in prose.

@drewdrewthis

Copy link
Copy Markdown
Owner Author

"No other optimizations?" — the full survey, with verdicts

Everything considered, and why it is or is not in this PR. Rejections are as load-bearing as the changes.

# Lever Size Verdict
1 Tier leakprocedure-scout declares model: sonnet, ~35% of spend ran on Opus ~66% of /how-do-i's priced cost Not here. Biggest lever by far, but the frontmatter is already correct — there is nothing in this repo to edit. #22 AC-1 is a disconfirming check that must run before any change.
2 Fork boot: the CLAUDE.md import chain every fork inherits ~7,934 tok = 26.3% of boot Not here — host-side. orchard-codex#303.
3 tools: allowlists on both gate agents attacks the ~67.6% tool-schema/harness share In this PR. Unmeasured — no before/after cache_write.
4 --full to collapse the scout's Read fan-out ~44% of ingested volume, pre-#9 Rejected. PR #9 already batches reads into one awk call, and Step 3 now deliberately forbids --full on a broad survey. My measurement predates #9 (transcripts end 10:58Z, #9 merged 11:45Z), so the premise was stale.
5 fm_value() per-file awk fork in query-records.sh ~2% of a 76s budget Already fixed by merged #5 (2.57s → 0.046s on --kind).
6 FTS5 index over the seven stores Rejected. The 38–583x recall benchmark ran on a corpus ~135x larger; the ratios do not transfer to 5.95MB / 784 files, where the script is already ~0.15s. Its only real value was collapsing shell calls, which #9 did.
7 Carry prior digests forward within a session scout ingests ~21,579 tok to return ~1,702 (12.7x) Filed, not built#24. The one genuinely un-filed idea. Note this is not memoizing "the gate ran": per-turn firing stays.
8 Skip-the-subagent fast path for unambiguous lookups Rejected by owner ruling (2026-08-03): the subagent exists to digest as a whole; the fork's cost is the price of context isolation.
9 Haiku tier for the scout Blocked on #22. If the model: declaration is not honoured, changing it is a no-op. Sequencing, not merit.
10 Per-gate off-switch Already in flight — PR #10 / #16, not mine.
11 No CI (#20) Not an optimization, but it gates trusting one. Every perf number on this repo — including this PR's 154-pass — is hand-run.

The honest summary: the two biggest levers (#22, #303) are both outside this PR's reach, and four candidates were rejected on evidence rather than shipped. What is here is the correctness fix plus one unmeasured cost change.

One caveat on this PR's own tools: work: the allowlists are on the branch but not in the installed plugin until merge, so nothing running today is exercising them.

$ git show HEAD:plugins/procedures/agents/work-reviewer.md | sed -n 1,5p
name: work-reviewer
...
model: sonnet
tools: Read, Grep, Glob

@drewdrewthis
drewdrewthis merged commit 6327b8c into main Aug 7, 2026
1 check passed
@drewdrewthis
drewdrewthis deleted the fix/scout-cost-and-query-correctness branch August 7, 2026 12:40
@drewdrewthis

Copy link
Copy Markdown
Owner Author

✅ Post-merge verification: the tools: allowlist DOES bind on the fork path

This PR added tools: to agents/procedure-scout.md and agents/work-reviewer.md and shipped it unmeasured — with an open worry (raised on #25) that the fork path might ignore an agent's tools: the same way it was measured ignoring that agent's model:. It does not. Measured:

Two-state probe, real claude -p processes, project-scoped agent + context: fork skill in a scratch dir (the installed plugin untouched). The skill body instructs the fork to run Bash with echo TOOLPROBE_OK and then self-report AVAILABLE or BLOCKED. Only the agent's frontmatter differs:

### STATE UNRESTRICTED (agent frontmatter: <no tools: key>)
--- fork tool calls:  1 Bash
--- TOOLPROBE_OK in transcript: 3 mentions   (i.e. the command really ran)
--- verdict word: AVAILABLE

### STATE ALLOWLIST (agent frontmatter: tools: Read, Grep, Glob)
--- fork tool calls:  (none)
--- TOOLPROBE_OK in transcript: 1 mention    (the prompt only — never executed)
--- verdict word: BLOCKED

The self-report alone would be weak — a model can claim BLOCKED without trying. The transcript-level tool-call count and the TOOLPROBE_OK echo are the load-bearing evidence: in the allowlisted state the fork issued zero tool calls and the string never appears as output.

So:

  • work-reviewer genuinely cannot call Bash now. Its Boundaries — "Never re-run the work, read the diff, or verify independently" — are enforced by the harness rather than by prose. That closes the mechanism gap behind the three logged incidents of read-only-briefed forks writing to live GitHub PRs.
  • procedure-scout is narrowed but not sealed. It keeps Bash by necessity, and Bash still reaches gh/git/rm. That caveat in this PR's body stands unchanged.

The asymmetry worth recording

On the context: fork path, an agent's frontmatter tools: is honoured while its model: is not (#25 measured a model: sonnet agent running claude-opus-5 from an opus parent, fixed by moving the declaration to the SKILL.md). Two keys in the same file, opposite treatment — not something to infer from either one.

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.

query-records.sh: repeated --keyword silently drops all but the last, and an all-short-token query returns empty with exit 0

2 participants