Skip to content

feat(review): add review:explain-change with a digest policy and an interactive view - #6018

Merged
kyle-sexton merged 17 commits into
mainfrom
feat/1217-review-explain-change
Oct 3, 2026
Merged

kyle-sexton merged 17 commits into
mainfrom
feat/1217-review-explain-change

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #1217

Summary

Slice 21 of #5835: the review:explain-change core. It renames review:pr-explainer to review:explain-change and leaves a one-release stub. It adds a digest_policy (off, offer by default, always) that resolves from a new review-digest cascade concern, and it builds an interactive digest page only through lib/view-builder.mjs. The risk-map check, quiz, recording link, and artifact publish default stay with #5856.

Fix

  • plugins/review/skills/explain-change/
    • SKILL.md: the markdown digest (why, before and after, risk map, where to focus, annotated hunks) is the record. The page is a view of it.
    • templates/digest.html: the checked-in interactive template. It uses data-rv-* bindings only, filters files, collapses hunks, and has a "Reviewed" pick per file, a note, and copy and save buttons.
    • scripts/build-digest.mjs: copies the input down to the fields the template binds, all as strings. It builds with view-builder's interactive profile, so the data goes into the JSON data block and the hash-pinned runtime renders it as text. It writes to the OS temp directory and refuses any --out inside a working tree, so the view never sits beside the record. --check validates a page.
    • scripts/digest-policy.mjs: reads gh pr view --json files,additions,deletions,labels on stdin and decides skip, offer, or build. The offer triggers are more than 5 files, more than 200 changed lines, a HIGH or CRITICAL blast radius, a risk-path glob, and the explain-change label. always builds at --event ready, and --requested (a direct ask) always builds. It also resolves the rendered-views medium key. This lane ships file; the artifact default is review:explain-change: fresh-context risk-map check, quiz section, run-e2e recording link and artifact publish default #5856's.
    • The skill never posts. Its allowed-tools grant only gh pr view, gh pr diff, and the two scripts, and neither script calls gh.
  • plugins/review/skills/pr-explainer/SKILL.md: a one-release stub naming /review:explain-change, with disable-model-invocation: true. The old report builder build-explainer.mjs is removed.
  • docs/conventions/review-digest.md: the owner doc for the concern. Its json config block is this repository's team layer and holds the shipped defaults. A test keeps it equal to the script's DEFAULTS.
  • docs/conventions/config-cascade/README.md: adds an Implementers row and a root-rule row for review-digest, and regenerates the semantics table. The row declares one deviation: an untracked team layer is reported and treated as absent instead of stopping the run, and an overlay that is not gitignored is reported but still applied. The reason is that every key only decides when a reader is offered a view.
  • docs/conventions/rendered-views/README.md and its CHANGELOG now name the new lane.
  • lib/html-escape.test.sh had used the removed pr-explainer builder as its exemplar page. It now builds that page with view-builder's report profile. Three things went with the old builder: the title-attribute case (the report profile does not allow slots inside tags), the builder-source interpolation scan, and the builder CLI cases.
  • Review plugin 0.36.4 to 0.37.0, with a CHANGELOG entry. README, catalog, and cheat sheet are regenerated.

Verification

  • bash plugins/review/tests/explain-change.test.sh: 40 of 40 pass. It covers one test per offer trigger (files, changed lines, HIGH, CRITICAL, two risk paths, label) and that exact thresholds do not fire. It covers off, always at ready, always before ready, and a direct request. Through the CLI it covers the cascade (user-global, then the tracked team docs block, then the overlay, key by key; the argument beats every layer; a malformed layer, an invalid value, or an unknown key degrades soft; the docs block beats .claude/review-digest.json with a warning; two blocks are invalid) and medium resolution. For the builder it checks that hostile data stays in the JSON data block, that unbound fields never reach the page, that the page passes the interactive profile, and that a path inside the working tree is refused. It also checks the read-only boundary.
  • bash plugins/review/tests/explain-change-chrome.test.sh, bash lib/html-escape.test.sh (28 cases), and bash lib/view-builder.test.sh pass.
  • scripts/affected-tests.sh --run --jobs 6: every selected shell suite passes except check-script-contract.test.sh, which failed only because htmlhint was missing. After npm ci it passes 48 of 48, and scripts/check-html-assets.sh is clean. htmlhint on the new template is clean.
  • These gates pass: check-changelog-parity.sh (--check, --check-bump, --check-preserved), generate-catalog.mjs --check, generate-cheatsheet.mjs --check, sync-config-cascade-semantics.py --check, check-skill-leaf-names.sh, check-docs-naming.sh, sync-shared-copies.sh --check, check-contract-clause-coverage.py, check-orphaned-fixtures.sh, check-spoke-plugin-root.sh, sync-plugin-options-docs.py --check, and check-purged-em-dashes.sh. markdownlint finds no issues in the changed markdown, and the ai-slop detector finds nothing in the new prose.
  • skill-quality check-skill.sh: explain-change passes with 0 warnings. pr-explainer passes with 5 warnings, all of them the deliberate move of trigger phrases to explain-change. check-evals-quality.sh passes on the new evals.
  • I opened a page built from hostile data in headless Chromium. Every binding rendered, including nested hunks and builder row ids (files-1-hunks-2), and the <img> and </script> payloads appeared as text.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 3, 2026 01:30
…nteractive view

Rename review:pr-explainer to review:explain-change and leave a one-release
stub. The markdown digest is the record; build-digest.mjs builds the view
from a checked-in template plus escaped JSON through view-builder's
interactive profile, outside the working tree. digest-policy.mjs resolves
the new review-digest cascade concern (off, offer, always; offer triggers
on files, changed lines, blast radius, risk paths, and an opt-in label)
and the rendered-views medium key. The skill never posts to the pull
request and never sets a check status.

Refs #1217

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-digest.mjs takes no --out: each page goes to a fresh mkdtemp
directory under the realpath of the OS temp dir, written with flag wx,
so a diff-steered caller cannot aim K2 text at rules, shell, or
settings files.

digest-policy.mjs reads the team layer and the rendered-views team file
from the pull request's base ref (baseRefName), applies an overlay only
when untracked and gitignored, and always fires risk-path for a change
to any digest config file, so a checked-out PR cannot silence its own
digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a case-insensitive filesystem a PR could track
.claude/Review-Digest.local.json and have it read as the untracked
overlay. The tracked check now lists files with an :(icase) pathspec,
and the always-on config paths match case-insensitively against
normalised PR paths (backslashes, ./, duplicate slashes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 3, 2026 06:39
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T06:44:20.547033Z 24eedc6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 5m 41s —— View job


Review complete

Scope

Per REVIEW.md, security findings are out of scope for this lane: .github/workflows/claude-security-review-hosted.yml exists, so that lane owns vulnerabilities, trust-boundary, and injection concerns here. This review covers correctness, design, conventions, error handling, and maintainability.

What I checked

I traced the two new scripts line by line against their stated contracts and the cascade/view-builder machinery they depend on:

  • digest-policy.mjs: the trigger thresholds (strict >, matching the "more than" wording in docs/conventions/review-digest.md), the off/offer/always/--requested decision table, the three-layer cascade (user-global → team → overlay, later layer wins key-by-key) and its home/root/case-variant/gitignore guards, the **/*/? glob-to-regex translation, and the medium resolution sharing the same warnings array as the digest config. All of it checks out against the behavior documented in docs/conventions/review-digest.md and the config-cascade semantics row added for review-digest.
  • build-digest.mjs and templates/digest.html: shapeDigest keeps only template-bound fields as strings; the focus array of plain strings is handled correctly by view-runtime.js's primitive-scope branch (bindText falls back to asText(scope) when the list item isn't an object, so data-rv-text="item" on a string row works as designed, not a bug). The temp-dir-outside-working-tree refusal, the wx exclusive write, and the --out-is-never-accepted contract all hold up.
  • Cross-checked the chrome-token test (explain-change-chrome.test.sh) against plugins/visualization/reference/html-chrome.html: all five color tokens and the dark-mode --focus → --clay-soft substitution match.
  • Doc/metadata consistency: plugin.json version (0.37.0) matches the CHANGELOG entry, README.md, docs/catalog.md, and docs/skill-cheat-sheet.md all describe the new skill and the pr-explainer stub consistently. No stale references to the removed build-explainer.mjs outside historical CHANGELOG entries and the unrelated education plugin.
  • The "team config comes from the base ref, not the working tree" and "a tracked case-variant overlay counts as tracked" guards (the subject of this PR's own 27d27b8/48d3c19 fix commits) hold correctly under the scenarios in explain-change.test.mjs.

I found no correctness issues meeting this lane's high-signal bar (something a careful senior reviewer would block or flag), so there are no inline comments to post.

Not independently verified

I could not execute shell/node commands in this sandbox (tool-use approval was denied for running the test suite, e.g. bash plugins/review/tests/explain-change.test.sh), so the test results, lint gates, and the headless-Chromium check described in the PR's Verification section are author-claimed and not independently confirmed by me. My review is based on static reading of the diff and the cascade/view-builder machinery it builds on.

Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 55s —— View job


Security review (/review:security-review)

  • Read trigger instructions and repo CLAUDE.md/AGENTS.md
  • Invoke /review:security-review skill
  • Gather diff context against origin/main
  • Analyze security-relevant changes (scripts, templates, policy logic)
  • Post findings

Scope: files changed in this PR only (gh pr diff / git diff origin/main...HEAD), per the skill's instructions. GitHub Actions hardening is out of scope (zizmor's lane); no workflow files changed here anyway.

Verdict: no security issues found

This PR's trust-relevant surface is plugins/review/skills/explain-change/scripts/digest-policy.mjs and build-digest.mjs, plus the new templates/digest.html. I checked the four properties the design claims and each holds:

  • A pull request cannot configure its own digest policy. The team layer is read via git show <baseRef>:<path> against the PR's base ref, never the working tree (digest-policy.mjs#L92-L101). baseRef is validated against a strict charset before use, and execFileSync passes args as an array (no shell), so there's no command/argument injection even though the value nominally comes from gh pr view --json baseRefName.
  • Hostile PR content never becomes markup/script. build-digest.mjs's shapeDigest coerces every field to a plain string and drops anything not on the template's allow-list (build-digest.mjs#L32-L49), and the template only uses data-rv-* bindings rendered through the (unchanged, already-reviewed) view-builder.mjs interactive profile, which puts data in a JSON block and renders via textContent, never innerHTML.
  • The view never lands in the working tree. build-digest.mjs resolves the real path of the OS temp dir and refuses to write if that path is inside a git working tree, and writes with wx (fail-if-exists) into a freshly mkdtemp'd directory (build-digest.mjs#L94-L116).
  • The skill truly cannot post. explain-change/SKILL.md's allowed-tools only grants the two scripts, gh pr diff, gh pr view, Read, Glob, Grep — no gh pr comment/gh pr review/gh api (SKILL.md#L6).

I also checked the instruction-surface-deletion lens: the pr-explainer stub drops its old narrow allowed-tools list, but the stub's body never reads a diff or PR text itself — it only names the rename and forwards to /review:explain-change, which carries its own full tool scoping. No untrusted input reaches the model while the looser stub is active, so nothing in the register's protected classes is actually exposed.

One non-blocking doc-accuracy note (not a vulnerability, code is already the safe behavior): the new review-digest row in docs/conventions/config-cascade/README.md states "an overlay that is not gitignored is reported and still applied" (README.md#L526), but overlayApplies() in digest-policy.mjs does the opposite — it returns false ("layer ignored") precisely when the overlay is not gitignored (digest-policy.mjs#L196-L208), matching docs/conventions/review-digest.md's own text ("The overlay applies only when untracked and gitignored") and the test suite. Worth a one-line fix to the config-cascade row so a future change doesn't "correct" the code to match the wrong doc and reopen the trust boundary it currently protects.

Fix this →

No command/path injection, XSS, or trust-boundary bypass found in the new explain-change scripts or template.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24eedc6b88

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/review/skills/explain-change/SKILL.md
Comment thread plugins/review/skills/explain-change/scripts/digest-policy.mjs Outdated
Comment thread plugins/review/skills/explain-change/scripts/digest-policy.mjs Outdated
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review has reviewed this pull request through 7003422; a later push is reviewed from there.

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

claude-review has reviewed this pull request through 7003422; a later push is reviewed from there.

kyle-sexton and others added 2 commits October 3, 2026 02:47
…ge test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 7s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Gather context: diff origin/main...HEAD at 0047e1c, prior review comments
  • Skip gate: not applicable — this head (0047e1c) wasn't reviewed by this lane yet (prior review covered 24eedc6; only cfaaaba was added since, a small evals/test tweak)
  • Review changed files for correctness, design, conventions, error handling, maintainability (security scoped out — claude-security-review-hosted.yml owns that lane)
  • Post inline finding
  • Post summary

Scope

Per REVIEW.md, security findings are this repo's hosted security lane's job, not this one. This pass covers correctness, design, conventions, and maintainability.

What's new since the last review at this head

The only commit since 24eedc6 (last reviewed by this lane) is cfaaaba: adds a stub eval for pr-explainer and swaps "colour" → "palette" in explain-change.test.mjs's unknown-key test (avoiding an ai-slop/typo false hit on the British spelling). Both are clean — no issues.

Finding

One inline comment posted on docs/conventions/config-cascade/README.md:526:

The new review-digest implementer row says "an overlay that is not gitignored is reported and still applied," but digest-policy.mjs's overlayApplies() does the opposite — it ignores the overlay precisely when it is not gitignored — and that matches this PR's own docs/conventions/review-digest.md:53. The two docs added in the same PR disagree with each other, and the config-cascade row disagrees with the code. This was previously flagged as a non-blocking doc-accuracy note by this PR's security review comment and by a Codex review comment, and remains unfixed as of this head. Not a vulnerability (the code is already the safe behavior), but a maintainability risk: a future edit could "fix" the code to match the wrong sentence and reopen the isolation it currently protects. One-line fix: flip the clause to "...is reported and the layer is ignored."

Not re-litigated

Codex's P1 note (digest policy has no caller yet for unrequested PR events) and P2 note (base-ref resolution for forked PRs) are not raised here as findings: the former is explicitly disclosed in the PR's own Related section ("Not wired here... belongs to source-control"), and the latter doesn't hold up under this repo's CI checkout model, where origin is always the base repository for both same-repo and fork-sourced PRs, making refs/remotes/origin/<baseRefName> the correct ref regardless of fork origin.
· Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 39s —— View job


Security review (/review:security-review)

  • Read trigger instructions and repo CLAUDE.md/AGENTS.md
  • Invoke /review:security-review skill
  • Gather diff context against origin/main and the delta since the last security review
  • Analyze security-relevant changes
  • Post findings

Verdict: no new security issues — skip gate applies

This run was triggered by pull request synchronize. The last successful security review on this
PR covered commit 24eedc6 and found no security issues in digest-policy.mjs, build-digest.mjs,
templates/digest.html, or the SKILL.md/allowed-tools boundaries.

I diffed 24eedc6..0047e1c (the current head) and confirmed the only change since that review is
commit cfaaaba:

  • plugins/review/skills/pr-explainer/evals/evals.json: adds a stub eval case for the renamed-skill
    pointer. No executable surface, no trust-boundary change.
  • plugins/review/tests/explain-change.test.mjs: renames a test fixture key (colour → palette)
    to dodge a typo-checker false hit. Test-only, no production code touched.

None of the security-relevant surface — digest-policy.mjs's base-ref isolation and cascade
resolution, build-digest.mjs's temp-dir/working-tree guard and string-coercion sanitization, the
template's data-rv-*-only bindings, or the skills' allowed-tools scoping — changed. The rest of
the diff on this head (brought in by the origin/main merge commits) is unrelated prerequisites.*
scaffolding for other plugins, outside this PR's own changed-files set and outside this skill's
scope (review only the files changed in this PR).

Per the skip gate ("this head already has a successful security review that still applies"), I'm
not re-deriving findings for unchanged code. No new inline comments.

Standing note (not new, not a vulnerability)

One doc-accuracy mismatch flagged in the prior review is still open: the review-digest row in
docs/conventions/config-cascade/README.md
states "an overlay that is not gitignored is reported and still applied," but
overlayApplies()
does the opposite — it ignores a non-gitignored overlay, matching docs/conventions/review-digest.md
and the test suite. The code is already the safe behavior; only the config-cascade sentence is wrong.
Worth a one-line fix so a future change doesn't "correct" the code to match the wrong doc.

Fix this →

No command/path injection, XSS, or trust-boundary bypass found in the changes since the last review.
· Branch: feat/1217-review-explain-change

Comment thread docs/conventions/config-cascade/README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 11s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Check skip gate: this head (b10026d) already has a successful review from this lane that still applies
  • Gather context: diff origin/main...HEAD at b10026d
  • Confirm no PR content changed since the last reviewed head
  • Post summary

Skip gate applies

git log 0047e1c..b10026d shows one commit beyond the last head this lane reviewed: a merge of
origin/main (bringing in 5f9e04f, the unrelated shared-lib regen-flow migration). That merge
touches two files textually (docs/conventions/config-cascade/README.md's root-rule paragraph and
a plugins/review/CHANGELOG.md entry for the shared html-escape.mjs/standards-contract.md
sync), but git diff origin/main...HEAD confirms the PR's actual diff against its base is
byte-identical to the 21-file, +1329/-401 set already reviewed at 0047e1c — the merge nets to
zero against origin/main, so nothing in this PR's own content changed. No new review surface to
check.

Standing note (not new)

The doc/code contradiction flagged in the prior reviews is still open: the review-digest row in
docs/conventions/config-cascade/README.md:526
says "an overlay that is not gitignored is reported and still applied," but overlayApplies() in
digest-policy.mjs#L196-L208
does the opposite — it ignores a non-gitignored overlay, matching docs/conventions/review-digest.md:53
and the test suite. The code is already the safe behavior; only this one sentence is wrong.
Codex, this PR's own security-review lane, and this lane's prior pass at 0047e1c have all flagged
this already and it remains unfixed. Not re-posting a duplicate inline comment since this is the
third report of the same line.

Fix this →
· Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 4s —— View job


Security review (/review:security-review)

Verdict: no new security issues — skip gate applies

This run was triggered by pull request synchronize. The last successful security review on this PR covered commit 0047e1c and found no security issues in digest-policy.mjs, build-digest.mjs, templates/digest.html, or the SKILL.md/allowed-tools boundaries (and itself skipped re-review of 24eedc6..0047e1c on the same basis).

I diffed 0047e1c..b10026d (the current head). The only commit on top is b10026d, a merge of origin/main, which brings in one unrelated upstream commit, 5f9e04f (build(shared-lib): migrate the small shared-library clusters to the regen flow..., #5938). That commit touches some of the same paths this PR also modifies — lib/html-escape.mjs, plugins/review/.claude-plugin/plugin.json, plugins/review/CHANGELOG.md/README.md, docs/conventions/config-cascade/README.md, docs/conventions/rendered-views/README.md/CHANGELOG.md, and the pr-explainer stub files — so I verified the merge directly rather than trusting the skip gate on commit count alone:

  • No leftover conflict markers in any file this PR owns.
  • lib/html-escape.mjs (actual escaping logic) is byte-identical between 0047e1c and b10026d — the upstream commit only added a generated-file header elsewhere; no escaping behavior changed.
  • plugins/review/skills/pr-explainer/scripts/build-explainer.mjs is still deleted, and the pr-explainer stub still carries disable-model-invocation: true with no tool grants — consistent with this PR's design, not reverted by the merge.
  • The remaining overlaps (CHANGELOG.md, README.md, plugin.json) resolved to additive, non-functional entries (a sync CHANGELOG line explicitly noting "no change to this plugin's lib and reference").
  • plugins/review/skills/explain-change/ (the security-relevant surface: base-ref isolation, string-coercion sanitization, temp-dir/working-tree guard, allowed-tools scoping) has no commits on top of it since 48d3c19, which predates the last review.

None of the trust-relevant surface changed. Skip gate applies; no new findings to report.

Standing note (not new, not a vulnerability)

The doc-accuracy mismatch flagged in the two prior security reviews is still open: the review-digest row in docs/conventions/config-cascade/README.md:526 states "an overlay that is not gitignored is reported and still applied," but overlayApplies() does the opposite — it ignores a non-gitignored overlay, matching docs/conventions/review-digest.md and the test suite. The code is already the safe behavior; only the config-cascade sentence is wrong. This has also been raised by Codex and by this repo's code-review lane; a one-line fix to the doc sentence would close it.

Fix this →

No command/path injection, XSS, or trust-boundary bypass found in the changes since the last review.
· Branch: feat/1217-review-explain-change

…uard the overlay

digest-policy.mjs reads the team layer and the rendered-views team file
at the PR's baseRefOid, which names the base repository's commit even
for a fork; a missing or malformed oid skips the team layer with a
warning. An untracked overlay that is not gitignored now applies with a
warning, as the config-cascade convention says, and an overlay reached
through a symlinked .claude or overlay file, or resolving outside
.claude, is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 39s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Read .claude-lane/incremental.diff for the scoped change set (since b10026d)
  • Review the five changed files for correctness, design, conventions, error handling, maintainability
  • Post inline findings (none met the high-signal bar)
  • Post summary comment

Scope

Per REVIEW.md, security findings are this repo's hosted security lane's job, not this one. Incremental scope per the lane header: only the diff since b10026d in docs/conventions/review-digest.md, plugins/review/CHANGELOG.md, plugins/review/skills/explain-change/SKILL.md, plugins/review/skills/explain-change/scripts/digest-policy.mjs, and plugins/review/tests/explain-change.test.mjs. All other files were already reviewed at b10026d.

What changed since b10026d

Commit 91f51b9 does two things:

  1. Fork-safety fix for the base-ref read. digest-policy.mjs now reads baseRefOid (a 40-hex commit SHA, validated by regex, then checked with git cat-file -e <oid>^{commit} before use) instead of resolving refs/remotes/origin/<baseRefName> / refs/heads/<baseRefName>. This closes the Codex P2 finding from the earlier review round: a fork PR's baseRefName could previously resolve against the wrong repository's branch (or a stale local tracking ref). Pinning to the exact commit the rest of that same gh pr view call's facts (files, additions, labels) were read against is also more internally consistent than re-resolving a branch name later. When the commit isn't present in the clone (e.g. a shallow checkout), the team layer now degrades softly with a named warning — consistent with this surface's existing "soft degrade, no policy-floor" design.
  2. Overlay semantics now match the documented contract, by changing the code instead of the doc. The three earlier review rounds (Codex, the security lane, and this lane) all flagged that docs/conventions/config-cascade/README.md's review-digest row said "an overlay that is not gitignored is reported and still applied" while overlayApplies() did the opposite. This commit resolves the contradiction by changing overlayApplies() to match that sentence — a non-gitignored, untracked overlay now applies with a warning rather than being ignored — and adds new guards: a symlinked .claude directory or a symlinked overlay file is refused (digest-policy.mjs:205-213), closing a path that would otherwise have let an overlay escape .claude undetected once "not gitignored" stopped being a hard block. docs/conventions/review-digest.md:51-55 was updated to match, and docs/conventions/config-cascade/README.md:526 (unchanged in this commit) now reads as accurate rather than contradictory. I verified this resolves the standing note from the last four review passes on this PR — this is why it isn't re-raised here.

Since this surface is explicitly non-security (digest_policy and its cascade only decide whether a reader is offered a view, never a gate), trading the hard "not gitignored → ignored" rule for "not gitignored → applied, with a warning, plus new symlink refusal" is a reasonable way to make the code match its own documented contract, and the new tests (explain-change.test.mjs:258-304) cover the untracked-unignored-applies case, the symlinked-file case, and the tracked-symlinked-directory case.

Verification

Test and doc changes in this increment are internally consistent: explain-change.test.mjs:164 now asserts /not on the base commit/ (matching the renamed warning text), the MAIN placeholder helper (explain-change.test.mjs:208-212) correctly substitutes the live main SHA before invoking the CLI, and the "base commit missing from the clone" test uses the all-zero OID (a syntactically valid but non-existent commit) to exercise the new soft-degrade path. I did not re-run the suite myself (no Bash test execution in this pass); this reading is static.

No new findings

I did not find a correctness issue in this increment meeting this lane's high-signal bar. One thing I checked and ruled out as not a finding: this repo's own plugins/source-control/skills/babysit-prs/reference/freshness.md:99-103 documents that baseRefOid can empirically lag a base branch's live tip, and recommends baseRefName + compare for merge-state freshness checks. That gotcha is about detecting whether a PR's head is behind the base for merge purposes; it doesn't undermine digest-policy.mjs's use, which only needs a consistent, correctly-scoped-to-the-target-repo commit to read a config file from — a property baseRefOid provides that baseRefName did not for forks. Not raising this as a finding.
· Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 13s —— View job


Security review (/review:security-review)

  • Read trigger instructions and repo CLAUDE.md/AGENTS.md
  • Invoke /review:security-review skill, incremental scope since b10026d
  • Review .claude-lane/incremental.diff (the 5 changed files) for trust-boundary issues
  • Post findings

Verdict: one finding — a trust-boundary weakening in the overlay gate

Scope: per the custom instructions, only the delta since b10026d in docs/conventions/review-digest.md, plugins/review/CHANGELOG.md, plugins/review/skills/explain-change/SKILL.md, plugins/review/skills/explain-change/scripts/digest-policy.mjs, and plugins/review/tests/explain-change.test.mjs.

Good fix, not a finding: baseRefName → baseRefOid (digest-policy.mjs#L93-L103) correctly resolves Codex's P2 note from the prior round (a branch name under refs/remotes/origin/<name> could be the fork's copy, not the base repo's). Validating against a strict 40-hex regex and requiring git cat-file -e to confirm the commit is locally present, with a fail-closed warning otherwise, is the right shape. The new .claude/overlay symlink-escape guard (digest-policy.mjs#L205-L209) is also a solid defensive addition.

Finding — the overlay's gitignore requirement was dropped, reopening the gap three prior reviews already flagged:

overlayApplies() L210-L213:

if (!git(root, ["check-ignore", "-q", "--", path])) {
  warnings.push(`overlay ${path}: not gitignored, so it can reach team history`);
}
return true;

Before this round, a non-gitignored overlay hit return false ("layer ignored"). Now it only warns and the overlay is still applied. docs/conventions/review-digest.md#L54-L55 was edited to match this new behavior ("one that is not gitignored is reported and still applied").

This is exactly the contradiction this lane, the hosted security-review lane, and Codex all raised against the previous head (0047e1c/b10026d): config-cascade/README.md's row said the overlay "is reported and still applied" when not gitignored, while the code (correctly, per all three reviews) refused it. All three review passes recommended fixing the doc sentence, explicitly because "the code is already the safe behavior" and "a future edit could 'fix' the code to match the wrong sentence and reopen the isolation it currently protects." This diff does precisely that: it changed the code to match the wrong sentence instead of fixing the sentence.

Why this matters as a trust boundary and not just a doc nit: the one remaining gate on the overlay layer is "untracked by git." A file can be untracked yet still attacker-influenced within a single PR's own CI run — e.g. a malicious PR's package.json/lockfile adding a postinstall script (this plugin's own verification steps run npm ci in the same job before skill scripts execute) could drop an unignored .claude/review-digest.local.json setting digest_policy: "off" or widening risk_paths/blast_radius thresholds. That defeats the cascade's own stated guarantee, repeated unchanged by this same diff three lines above: "a checked-out pull request cannot configure its own digest." Requiring the overlay to also be gitignored closed exactly that gap (an unignored file dropped by a build step reads as "this wasn't meant to be personal-local config," so it's rejected); dropping that requirement reopens it.

The new tests (explain-change.test.mjs, "an untracked overlay that is not gitignored applies, with a warning") confirm this is intentional, not an oversight — but intent doesn't change the exploitability, and the change runs directly against the fix three independent review passes already agreed on for this exact line.

Recommendation: revert overlayApplies()'s gitignore check to return false (restore the original "layer ignored" behavior for a non-gitignored overlay), and instead fix the stale config-cascade/README.md row (unchanged by this diff, so still open) to read "...and the layer is ignored," matching this file's own now-correct sentence before this round.

Fix this →

Not re-flagged

Codex's P1 note (no caller wires digest_policy into unrequested PR lifecycle events yet) is unchanged by this diff and was already disclosed as out of scope for this slice in the PR's own Related section.

No command/path injection or other new vulnerability found in this delta; baseRefOid validation and execFileSync argv-array usage remain injection-safe.
· Branch: feat/1217-review-explain-change

cursoragent and others added 3 commits October 3, 2026 08:04
# Conflicts:
#	docs/conventions/rendered-views/CHANGELOG.md

Co-authored-by: ksextonmelodic <ksextonmelodic@gmail.com>
A PR could make .claude a submodule holding the overlay files, which
ls-files on the parent repo never sees. The overlay is now refused when
.claude itself is a tracked entry (gitlink, file, or symlink) in any
case, or when .claude/.git exists, and .gitmodules joins the config
paths that always fire risk-path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lodic-software/claude-code-plugins into feat/1217-review-explain-change
@kyle-sexton kyle-sexton added do-not-merge Hard merge gate: do not merge while applied. and removed do-not-merge Hard merge gate: do not merge while applied. labels Oct 3, 2026
Keep review 0.38.0 above main's 0.37.2 and both changelog entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 3m 30s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Check skip gate: this head (c221d9e) has new content since the last reviewed head (91f51b9)
  • Gather context: git show on the new commits, diff of this PR's own files between 91f51b9 and c221d9e
  • Review the new change for correctness, design, conventions, maintainability
  • Post inline finding
  • Post summary

Scope

Per REVIEW.md, security findings are the hosted security lane's job, not this one. No network fetch was available in this sandbox, so I diffed locally against commits already present in the checkout (91f51b9, the last head this lane reviewed) rather than origin/main.

What's new since the last review at 91f51b9

Diffing this PR's own 21 changed files between 91f51b9 and c221d9e shows exactly one functional commit: 509abc8 ("refuse an explain-change overlay inside a .claude submodule"). The rest of the delta (65a9855, 9671ece, c221d9e) is a merge of origin/main bringing in unrelated plugin work (rendered-views entries for debugging:debug/discovery:blindspot, new check-prerequisites skills, a shared prerequisites.sh sync CHANGELOG line) — none of it touches this PR's own review-digest surface.

509abc8 closes a real gap: previously, overlayApplies() only checked whether the overlay file was tracked by the parent repo's ls-files. A PR could commit .claude as a git submodule (git submodule add), and the overlay file living inside that submodule would never show up in the superproject's ls-files, letting a PR-controlled review-digest.local.json/rendered-views.local.md apply despite the documented "a pull request cannot configure its own digest" guarantee. The fix adds an ls-files -s check for a tracked entry (gitlink, file, or symlink, any case) exactly at .claude, plus an existsSync(.claude/.git) fallback for an already-initialized submodule, and adds .gitmodules to CONFIG_PATHS so adding one always fires risk-path. I traced the logic line by line — the exact-match filter on the ls-files -s -z output correctly isolates the .claude gitlink/file/symlink entry without being fooled by nested tracked paths, and the new/updated tests (explain-change.test.mjs) cover the submodule case and the pre-existing symlink case together. No correctness issue found in the fix itself.

Finding

One inline comment on docs/conventions/review-digest.md:55: the owner doc's overlay paragraph still only names the symlink and gitignore guards and wasn't updated for the new submodule/tracked-entry guard or the .gitmodules addition to CONFIG_PATHS; the same gap exists in the plugins/review/CHANGELOG.md 0.38.0 bullet. Not a vulnerability — the code already has the fix — but this is the fourth time in this PR's own review history that the doc describing this exact overlay paragraph has drifted from the code implementing it, so it's worth closing now.

Not re-litigated

Codex's P1 note (no caller wires digest_policy into unrequested PR lifecycle events yet) is unchanged and already disclosed as out of scope for this slice in the PR's own Related section.
· Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 51s —— View job


Security review (/review:security-review)

Scope

The only commit touching this PR's own files since the last security review (91f51b91d, which
found the open overlay-trust-boundary note below) is
509abc8:
fix(review): refuse an explain-change overlay inside a .claude submodule. The two merge commits
on top (65a9855, c221d9e) bring in unrelated origin/main history; git diff 91f51b91d86c6a9b6c702063264964c3817a63cc..HEAD confirms no other file this PR owns changed.

Verdict: the new submodule guard is sound; no new vulnerability

digest-policy.mjs#L207-L212 adds, ahead of the existing symlink/within() check:

const entries = (gitOut(root, ["ls-files", "-s", "-z", "--", ":(icase).claude"]) ?? "").split("\0");
if (entries.some((e) => e.split("\t")[1]?.toLowerCase() === ".claude") || existsSync(join(dir, ".git"))) {
  warnings.push(`overlay ${path}: .claude is a submodule or tracked entry; layer ignored`);
  return false;
}

This closes a real gap in the prior state: a git index entry for the literal path .claude
(directories never get their own index entry — only files, symlinks, and gitlinks do) can only
mean a PR-controlled submodule, file, or symlink sits where a real directory should be. A
submodule's checked-out contents are an ordinary directory on disk, so lstatSync on the overlay
path previously reported "not a symlink" and within() reported "inside .claude" — the old
guard had no way to see it, exactly as the commit message states (ls-files on the parent repo
never lists a submodule's own tracked files). The new -s/gitlink check catches the registered-submodule
case; existsSync(.claude/.git) is sound defense-in-depth for an unregistered nested repo dropped
by, e.g., a build step. Both checks run with static arguments through execFileSync's argv array,
so there's no injection surface. .gitmodules joining CONFIG_PATHS (always fires risk-path) is a
reasonable detection addition, not a trust boundary itself.

The new test (explain-change.test.mjs, "a .claude submodule holding the overlay is refused...")
exercises exactly this path — registers .claude as a real git submodule add, confirms the
overlay inside it is ignored and .gitmodules/.claude fire risk-path. I could not execute the
suite myself (no Bash approval for node --test in this sandbox), so this reading is static, matching
the "not independently verified" caveat earlier reviews on this PR also recorded.

Standing note, not re-flagged as a new finding

The design point raised by the prior security review at 91f51b91d — overlayApplies() applies
(with a warning) a non-gitignored, untracked overlay rather than refusing it — is unchanged by
509abc8 and remains consistent between
digest-policy.mjs#L213-L216
and docs/conventions/review-digest.md:54-55. I'm not re-opening it as a finding here: the author's
reply at 2026-10-03T07:05:59Z explains the considered choice (code now matches the documented
contract), and re-examining the blast radius against this skill's own stated invariant — it "never
posts to the pull request and never gates merge" (SKILL.md's description, repeated in the PR
body) and, per Codex's still-open P1 note, isn't yet wired to any unsolicited/automated trigger at
all — the worst case this layer can cause is an advisory, non-posting, local-temp-file digest being
over- or under-offered to a human who can run gh pr diff regardless. That's a UX nicety, not a
security control, so it doesn't meet this lane's bar for a trust-boundary finding. Worth
re-examining once #5856 actually wires an unsolicited caller, at which point I'd re-check whether
the blast radius has changed.

No command/path injection, symlink-escape, or other new vulnerability found in the delta since the
last security review.

Branch: feat/1217-review-explain-change ·

Comment thread docs/conventions/review-digest.md Outdated
Keep review 0.38.0 above main's 0.37.2 and both changelog entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 1s —— View job


Security review (/review:security-review)

Verdict: no new security issues — skip gate applies

This run's last successful security review on this PR covered c221d9e8 and found no issues (the submodule-overlay guard added there was sound, and the earlier overlay-gitignore trust-boundary finding had already been resolved by the author at 91f51b91d).

I diffed c221d9e8..0dc4e081 (the current head). Two commits sit on top:

Restricting to this PR's own changed files, the only diff in that range is two documentation lines:

Both edits are non-conflicting, additive list entries that origin/main's merge carried in from PR #6062; neither touches this PR's own trust-relevant surface (digest-policy.mjs's base-ref isolation, cascade/overlay resolution, or the submodule guard; build-digest.mjs's string-coercion and temp-dir/working-tree guard; the template's data-rv-* bindings; or the skills' allowed-tools scoping). I confirmed no file under plugins/review/skills/explain-change/, plugins/review/skills/pr-explainer/, or plugins/review/tests/ changed in this range.

Skip gate applies. No new findings.

Standing notes (not new, already resolved or disclosed)

  • The overlay-gitignore and fork-base-ref trust-boundary issues raised across earlier rounds of this PR's review history were resolved by the author (91f51b91d, 509abc8) and confirmed sound in the prior review at c221d9e8.
  • Codex's P1 note (no caller wires digest_policy into unrequested PR lifecycle events yet) remains unchanged and is explicitly disclosed as out of scope for this slice in the PR's own Related section.

No command/path injection, XSS, or trust-boundary bypass found in this PR's changes since the last review.
· Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 6s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Check skip gate: this head (0dc4e08) vs the last head this lane fully reviewed (c221d9e)
  • Confirm no PR content changed since the last reviewed head
  • No new findings to post

Skip gate applies

git log c221d9e..0dc4e08 shows one commit beyond the last head this lane reviewed: a merge of
origin/main, bringing in eafc570 (feat(architecture): interactive views for the map-* records,
#6062) — unrelated work on plugins/architecture/, plugins/planning/, and the shared-copy
registries.

That commit lands in two files this PR also touches, docs/conventions/rendered-views/README.md
and docs/conventions/rendered-views/CHANGELOG.md (both add their own entries to the same shared
doc). I checked git diff origin/main...HEAD directly rather than trusting the commit count alone,
and it confirms this PR's own net diff against the current origin/main is byte-identical in
content to the 21-file, +1421/-401 set already reviewed at c221d9e — the merge nets to zero
against origin/main for this PR's own authored lines; the map-* entries just moved underneath
unchanged. Nothing in this PR's own content changed, so there is no new review surface.

Standing items (not re-raised, already resolved on this head)

The doc/code contradiction on the overlay's gitignore behavior (docs/conventions/config-cascade/README.md:526
vs digest-policy.mjs's overlayApplies()) that three earlier rounds of this lane and the security
lane flagged was resolved in 91f51b91d by changing the code to match the documented contract, and
the .claude-submodule overlay guard added in 509abc8 is sound. The one open doc-completeness note
from this lane's prior pass — docs/conventions/review-digest.md:55 not yet naming the submodule/tracked-entry
guard — remains open as of this head; not re-posting a duplicate since it was already flagged at c221d9e.
· Branch: feat/1217-review-explain-change

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 4s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Read .claude-lane/incremental.diff for the scoped change set (since 0dc4e081)
  • Review the two changed files for correctness, design, conventions, error handling, maintainability
  • Post inline findings (none met the bar)
  • Post summary comment

Scope

Per REVIEW.md, security findings are the hosted security lane's job, not this one. Incremental scope per the lane header: only the diff since 0dc4e081 in docs/conventions/review-digest.md and plugins/review/CHANGELOG.md. Every other file was already reviewed at that head.

What changed in this increment

Both edits close the doc-completeness note this lane raised at c221d9e (and the one four review passes earlier, on the config-cascade/README.md row): the overlay paragraph and the 0.38.0 changelog bullet didn't yet mention the .claude-submodule/tracked-entry guard added in 509abc8.

  • docs/conventions/review-digest.md:54-56 now reads: "...is refused when .claude or the overlay is a symlink, or when .claude is itself a tracked entry (a submodule or a tracked file) or holds a .git; one that is not gitignored is reported and still applied." I checked this against overlayApplies() in digest-policy.mjs:198-220 line by line: the tracked-entry/.git check, the symlink/escape check, and the not-gitignored-still-applies tail all match the code's actual order and effect. This is the fourth and, on this reading, now-accurate pass at this exact paragraph.
  • plugins/review/CHANGELOG.md:24-26 adds "and not when .claude is a submodule or tracked entry" to the same bullet. The .gitmodules addition to CONFIG_PATHS (from the same prior commit) isn't named explicitly here, but it's still covered by the bullet's existing generic clause ("a change to any digest config file always fires the risk-path trigger") since .gitmodules is in that list — not a gap worth a finding.

No findings

Both changes are accurate, narrowly-scoped doc fixes with no code behavior change in this increment. No correctness, design, or convention issues found.

Not re-litigated

Codex's P1 note (no caller wires digest_policy into unrequested PR lifecycle events yet) is unchanged by this increment and remains explicitly disclosed as out of scope for this slice in the PR's own Related section.
· Branch: feat/1217-review-explain-change

Keep review 0.38.0 above main's 0.37.2 and both changelog entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 5s —— View job


Security review (/review:security-review)

Verdict: no new security issues — skip gate applies

The last successful security review on this PR covered commit 0dc4e081 and found none (and itself applied the skip gate against c221d9e8, which found the .claude-submodule overlay guard sound).

git log 0dc4e081..be6fc899 shows:

  • 62337da — this PR's own commit, doc-only: it extends one sentence in docs/conventions/review-digest.md#L54-L56 and one plugins/review/CHANGELOG.md bullet to name the .claude-submodule/tracked-entry guard that 509abc8 already added to digest-policy.mjs's overlayApplies(). No code changed; the sentence now matches the guard that was already in place and already reviewed.
  • 1b8b560 — an unrelated external PR (fix: keep Bash-run plugin scripts out of other plugins' data directories #6065, "keep Bash-run plugin scripts out of other plugins' data directories") brought in by the origin/main merge. It never touches plugins/review/skills/explain-change/, plugins/review/skills/pr-explainer/, or plugins/review/tests/.
  • be6fc89 — the merge commit itself.

I confirmed directly rather than trusting the commit list alone: git diff origin/main...HEAD --stat at the current head is byte-identical (same 21 files, same +1422/-401) to the set already reviewed at every prior head on this PR. None of this PR's trust-relevant surface changed — digest-policy.mjs's base-ref isolation (baseRefOid), cascade/overlay resolution, the .claude-submodule guard, build-digest.mjs's string-coercion and temp-dir/working-tree guard, the template's data-rv-*-only bindings, or the skills' allowed-tools scoping.

Skip gate applies. No new findings.

Standing notes (not new, already resolved or disclosed)

  • The overlay-gitignore and fork-base-ref trust-boundary issues raised across earlier rounds of this PR's review history were resolved by the author (91f51b91d, 509abc8) and confirmed sound in every subsequent pass.
  • Codex's P1 note (no caller wires digest_policy into unrequested PR lifecycle events yet) remains unchanged and is explicitly disclosed as out of scope for this slice in the PR's own Related section.

No command/path injection, XSS, or trust-boundary bypass found in this PR's changes since the last review.

Branch: feat/1217-review-explain-change ·

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 11s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Check skip gate: this head (be6fc89) vs the last head this lane fully reviewed (62337da)
  • Confirm no PR content changed since the last reviewed head
  • No new findings to post

Skip gate applies

git log 62337da..be6fc89 shows one commit beyond the last head this lane reviewed: a merge of
origin/main, bringing in 1b8b560 (#6065, fix: keep Bash-run plugin scripts out of other plugins' data directories) — unrelated work on seven other plugins' scripts.

That merge lands in four files this PR also touches (shared aggregator docs that regenerate across
unrelated PRs): docs/catalog.md, docs/conventions/rendered-views/CHANGELOG.md,
docs/conventions/rendered-views/README.md, and docs/skill-cheat-sheet.md. I checked the content
of each change directly rather than trusting the commit count alone — all four are forwarded from
other, already-merged PRs with no connection to #6065 itself: an education:eli5 →
education:illustrate rename (catalog entry, rendered-views emitter list, cheat-sheet row) and a
new animation:check-prerequisites cheat-sheet row. None of it touches this PR's own files —
plugins/review/**, docs/conventions/review-digest.md, or the review-digest row in
docs/conventions/config-cascade/README.md are byte-identical to 62337da. This PR's own net diff
against origin/main is unchanged in both file list and stats (21 files, +1422/-401, matching this
PR's own changed-files set). Nothing in this PR's own content changed, so there is no new review
surface.

Standing items (not re-raised, already resolved on this head)

The doc/code contradiction on the overlay's gitignore behavior
(docs/conventions/config-cascade/README.md:526 vs digest-policy.mjs's overlayApplies()) that
earlier rounds of this lane and the security lane flagged was resolved in 91f51b91d by changing
the code to match the documented contract. The .claude-submodule overlay guard added in
509abc8 is sound, and the doc-completeness gap this lane flagged at c221d9e (the overlay
paragraph and CHANGELOG bullet not naming the submodule guard) was closed in 62337da. No open
findings remain from this lane's review history on this PR.

Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 10s —— View job


Security review (/review:security-review)

Verdict: no new security issues — skip gate applies

The last successful security review on this PR covered commit be6fc899 and found none (the overlay-gitignore and fork-base-ref trust-boundary issues raised in earlier rounds were already resolved by the author at 91f51b91d/509abc8, and confirmed sound in every subsequent pass).

git log be6fc89..HEAD shows two commits beyond that head:

Restricting to this PR's own changed files: git diff origin/main...HEAD --stat at the current head is byte-identical (same 21 files, same +1422/-401) to the set already reviewed at every prior head on this PR. The only overlap with files #6082 touched is two shared-aggregator doc lines this PR also carries:

Both are additive, non-conflicting edits to an unrelated plugin's description carried in by the merge; neither touches review-digest/explain-change content. I confirmed directly that none of this PR's own security-relevant surface changed in this range:

git diff be6fc89..HEAD -- plugins/review/skills/explain-change/scripts/digest-policy.mjs \
  plugins/review/skills/explain-change/scripts/build-digest.mjs \
  plugins/review/skills/explain-change/SKILL.md

→ empty. digest-policy.mjs's base-ref isolation (baseRefOid), cascade/overlay resolution, the .claude-submodule guard, build-digest.mjs's string-coercion and temp-dir/working-tree guard, the template's data-rv-*-only bindings, and the skills' allowed-tools scoping are all unchanged.

Skip gate applies. No new findings.

Standing notes (not new, already resolved or disclosed)

  • The overlay-gitignore and fork-base-ref trust-boundary issues raised across earlier rounds of this PR's review history were resolved by the author (91f51b91d, 509abc8) and confirmed sound in every subsequent pass.
  • Codex's P1 note (no caller wires digest_policy into unrequested PR lifecycle events yet) remains unchanged and is explicitly disclosed as out of scope for this slice in the PR's own Related section.

No command/path injection, XSS, or trust-boundary bypass found in this PR's changes since the last review.
· Branch: feat/1217-review-explain-change

@claude

claude Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 39s —— View job


Code review complete

  • Read repo instructions (CLAUDE.md/AGENTS.md, REVIEW.md) and invoke /review:code-review
  • Check skip gate: this head (7003422) vs the last head this lane fully reviewed (be6fc89)
  • Confirm no PR content changed since the last reviewed head
  • No new findings to post

Skip gate applies

git log be6fc89..7003422 shows two commits beyond the last head this lane reviewed: a merge of
origin/main bringing in d022d4b (#6064, build(shared-lib): migrate hook-utils.sh to the regen flow and retire the sync machinery), and a second merge bringing in d301b20 (#6082,
feat(speech): add an optional elevenlabs backend with a cost estimate before every call) —
unrelated work on the speech plugin and the shared-lib hook sync machinery.

Those merges touch a few files this PR also carries (shared aggregator docs that regenerate
across unrelated PRs): docs/catalog.md and docs/skill-cheat-sheet.md. I checked the content of
each change directly rather than trusting the commit count alone — both deltas are forwarded from
#6082's own speech plugin catalog/cheat-sheet entries, with no connection to this PR. git diff be6fc89...7003422 restricted to this PR's own files (plugins/review/**,
docs/conventions/review-digest.md, docs/conventions/config-cascade/README.md,
docs/conventions/rendered-views/**, lib/html-escape.test.sh) is empty, and git diff origin/main...HEAD --stat is still the same 21-file, +1422/-401 set already reviewed at every
prior head. Nothing in this PR's own content changed, so there is no new review surface.

Standing items (not re-raised, already resolved on this head)

The doc/code contradiction on the overlay's gitignore behavior
(docs/conventions/config-cascade/README.md:526 vs digest-policy.mjs's overlayApplies()) that
earlier rounds of this lane and the security lane flagged was resolved in 91f51b91d by changing
the code to match the documented contract. The .claude-submodule overlay guard added in
509abc8 is sound, and the doc-completeness gap this lane flagged at c221d9e (the overlay
paragraph and CHANGELOG bullet not naming the submodule guard) was closed in 62337da. No open
findings remain from this lane's review history on this PR.
· Branch: feat/1217-review-explain-change

@kyle-sexton
kyle-sexton merged commit db4cdd3 into main Oct 3, 2026
19 checks passed
@kyle-sexton
kyle-sexton deleted the feat/1217-review-explain-change branch October 3, 2026 16:16
kyle-sexton added a commit that referenced this pull request Oct 3, 2026
…ection

main released discovery review (feat(review): add review:explain-change with a digest policy and an interactive view (#6018); fix(discovery): run research gates as one plain command); this branch's entries for those plugins move one patch above.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Oct 3, 2026
No related issue: wave 3 PR 2 of the CI performance program (precise
test selection). Refs #3932, #6021.

## Summary

`scripts/affected-tests.sh` now selects the suites a change runs or
reads, in the change's own language, instead of every suite that
mentions a file name anywhere, every shell suite of a touched plugin,
and three always-run suites. Over the 895 pull requests merged to main
in the 7 days to 9671ece it selects 9,534 suites where main's selector
selects 34,365, within 5% of the design model's 9,095, and both real
main breaks from the window are still selected. Every Node suite it
selects also runs: a Node suite that CI runs through a sibling
`.test.sh` brings that wrapper (R9).

## Fix

Rules (full text in the script header):

- **Same-language edges (R3).** A file in the changed file's language
that names it on a code line is a dependent, transitively.
- **Another language counts only where the line runs or loads the file
(R4):** an interpreter or process API on the line, matched as a word
(the `.sh` of `x.sh` is not one), or a path to the file. A chain takes
at most one such transition. A changed data file (C#, Markdown, YAML,
...) reaches code of any language that names it without spending the
transition.
- **Comment lines never count**, in suites and in code. `# shellcheck
source=` and JSDoc `@import` / `import()` still count.
- **Manifests select no suite through a mention.** plugin.json,
marketplace.json, hooks.json, settings.json, package.json,
package-lock.json, CHANGELOG.md and LICENSE reach a suite only through
R1, R2 or a declared scope; their gates own them.
- **Ambiguous names count only when the mention resolves.** This covers
basenames two or more files carry, plus README.md, SKILL.md, AGENTS.md,
CLAUDE.md and index.md. A mention resolves when it comes from the file's
own directory; when it is a bare name from a directory above the file
with no other file of that name below; when it ends in the file's
shortest unique path suffix of two or more components; or when it is a
path relative to a directory below the root that holds both files and
has a directory in it (`$PLUGIN_DIR/skills/interview/SKILL.md`). A
name-only path (`$SKILL_DIR/SKILL.md`, `$T/README.md`) or a
root-relative one (`$ROOT/.github/workflows/ci.yml`) does not resolve,
because tests build those paths under temporary directories; the suites
the strace saw reading such files declare them.
- **No Python import rule.** `import foo` does not name `foo.py`, as in
design rules S1-S9. A module that only an import reaches is unmapped and
falls back to the Python corpus (S9).
- **Declared scopes replace R8's whole-plugin rule and the always list
(S7).** A suite that reads files it never names declares them in its
leading comment block, before any code or docstring: `# test-scope:
<glob> [<glob>...]`, one or more lines. 87 suites carry 132 globs,
seeded from an strace of every suite. `scripts/affected-tests.test.sh`
also declares the live files its LIVE cases find by glob (the github
`advise` and planning `interview` skill bodies, the autonomy reference
docs), so renaming or deleting one runs it; its reference-YAML case runs
in a fixture on probe files, so that YAML stays unmapped. A changed
suite whose glob matches no file fails the run.
`scripts/affected-tests-always.txt` is deleted, and `--with-always` is
accepted and does nothing.
- **A wrapped Node suite brings its wrapper (R9).** A selected
`<stem>.test.js` or `<stem>.test.mjs` whose directory holds
`<stem>.test.sh` selects that wrapper too, after every other rule and
before `--shard`. CI runs such a suite only through the wrapper
(`scripts/run-outside-node-suites.sh` reports it `OWNED` and runs
nothing), and the walk stops at a reached suite, so a change reaching
`exec-bash.resolver.test.mjs` through `lib/exec-bash.mjs` used to select
the suite, not the wrapper, and CI ran neither.
- **An unmapped file runs only its own language's suites.**
`--unmapped-corpus` keeps the report, adds that language's corpus and
exits 4. Wiring it into `ci.yml` is PR 3's job.
- **No-suite list.** `plugins/*/evals/*` replaces the eval fixture
entries. The Python module entries main listed
(`plugin_cache_versions.py`, `discover.py`, `docs_crosscheck.py`, the
`session_bridge.py` copies) are removed, so a change to one is unmapped
and falls back to the Python corpus instead of selecting nothing.
`plugins/performance/lib/spawn_noise.py` (a copy nothing imports) is
added.
- **`--replay <range> [--against <ref>]`** reruns the selector on each
first-parent commit against its parent in a scratch clone, with this
tree's no-suite list and declared scopes (handed to older commits
through `AFFECTED_TESTS_SCOPES`), and, with `--against`, prints only the
suites the two selectors disagree on, `<ref>` using its own headers.
- **Releases.** The 30 plugins whose suites gained a header get a patch
bump and a CHANGELOG entry naming those suites; nothing they run
changed.

## Verification

### Replay: 7 days of merged pull requests

Every first-parent commit on main from 2026-09-26 to 9671ece (900; 895
change a file), selected against its parent three ways: main's selector
at 9671ece, this PR's selector, and the design's model (`selmodel.py`,
the script that produced the design's section 3 figures) re-run on the
same commits.

| measure (895 PRs) | main | this PR | design model |
|---|---|---|---|
| suites per PR, p50 / p90 / p95 / max | 22 / 83 / 142 / 523 | 5 / 24 /
33 / 281 | 4 / 23 / 31 / 273 |
| suites selected, total | 34,365 | 9,534 | 9,095 |
| total with the language-scoped unmapped fallback (S9, PR 3) | 38,094 |
15,290 | 27,360 |
| total as CI runs it today (unmapped: whole shell corpus) | 43,852 |
21,686 | 32,445 |
| shell suites selected | 32,420 | 7,946 | 7,352 |
| PRs with an unmapped file | 22 | 26 | 48 |
| PRs with no shell change that start shell suites | 491 of 491 (10,269)
| 351 of 491 (1,560) | 312 of 491 (1,361) |
| PRs changing no code that start any suite | 356 of 356 (7,181) | 233
of 356 (974) | 203 of 357 (856) |
| PRs selecting no suite | 0 | 127 | 157 |

The design's section 3 figures (p50 3, p95 28, 7,348 total, 13,371 with
the fallback) came from a different window, 826 PRs to 2026-10-02
20:52Z. The same model gives the last column on this window, so that
column is the target the 5% bar applies to.

### Design targets vs measured

| | this PR | design model | difference |
|---|---|---|---|
| suites selected, total | 9,534 | 9,095 | +439 (+4.8%) |
| p50 / p90 | 5 / 24 | 4 / 23 | +1 / +1 |
| p95 | 33 | 31 | +2 (+6.5%) |
| max | 281 | 273 | +8 (+2.9%) |

The total is within the 5% bar. p50 is 1 suite over the model and p95 2,
and the declared scopes account for both: selections only this PR makes
are 713 through a declared scope, 41 wrappers (R9), 31 through a mention
and 16 siblings; selections only the model makes are 362 (its guessed
plugin scans 190, its interpreter test matching the `.sh` of a file name
116, siblings 56). Without the 713 declared-scope selections, p95 would
be 30. The declared scopes are what the strace saw the suites read, plus
the live files `scripts/affected-tests.test.sh` reads by glob (18 of the
713: edits to the github `advise` and planning `interview` skill bodies
and the autonomy reference docs).

What dropping the Python import rule cost, against the previous head:
903 (PR, suite) selections over 75 suites, 264 of which main also made;
the strace saw the suite read a changed file in 60 of them. Where
nothing else maps the module (`discover.py`, `docs_crosscheck.py`,
`plugin_cache_versions.py`), the change is unmapped and the S9 fallback
runs the Python corpus. Where the module maps through a sibling or a
mention (`hygiene.py`, `destructive_guard.py`), the suites that only
import it are not selected; the design accepts that and catches it with
the twice-daily full run and the trace audit (PR 4).

### The C# fixture case

`plugins/code-metrics/scripts/fixtures/sources/CmSample.cs` goes from 14
suites to 7. `dispatch.test.sh`, `audit-complexity.test.sh` and
`audit-type-debt.test.sh` name the file. `audit-coverage.test.sh`,
`audit-duplication.test.sh` and `audit-size.test.sh` declare
`plugins/code-metrics/scripts/fixtures/*`, which the strace saw them
read. `setup-check.test.sh` sits beside `setup-check.sh`, which copies
the plugin's `scripts/`. The design predicted 4; the three declared
scopes make the difference.

### The two real main breaks are still selected

- 88dd7e1 selects `plugins/github/github.test.sh` (its header declares
`plugins/github/*`) and
`plugins/planning/tests/interview-defenses.test.sh`.
- e544012 selects `plugins/planning/tests/interview-defenses.test.sh`:
`$PLUGIN_DIR/skills/interview/SKILL.md` resolves to the changed file.

The suite also pins both against the live tree.

### Trace audit of every drop

Every suite ran under strace to record its file reads. Of 25,352 (PR,
suite) pairs this PR drops against main, over 700 suites, the strace saw
the suite read a changed file in 1,004 pairs over 53 suites:

- **Manifest identity reads:** `install_state`, `sync-run`, the planning
`surface`/`watch` suites and the `*-format` hook suites read a plugin's
name or version from `plugin.json`, or Node reads the root
`package.json` while resolving modules. The root package files are pins
that PR 3 routes to the Node lanes (S8).
- **Live gates CI also runs as whole-tree gate steps:**
`check-loop-lane-floor-drift`, `check-fixture-git-isolation`,
`check-summary-reader-parity`.
- **`scripts/affected-tests.test.sh`:** its LIVE cases run the selector,
whose `git grep` reads every file.
- **Plugin copies and walks:** `abort-boundary`,
`check-guardrails-ps-differential` and `test_kill_switch_probe.py` read
a README or changelog, which does not change what they test.
- **Python imports:** the 60 pairs above, where a test imports a changed
module that something else maps.
- **Relations only today's tree has**, since the trace ran on today's
tree and each PR replays on its own: `interview-defenses.test.sh` began
naming `context/surface.md` after 7 of its PRs.
`check-prerequisite-probes.test.sh` has been deleted since.

The full list, each suite with its PR count, its trace verdict and
main's reasons for selecting it, is in [this
comment](#6059 (comment))
(57 KB, too large for this body).

### Every selected Node suite runs (R9)

The figures above are the previous head's per-PR selections with R9
applied. A real replay of this head's selector over the same 900
commits, with the same inputs as the previous head's replay, removes
nothing and adds exactly those 41 (PR, suite) selections over 33 PRs,
each the `.test.sh` wrapper of a Node suite already selected:
`lib/exec-bash.resolver.test.sh` 31,
`plugins/guardrails/hooks/exec-bash.resolver.test.sh` 7,
`plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.sh`
3. No PR's exit code or unmapped set changes; three `--unmapped-corpus`
runs also gain the wrappers of the Node suites their corpus adds.

Selected Node suites that CI runs nowhere (outside the registered
packages and the four sub-projects, with a wrapper that is not
selected): 41 (PR, suite) pairs over 33 PRs at the previous head, 5 of
which main covered by selecting the wrapper; 0 at this head.

On today's tree, `lib/exec-bash.mjs` plus
`plugins/guardrails/hooks/exec-bash.mjs` selects both
`exec-bash.resolver.test.mjs` suites and both wrappers, and
`run-outside-node-suites.sh --paths` on the two Node suites (CI's exit-3
branch) reports each `OWNED` by its wrapper and exits 0. The workflow
scripts of review, planning, testing, discovery and multi-agent and the
autonomy `fixture-harness.mjs` and `resolve-prerequisites.mjs` select 9
wrapped Node suites, each with its wrapper.

### Local runs

WSL (Ubuntu 26.04) at the head, main merged:

- `scripts/affected-tests.test.sh`: PASS=134 FAIL=0 (the 4 Python-import
cases are gone; the R8 cases now build suites with headers, including a
declaration below code that declares nothing and a stale glob that fails
only the run changing its suite; the R9 case, a Node suite reached
through a mention, fails on the previous head's selector, 133/1)
- `scripts/lib/gate-entry.test.sh`: 33/0
- Headers read back from the 87 suites equal the former list entry for
entry, plus the three globs `scripts/affected-tests.test.sh` adds for
the live files it reads.
- The selector on `plugins/github/skills/advise/SKILL.md`,
`plugins/planning/skills/interview/SKILL.md` and an autonomy reference
doc selects `scripts/affected-tests.test.sh` through its header; the
reference YAML (`plugins/toolchain/reference/ecosystems/go.yaml`,
`docs/conventions/ecosystem-commands/examples/go.yaml`) stays UNMAPPED
at exit 1.
- shellcheck clean on the selector and the 81 changed shell suites; the
pinned ruff check passes on the 6 changed Python suites.
- `check-changelog-parity.sh` `--check`, `--check-bump`,
`--check-preserved` and `--check-order` pass against origin/main. Where
main released a plugin this PR also releases (planning in #6063;
animation in #6081; code-metrics, harness-ops, repo-hygiene,
session-flow and source-control in #6065; actionlint, animation,
autonomy, context-guard, guardrails, harness-ops, source-control, speech
and testing in #6064; speech in #6082; review in #6018; discovery in
#6088), this PR's entry sits one patch above main's.
- Main's #6064 deleted `scripts/lib/sync-cluster.sh`, the only file the
`scripts/lib/sync-*.sh` glob in `scripts/affected-tests.test.sh`'s
header matched; the glob is dropped, since the selector fails (exit 2)
on any diff that changes a suite declaring a glob that matches nothing,
as it did on the merge commit alone.

## Related

- #3932: the CI performance program.
- #6021 (wave 3 PR 1, `ci.yml` job rename) has landed. It edited
`scripts/affected-tests-always.txt`, and this PR keeps that file
deleted.
- PR 3 should:
- run `--unmapped-corpus` in place of the whole-shell-corpus fallback,
which gives the 15,290 figure above;
- route the pins (root `package*.json`, `.node-version`, Python pins) to
their lanes (S8);
  - drop `--with-always` from `ci.yml`;
  - update the `ci.yml` comment that names `affected-tests-always.txt`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: ksextonmelodic <ksextonmelodic@gmail.com>
kyle-sexton added a commit that referenced this pull request Oct 3, 2026
… a recording link, and publish as an Artifact by default (#6102)

Closes #5856

## Summary

Completes the `review:explain-change` digest on top of the core from
#6018:

- **Fresh-context risk-map check.** Step 3 of the skill dispatches one
subagent with a fixed brief that carries only the pull request number
and repository. It never gets the record, the builder's risk rows, or
its reasoning. The skill then marks each row `agreed`, `disputed` (the
row and its level stay, with the checker's level and reason), `added`
(an area only the checker named), or `unchecked` (no check ran). Rows
are never dropped. The page shows a Check column.
- **Optional quiz.** `--quiz`, or a reader's request, adds three to five
questions to the record and the page. The reader ticks choices. The
copied or saved reply carries only builder ids
(`quiz-1-questions-<q>-choices-<c>`). With no request, neither the
record nor the page has the section.
- **run-e2e recording link.** When `/testing:run-e2e` recorded the pull
request's head (`headRefOid`), the record links the recording and the
page shows its path as text.
- **Artifact by default.** `digest-policy.mjs` resolves `medium` to
`artifact` when no layer sets it. `medium: file` in
`~/.claude/rendered-views.md` keeps the page local. The rendered-views
README and the review-digest convention now state the `artifact`
default.

## Fix

- `build-digest.mjs` `shapeDigest` adds `risks[].check` (limited to the
four values, anything else becomes `unchecked`), `risks[].checker`, and
turns `recording` and `quiz` into lists of zero or one section. A list
with no rows renders nothing, so an absent quiz or recording leaves no
heading. The runtime (`lib/view-runtime.js`) and `lib/view-builder.mjs`
are unchanged.
- `templates/digest.html` adds the Check column and puts the recording
and quiz sections inside `data-rv-each` containers. It contains no
data-driven attribute and no non-fragment `href`: the recording path is
shown as text, never as a link.
- Security behavior from #6018 is unchanged: no `--out`, `mkdtemp` plus
`wx` output, refusal of a temp dir inside a working tree, the team layer
read at `baseRefOid`, the overlay guards, `CONFIG_PATHS`, and output
only through the interactive profile. The digest never posts to the pull
request and never gates merge.
- `review` 0.38.0 to 0.39.0 with a CHANGELOG entry. There is also a
rendered-views convention CHANGELOG entry.

## Verification

- `node --test plugins/review/tests/explain-change.test.mjs`: 54 pass, 0
fail. New tests: quiz, recording, and check fields reach the page only
through the data block, including hostile strings in each one; the check
value is limited to the four values; quiz and recording come out as
empty lists when the input lacks them, and an empty page still
validates; quiz and recording headings sit inside their list containers;
the checker brief's only placeholders are `<n>` and `<owner/repo>`; the
default `medium` is `artifact` and a user-global `medium: file`
overrides it.
- `bash plugins/review/tests/explain-change-chrome.test.sh`: all tokens
ok.
- Rendered both pages in Chromium through playwright-cli from a local
HTTP server. Full digest: the Check column, recording, and quiz all
render, and ticking a choice then saving produced `picked:
quiz-1-questions-2-choices-2`. Bare digest: no quiz or recording
heading, and both optional containers have `display: none`.
- `skill-quality` `check-skill.sh` over `plugins/review/skills`:
explain-change PASS with 0 warnings. `check-evals-quality.sh` PASS.
`scripts/check-changed-skills.sh origin/main` PASS.
`check-skill-description-voice.sh origin/main` PASS.
`check-changelog-parity.sh --check-bump origin/main` PASS.
`check-purged-em-dashes.sh` PASS. markdownlint-cli2 with the repo config
is clean on the six changed markdown files.
- Not run: `scripts/check-html-assets.sh`, because htmlhint is not
installed in this worktree (`npm ci` not run).

## Related

- Refs #5835, the parent spec container (wave C1).
- Builds on #6018 (#1217), the explain-change core.
- Not changed in this PR: `~/.claude/rendered-views.md` (`medium: file`
for the owner's personal layer) is user-scope config for the owner to
set.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <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.

review-evidence artifacts: change digest, execution recordings, visual-quality inspector role — plus a shareable artifact-storage seam

2 participants