Skip to content

feat(hooks): boundary-events write guard and per-lens verdict ledger - #2195

Merged
bdfinst merged 10 commits into
mainfrom
feat/2164-verdict-ledger-writer
Sep 22, 2026
Merged

bdfinst merged 10 commits into
mainfrom
feat/2164-verdict-ledger-writer

Conversation

@bdfinst

@bdfinst bdfinst commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds boundary_events_write_guard.py, a PreToolUse hook blocking direct Write/Edit/Bash writes to .claude/metrics/boundary-events.jsonl (including its pre-migration legacy path) — the ledger is now writable only via the sanctioned emit_boundary_event() path, closing a self-certification gap (feat(hooks): block direct writes to the boundary-events ledger outside the sanctioned emitter #2171).
  • Adds hooks/lib/review_verdicts.py (an unconditional, fail-open per-lens verdict store keyed (lens, file_path, file_content_hash)) and hooks/review_verdict_recorder.py (a SubagentStop hook that resolves a completed subagent's subagent_type via the harness's native attributionAgent signal, parses a new dispatch-prompt scope marker, and records a pass/findings verdict per in-scope file) — this slice only writes the store; nothing reads it yet (feat(hooks): record per-lens review verdicts bound to a content hash #2166).
  • Adds one structured Files in scope for this review: <path>, ... marker to /code-review's per-agent dispatch prompts (skills/code-review/SKILL.md), the shared contract between the writer and the recorder.

Test Plan

  • Full pre-push suite green: 10922 passed, 46 skipped (local CI mirror, scripts/ci-local.sh), plus a final 10999 passed, 48 skipped full-directory run before push.
  • boundary_events_write_guard.py: Write/Edit/Bash write-shaped commands (redirect, heredoc, tee, sed -i, Python open()) targeting the ledger — including relative/absolute/./-prefixed/symlinked-cwd/legacy-path forms — are blocked; reads (cat, grep, tail, python3 -c read-mode) are unaffected. Guard demonstrated live to both block and allow.
  • review_verdict_recorder.py: every Slice 2 Gherkin scenario covered by a fixture-transcript test (clean pass, mixed findings, unregistered/non-review subagent_type, empty/single-file scope, deleted in-scope file, malformed/missing-marker transcripts, trust-boundary). Verified live against a real subagent transcript from this session that the SubagentHandback message — not the transcript's literal last row — is correctly parsed for the JSON result (a defect caught and fixed mid-review, with RED→GREEN evidence).
  • pre_pr_review.py / review_gate_corroboration.py suites pass unmodified.
  • knowledge/telemetry-schema.md updated for both new emitters; test_schema_doc_covers_all_metrics_paths green.
  • Consent-off behavior demonstrated by test: review-verdicts.jsonl is unconditional (ALWAYS-ON), matching boundary-events.jsonl's own posture.
  • Reviewed by a 12-agent panel across two checkpoints (spec-compliance, correctness, security, test-review, structure, performance, naming, doc, concurrency, refactor-opportunity, domain, arch) plus a narrowed re-verification pass; all findings resolved or explicitly disclosed as accepted design decisions in the plan.
  • Farley Score (informational): 8.7/10 (Good) across 105 tests added/changed on this branch.

Closes #2166
Closes #2171
Part of #2164

🤖 Generated with Claude Code

https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n


Generated by Claude Code

…dger

Part of #2171, part of #2164.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
…commands

Part of #2171, part of #2164.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
Slice-1 review-fix loop: widen the Bash write-shape regexes (Python r+/mode=
open(), tee/sed non-first-operand forms, ~/$VAR/quoted-space path prefixes),
symmetrize targets_ledger()'s Write/Edit path comparison against
pre_tool_guard.py's own established idiom, correct the Bash remedy message
to a real invocable CLI shape, extract a shared _block() helper plus
per-tool handlers, and address naming/test-smell/test-review findings
(double-waiver markers, custom assertion/run_raw helpers, safety-net and
NUL-byte cwd coverage, empty-stdout assertions, de-hardcoded path constant).

Part of #2171, part of #2164.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
…uard

Opus-tier checkpoint on #2171's guard hook (security/domain/arch-review):
targets_ledger() now also blocks the pre-migration legacy path
(<root>/metrics/boundary-events.jsonl), which emit_boundary_event()'s
default migrate=True would otherwise silently promote into ledger
history from a forged Write/Edit. bash_command_writes_to_ledger() gains
an O(n) literal fast-path ahead of the write-shape regexes to remove a
quadratic-backtracking hang/bypass on long non-matching commands.
Dedupes the ledger stream name/path resolution onto
hooks/lib/review_dispatch_ledger.py instead of re-declaring it locally,
adds the missing telemetry-schema.md registration, mirrors block
messages to stderr per the documented exit-2 hook contract, and drives
the Bash remedy's --event vocabulary from a new
boundary_events.cli_event_names() accessor instead of a hand-copied
literal.

Part of #2164

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
…rompt

Add a structured "Files in scope for this review: <path>, ..." marker to
skills/code-review/SKILL.md step 4's dispatch-prompt construction, so a
future verdict recorder (Step 2.3, #2166) can recover which files an agent
was asked to review from its own transcript. No behavior change to what
gets reviewed or reported.

Adds hooks/lib/review_verdicts.py with just SCOPE_MARKER_PREFIX, the shared
constant this step's test and Step 2.3's future parser both import, so the
marker's literal format can't silently drift between renderer and parser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
Extends hooks/lib/review_verdicts.py (Step 2.1 landed just
SCOPE_MARKER_PREFIX) with emit_review_verdict()/load_verdicts() for
.claude/metrics/review-verdicts.jsonl — Step 2.2 of #2166's plan.

- emit_review_verdict(): unconditional (no consent gate, Decision 2),
  fail-open, appends one JSON line via atomic_state.append_line_locked,
  stamped with plugin_version.shipped_version(). Keyed per
  (lens, file_path, file_content_hash), not per-diff (Decision 3).
- load_verdicts(): returns no usable rows (never raises) for an absent
  file, a corrupted/non-object line, or a plugin_version older than
  current. No consumer yet this slice — review_verdict_recorder.py
  lands in Step 2.3.

Tests cover the write round-trip, each reader no-usable-rows case, an
unwritable-metrics-dir fail-open case, an explicit consent-off case
(DEV_TEAM_TELEMETRY unset + ~/.claude/telemetry.json {"enabled": false}),
and a mechanical content-guard confirming load_verdicts has no consumer
outside review_verdict_recorder.py and this module's own tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
Adds hooks/review_verdict_recorder.py (#2166 Step 2.3): a SubagentStop hook
that identifies the dispatched review agent via the harness-native
attributionAgent field (hooks/lib/cost_meter.py's documented attribution
mechanism, reused via scripts/lib/session_log.records rather than
reimplemented), falling back to the Task/Agent dispatch join only when that
field is absent. Checks subagent_type against review_agent_registry's
closed set, parses the Step 2.1 dispatch-prompt scope marker for the
in-scope file list, cross-references the agent's final JSON result's
issues[].file, and writes one review-verdicts.jsonl row per in-scope file
via review_verdicts.emit_review_verdict(). Fail-open throughout; a single
unreadable in-scope file is skipped without affecting the others.

A pre-implementation spike against 124 real subagent transcripts (recorded
in the module's own docstring) confirmed attributionAgent is present on
100% of usage-bearing records, one consistent value per file — no fallback
needed in practice, though it's still implemented per the plan.

Registers the hook in both settings.json and hooks.json (parity required by
tests/hooks/test_plugin_hooks_json.py), documents review-verdicts.jsonl in
knowledge/telemetry-schema.md (fixing test_schema_doc_covers_all_metrics_paths,
red since Steps 2.1/2.2 landed), and allowlists the new module's own
attributionAgent-naming docstring in repo_invariants.py's
transcript-parsing-confined-to-session-log check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
…corder

Fixes nine review findings from spec-compliance/correctness/security/test
checkpoints against review_verdict_recorder.py:

- CRITICAL: the hook read the transcript's literal last row as the agent's
  JSON result, but a real completed subagent's last row is a short wrap-up
  turn AFTER the SubagentHandback tool_use/tool_result exchange -- the real
  JSON lives in the handback's own input.message field. Verified against a
  real transcript in this session's own corpus before fixing. The hook wrote
  zero rows in production before this fix.
- Path-form mismatch between the scope marker and issues[].file (relative
  vs. absolute) caused false "pass" verdicts; both sides now resolve against
  cwd before comparison.
- Degenerate exits past subagent_type confirmation (missing scope marker,
  unparseable result) now record a boundary event naming the reason, instead
  of being silently indistinguishable from a legitimate no-op.
- _hash_file rejects non-regular files and caps read size, closing a
  FIFO/device hang past the hook's fail-open wrapper.
- Scope-marker paths are contained to cwd before being read/hashed.
- repo_invariants.py's transcript-parsing allowlist entry for this hook is
  corrected to accurately describe its agentId/attributionAgent reads, and
  its transcript reader now delegates to session_log's shared streaming
  iterator instead of a second whole-file reader.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
…s, dedupe lens check

Backstop review of #2166 + #2171 surfaced six findings:

- boundary_events.py's LOG_NAME (promoted from private _LOG_NAME) is now
  the single source of truth for the ledger filename;
  review_dispatch_ledger.LEDGER_STREAM is an alias of it, and
  boundary_events_write_guard.py + review_gate_corroboration.py import from
  the true owner instead of re-declaring/importing a narrower copy.
  repo_invariants.check_ledger_filename_single_sourced asserts the alias
  stays an identity going forward (4/8 reviewers independently reported this).
- review_verdict_recorder.py now emits the canonical cwd-relative POSIX
  file_path (falling back to an absolute resolved form) instead of the raw
  scope-marker path, so the same file groups under one lookup key for the
  future #2167 reader; telemetry-schema.md documents the new field shape.
- Renamed process()'s `target` to `resolved` to match _findings_files().
- Corrected _resolve_under_cwd's docstring from "outside the repo" to
  "outside cwd", matching its actual containment logic.
- Extracted review_agent_registry.is_registered_review_lens() to replace
  the strip-prefix + registry-read + membership check hand-rolled
  independently in agent_dispatch_ledger.py, boundary_events.py, and
  review_verdict_recorder.py.
- Fixed stale docstrings: review_verdicts.py no longer misnames
  review_verdict_recorder.py as load_verdicts()'s consumer; the
  cost_meter.py citation now points at its inline comment, not its module
  docstring; SKILL.md's scope-marker section names its live consumer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uBnw1i52qEik2k9LSHa4n
@bdfinst
bdfinst merged commit 210faa6 into main Sep 22, 2026
15 checks passed
@bdfinst
bdfinst deleted the feat/2164-verdict-ledger-writer branch September 22, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants