Skip to content

fix: batch Patrol fixes and the review guardrail attribution fix - #1535

Merged
ivankuznetsov merged 24 commits into
mainfrom
chore/patrol-batch-2026-10-15
Oct 15, 2026
Merged

ivankuznetsov merged 24 commits into
mainfrom
chore/patrol-batch-2026-10-15

Conversation

@ivankuznetsov

Copy link
Copy Markdown
Owner

Summary

Combines the remaining ready PRs into one branch so they share a single CI run. Each PR head is merged as-is.

PR Change
#1530 agent-cli-runtime: probe credential keys and probe overlay merges can no longer restore scrubbed runtime keys
#1525 eval: clear a stale default report when an inherited RAKEOPT is rejected
#1513 bot: centralize the brainstorm document location in the parser
#1516 bot: encapsulate idea-draft state decisions in IdeaDraftStore; deep-freeze commit_snapshot string leaves
#1461 review: diff reviewer prompts against the merge-base, not branch tips
#1468 review guardrail: attribute permission_change findings and exempt new test stubs

The Patrol fixes (1530, 1525, 1513, 1516, 1461) were validated by Patrol but not reviewed by Hive.

Conflict resolutions

Test plan

  • Locally green: test/unit/bot/*, reviewers/agent_test, stages/review/*, templates/*, integration/run_review_test, and all agent-cli-runtime component tests.
  • eval/support/reporter_test and agent_cli_runtime_component_test fail locally exactly as on main (brakeman 8.1.0 and bundler not installed locally).
  • CI.

After merge, the individual PRs get closed with a pointer here.

🤖 Generated with Claude Code

ivankuznetsov and others added 24 commits September 27, 2026 18:30
… stubs

The post-fix guardrail cleared the current file on each `diff --git`
line, but mode lines come before the ---/+++ pair (and a pure chmod has
no pair), so every permission_change finding rendered as `:?:`. Seed the
path from the diff --git header instead.

With the path known, a new executable under a test/spec/fixture
directory no longer trips permission_change. A fix pass that added the
E2E stub web/test/e2e/support/codex parked a review with zero
escalations in REVIEW_WAITING reason=fix_guardrail. Mode flips on
existing files and new executables elsewhere still trip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ysitter/pr-1468

# Conflicts:
#	test/e2e/lib/incident_budget.rb
#	test/e2e/lib/incident_budget_test.rb
#	wiki/gaps.md
#	wiki/testing.md
The shared brainstorm parser returned only logical question data
(round, number, text, answer), so BrainstormAnswerWriter had to
reconstruct physical identity from raw lines: target_question_line_index
re-scanned raw lines to map the parser's "first unanswered Q{n}" back to
a line index, question_line_index_for_ordinal re-scanned for the
ordinal-position Q header, and block_boundary? re-derived the
Q/Round/marker boundary rule the parser also encodes. Three parallel
location rules meant every future format change was a synchronized
cross-boundary edit, and the raw-line re-scan indexed a different line
array than the parser used (a lone-CR file yields more parser lines than
content.lines).

Centralize document location in Hive::BrainstormParser:

- Question now carries question_line_index, block_end_index (first
  Q/Round/marker boundary at-or-after the header, or EOF), and
  answer_line_index - all indexed against the parser's
  newline-normalized line array, now exposed as document_lines.
- The writer anchors directly on those fields: it selects the
  parser-identified unanswered target, fills the empty A-slot at
  answer_line_index before block_end_index, or inserts a new slot at
  block_end_index - no re-scan, no separate boundary rule.
- Writer line surgery runs on the parser's own line array (re-terminated
  with \n), so parser indices and raw lines can never disagree; the
  written document keeps the pre-write line-ending style (CRLF stays
  CRLF). The lone-CR write path now normalizes the whole document the
  same way the parser does.

The removed re-scan (target_question_line_index) was the last place a
writer-side positional guess could diverge from the parser, which is what
previously let a round boundary be crossed when routing an answer.
EOF-final questions (no trailing newline) keep their terminator, and
lone-CR documents now land answers instead of dead-ending in
:answer_slot_missing.
Branch-only reviewer templates told the reviewer to run
`git diff <default_branch>..HEAD` — the two-dot tip-to-tip range. When the
default branch advanced after the feature branch was cut, that range also
shows the inverse of the new default-branch commits, so reviewers saw
unrelated "changes" the branch never introduced and could file phantom
findings. The codex native reviewer already used the correct three-dot
merge-base range.

All reviewer templates (Grok, pr-review-toolkit, Claude, Codex CE, and the
legacy execute-review prompt) now render `git diff <default_branch>...HEAD`
so every reviewer sees only the changes the branch itself added.
A new template test pins the merge-base notation across all reviewer
templates.
IdeaDraftStore declared the phase/origin contract as its own, yet Router,
CallbackHandlers, and Supervisor each re-derived compound interpretations
from the live mutable Draft Struct: transcript-confirm routing, the
voice-during-draft short-circuit, immediate-commit-vs-file-collection,
voice reuse/reject/clear, and commit validation that consumed raw
attachment tuples.

The store now owns those decisions behind an explicit API: routing and
callback predicates (awaiting_transcript_confirmation?, non_voice_draft?,
voice_draft?, awaiting_text_draft?, transcript_only_voice_draft?), the
ensure_voice_draft/clear_voice_draft transitions, and a commit boundary of
commit_blocker plus a frozen commit_snapshot execution view. Consumers
delegate to them, so execution no longer depends on the store's mutable
representation, and the phase/origin invariants have a single authority.

No behavior change; regression tests cover every predicate, the reuse and
reject semantics, commit blockers, and snapshot immutability.
Review of the encapsulation fix found the frozen commit snapshot still
aliased the live Draft's mutable Strings: freeze does not deep-freeze, so
project, text, and attachment values (staging_path, dest_name, ext) were
shared with the live draft and in-place edits on it (<<, replace) rewrote
what had already been handed to execute_idea_commit.

commit_snapshot now duplicates and freezes every String leaf (nil passes
through), making the snapshot a genuinely independent execution view.
Regression tests cover in-place live-draft mutation after snapshotting
and nil text/project passthrough.
…ected

bin/hive-eval's inherited-RAKEOPT guard exited 1 before the shared
stale-report removal point, so a previously valid report at
tmp/hive-eval-report.json survived every RAKEOPT rejection (--dry-run,
-n, or malformed values). Clear the default report on that path, matching
the other pre-run guards; custom report paths are still only removed
after full validation.

The regression is wrapped in the reporter test's preserve_path helper so
it restores a developer's pre-existing default report instead of
clobbering the hardcoded checkout path, and the hive-eval report
lifecycle contract in the wiki is updated with a log fragment.
…bbed runtime keys

ProbeRequest accepted arbitrary environment keys, and OpenCode probe
child_environment applied the request overlay with Hash#merge after the
RUBYOPT/GEM_HOME/etc. denylist cleanup. A caller-supplied denylisted key
therefore survived into the child environment and broke Ruby-based
executable overrides or launcher shims with injected load paths.

Root-cause fix on both sides of the contract:

- ProbeRequest now validates that environment keys are OpenCode overlay
  keys and rejects anything else with ArgumentError.
- child_environment no longer blanket-merges the request environment; it
  copies only overlay-contract keys, so the denylist scrub stays final
  even if a request is constructed with keys outside the validated
  overlay contract.

Regression coverage: ProbeRequest rejects each denylisted runtime key,
and child_environment keeps scrubbed keys nil while overlay keys and
ambient safe keys still flow through.
Review rework on the scrubbed-runtime-keys fix required the bounded wiki
artifacts: the agent_cli_runtime module page now documents that
ProbeRequest rejects environment keys outside OPENCODE_OVERLAY_ENVIRONMENT_KEYS
and that the probe child environment copies only overlay-contract keys so the
denylist scrub stays final. A wiki/log.d fragment records the contract change
for the compiled log.
…ubbed runtime keys

The generation-2 overlay fix closed request.environment as an overlay
channel, but child_environment still copied
request.credential_environment_keys from the ambient environment after
the Ruby/Bundler/Gem denylist scrub. A probe request listing RUBYOPT (or
GEM_HOME, GEM_PATH, RUBYLIB, BUNDLE_*) as a credential key restored the
poisoned ambient value into the probe child, re-breaking Ruby-based
executable overrides and launcher shims the scrub protects.

Root-cause fix on both sides of the contract:

- New shared SCRUBBED_RUNTIME_ENVIRONMENT_KEYS constant names the Ruby,
  Bundler, and RubyGems injection keys the probe scrubs.
- ProbeRequest and OpenCodePreparationRequest reject any credential key
  in that list with ArgumentError, so neither constructor can select a
  scrubbed key back into a child environment.
- Defense in depth: child_environment now applies the denylist scrub
  last, after credential selection, executable-override selection, and
  the overlay copy, so no caller-controlled channel can reintroduce a
  scrubbed key even if a request bypasses constructor validation.

Regression coverage: both request constructors reject each scrubbed
credential key, and child_environment keeps the scrubbed keys nil while
credential values and overlay keys still flow through.

Wiki: the agent_cli_runtime module page documents the credential-key
contract, and a log fragment records the change.
# Conflicts:
#	test/e2e/lib/incident_budget_test.rb
#	wiki/gaps.md
# Conflicts:
#	test/e2e/lib/incident_budget_test.rb
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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