Skip to content

feat(ci): run plugin eval suites from dispatch and an opt-in PR label - #9835

Merged
kyle-sexton merged 32 commits into
mainfrom
feat/plugin-eval-dispatch-and-label
Oct 28, 2026
Merged

kyle-sexton merged 32 commits into
mainfrom
feat/plugin-eval-dispatch-and-label

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Closes #9704
Closes #9705

Summary

eval-browser-dispatch.yml now runs plugin eval suites two ways, under one job: by hand (dispatch on the default branch), and on the eval: run label for a same-repository, non-draft pull request. Every run checks its suite with validate-cases.py before any model call and keeps --max-cost-usd 5. Each run's JSON result goes through summarize.mjs into the job summary. When the credential secret is empty, the job ends with a notice and runs nothing.

Fix

  • Workflow: adds a pull_request labeled trigger, gated on the label name, a same-repository head and non-draft. On that path, check-trusted-trigger checks the trigger, and the label sender must be a listed human. It also adds a deterministic-only dispatch input and a case input. target must now be a plugins/<name> directory. It keeps the kill switch, the read-only token and no write token. The PR path grants no tools, runs no scaffold, keeps env scrub on and skips the Chromium install. Only eval: run label events share a pull request's concurrency group, so another label cannot replace a pending re-run.
  • .github/scripts/plugin-eval/select-plugins.mjs: maps changed paths to plugins/<p> for skills/, agents/ and evals/. The changed paths come from the checked-out head's own git diff against its merge base, so the check and the run read the same commit. It is an allowlist: a selected plugin runs only when every changed path is under evals/, or is markdown under skills/ or agents/ that keeps its frontmatter outside the descriptive keys (hooks, allowed-tools, shell and the rest) and adds no ! shell preprocessing. Frontmatter is read only in a plain shape (column-0 keys, indented continuations, closed fences); any other shape, a YAML alias in a non-descriptive key, or a path git quoted refuses the run.
  • .github/scripts/plugin-eval/deterministic-suite.mjs: copies the suite without cases that have an llm or baseline grader. The run then uses --eval-dir evals-deterministic.
  • .github/scripts/plugin-eval/summarize.mjs: applies the reference/ci.md parser order (partial, then skipped paid graders or errored runs, then a missing delta) and marks each such case NOT COMPARABLE, with no score. Otherwise each case gets PASS or FAIL against the run's threshold. Each case also gets a health line: whether the skill loaded in the with-arm and how many runs fired it. The last line is Result: <verdict>.
  • lane-funding.json adds pull_request to the eval lane's triggers. Loop-lane convention §7 documents the label (12.5.0).

Verification

  • actionlint .github/workflows/eval-browser-dispatch.yml: exit 0. zizmor: no findings.
  • node --test in .github/scripts/plugin-eval: 28 pass. These include the parser fixtures (scored, partial, skippedPaidGraders, errored; the last three print NOT COMPARABLE), the path filter (plugins/testing/evals/x selects plugins/testing only, and docs/ alone selects nothing), and the refusals: a script beside an eval change, bin/, commands/, non-markdown under skills/, a quoted path, frontmatter shapes a YAML reader parses differently (indented block, ? key, flow mapping, blank line before the fence), an alias, and a SKILL.md hooks: change read from a real git repository. None of the repository's 408 skill and agent files is refused on a body-only edit.
  • node --test in .github/scripts/work-resolve-issue: 291 pass.
  • .github/actions/resolve-config callers.test.mjs: 181 pass (lane-funding catalog).
  • scripts/check-affected.sh --base origin/main --no-fetch: 9 passed, 4 skipped.
  • No eval run was dispatched. test(ci): dispatch the generalized plugin eval workflow once and check its summary #9713 covers the first live dispatch.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 24, 2026 12:58
eval-browser-dispatch.yml now validates each suite with validate-cases.py
before any model call, offers a deterministic-graders-only dispatch input,
and runs on the `eval: run` label for a same-repository, non-draft pull
request: each plugin whose skills, agents or evals the PR changes, with
deterministic graders only, no granted tools, no scaffold, env scrub on and
--max-cost-usd 5. summarize.mjs writes a with-versus-without table with
PASS, FAIL or NOT COMPARABLE per case and a skill health line per case to
the job summary. An empty credential secret ends the job with a notice.

report-outcome adds the label when it opens a lane PR whose issue's
done-when asks for a measured eval pass (eval-pass.mjs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUS7XB8hUjcDnuSYLqJRzd
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 25, 2026 01:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 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-25T01:51:55.343453Z 9216719 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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-review: 1 inline finding(s) on 921671976ffc0d82874b9f56774acc29d5dc7e1f; the summary is in this lane's pull request comment.

claude-lane: claude-review

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 5450209a6d4217b3c915a9984381a8bc768dae8b: 0 finding(s), 0 as inline comments.

Incremental review of the change since ded9efc, in the four files listed. I found nothing to flag.

What changed: allowed_tools moved out of CASE_SAFE_KEYS and out of CASE_SAFE_EXECUTION_KEYS in select-plugins.mjs. A changed prompt.md or case.yaml that adds or edits allowed_tools is now refused. The workflow comment and the loop-lane README were updated to match.

What I read:

  • Refusal logic: caseRefusal (select-plugins.mjs:247-280) compares each key outside the safe sets between base and head. Top-level allowed_tools in either file type now produces changes allowed_tools. execution.allowed_tools in case.yaml produces changes execution.allowed_tools. A new file (null base) is treated as all-added, and an unchanged allowed_tools passes.
  • New test: Its six assertions match that logic. They cover edit, add, new file, unchanged, nested and top-level in case.yaml. I did not run the tests.
  • Workflow: The pull request path adds --allow-tools only inside the DISPATCH = true branch (eval-browser-dispatch.yml:529-544), so the new comment ("passes no --allow-tools") is accurate.
  • Cited doc: plugins/evals/skills/plugin-eval/SKILL.md:106 says neither a case nor a skill's allowed-tools can widen the grant, which backs the comment.
  • Other references: No other file under .github/scripts/plugin-eval references allowed_tools, so nothing else depended on the old safe-key entries.

Closing issues (#9704, #9705): This increment doesn't touch their criteria. It tightens the allowlist and doesn't alter the path filter or the summary parser.

Trusted paths: None of the four files matches a trusted-path glob. The change narrows what a pull request can pass through to the eval run, so it tightens the gate and does not widen it.

Spend: None. No cost or billing change.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-security-review: 2 inline finding(s) on 921671976ffc0d82874b9f56774acc29d5dc7e1f; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 5450209a6d4217b3c915a9984381a8bc768dae8b: 0 finding(s), 0 as inline comments.

Incremental review of the change since ded9efc: select-plugins.mjs, its test, the eval-browser-dispatch.yml comment, and the loop-lane README.

The change removes allowed_tools from CASE_SAFE_KEYS and from CASE_SAFE_EXECUTION_KEYS. A pull request that adds or edits a case's allowed_tools is now refused. select-plugins.mjs:257 and :265 compare every key outside those sets, so a case's top-level allowed_tools and execution.allowed_tools are both caught. The alias check at :275 also now covers allowed_tools. The workflow and README edits are comments and docs only.

This narrows what a pull request can change, so it is not a trust-control widening. The new test covers the add, the edit and the unchanged case for both prompt.md and case.yaml.

No security findings.

critical-findings: 0

Not checked: I didn't read the plugins/evals/skills/plugin-eval/SKILL.md paragraph the new comments cite, or run the tests.

Comment thread .github/lane-funding.json Resolved
Comment thread docs/conventions/loop-lane/README.md Resolved
kyle-sexton and others added 2 commits October 25, 2026 03:18
The PR path's refusal was a denylist of load-time paths, decided from the
live PR file list rather than the commit that runs. select-plugins.mjs now
lets a selected plugin through only when every changed path is under
evals/, or is markdown under skills/ or agents/ that keeps its
non-descriptive frontmatter (hooks, allowed-tools, shell and the rest) and
adds no ! shell preprocessing. The changed paths come from the checked-out
head's own git diff against its merge base, so the refusal and the run read
the same commit and the files API cap no longer applies.

The concurrency group is keyed on the label, so another label event can no
longer replace a pending eval re-run.

The lane auto-label is removed (eval-pass.mjs, the report-outcome.mjs block
and their tests, the docs sentence), and the label sender must be a listed
human: neither issue asks for an unattended opt-in.

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-security-review: 2 inline finding(s) on 25b3ab251ebbead6231beb41ba81631b18c52ed2; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

kyle-sexton and others added 2 commits October 25, 2026 03:27
git diff --name-only quotes a path with unusual bytes and wraps it in
double quotes, which the plugin path pattern did not match, so a quoted
load-time path beside an eval change passed. Such a path is now refused,
and changed paths are no longer trimmed before they are checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frontmatter reader took only column-0 `key:` lines, so an indented
block, a `? key` complex key, a flow mapping or frontmatter after a blank
line hid an added hooks key from the comparison. Only a plain shape is now
read (a `---` first line, a closing fence, unique column-0 keys with
indented continuation lines); any other shape refuses the file. A YAML
alias in a non-descriptive key is refused too, since it can pull in an
anchor changed under a descriptive one. None of the 408 skill and agent
files in the repository is refused on a body-only edit.

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-security-review: 1 inline finding(s) on f40b86466727683f528ba94e02f52d71130c5b78; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

…t split

frontmatter() split only on \r?\n, but a YAML reader also breaks lines on
a bare \r and on U+0085, U+2028 and U+2029, so a hooks key after one was
hidden from the comparison. A file holding any of them is now refused.
None of the 408 skill and agent files in the repository holds one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@melodic-automation-lanes melodic-automation-lanes Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 25, 2026
@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

The pr-unblock lane stopped on this pull request and needs a person: the merge stages a change under .github/workflows/, which the lane does not push.

  • Reason: workflow-staged
  • Resume key: 2fc9ba91eaae5f93d0887e0910258d3472105a70 (the head the lane stopped on)

To resume, deal with the reason above, remove the needs-human label (and do-not-merge, if set), then dispatch the pr-unblock workflow with pr-number=9835 and activity=resolve-conflicts:

gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=9835 -f activity=resolve-conflicts

kyle-sexton and others added 2 commits October 27, 2026 17:23
…f the public log

On a pull request run the cases come from the PR, so the CLI's console
output can carry model text. Write it to a file nothing uploads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/conventions/loop-lane/CHANGELOG.md
@melodic-automation-lanes melodic-automation-lanes Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 27, 2026
@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

The pr-unblock lane stopped on this pull request and needs a person: the merge stages a change under .github/workflows/, which the lane does not push.

  • Reason: workflow-staged
  • Resume key: 7d211e6f7eee2e22fc20aa590db914a313893876 (the head the lane stopped on)

To resume, deal with the reason above, remove the needs-human label (and do-not-merge, if set), then dispatch the pr-unblock workflow with pr-number=9835 and activity=resolve-conflicts:

gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=9835 -f activity=resolve-conflicts

@kyle-sexton kyle-sexton removed the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 27, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-security-review: 1 inline finding(s) on e7a44415eba901663658a466615908e3bc316d2a; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

kyle-sexton and others added 2 commits October 27, 2026 21:03
…cy before a PR eval

The pull request path denied Read of /home/runner/actions-runner, a guess at the
runner's install directory. Read it from the Runner.Worker process above the step,
deny it with both known layouts and the job's _temp and _actions directories, and
stop the job when it is not found. A probe step then has one Claude Code run Read
/proc/self/environ and the runner's .runner file, and stops the job unless the
result's permission_denials records both reads as denied.

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-security-review: 1 inline finding(s) on 486223fc3acb01c371212c79a4736d1339861802; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

@github-actions github-actions Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review holds this pull request for a person (critical-security) at 486223fc3acb01c371212c79a4736d1339861802.

The eval: run pull-request path lets any file under plugins/<p>/evals/ through select-plugins.mjs uninspected. A case's plugins: key can load a stub plugin from that directory, and the stub's SessionStart hook runs under --trust-plugin outside the sandbox. The hook can read the parent claude process's environment and send out MAINTENANCE_CLAUDE_CODE_OAUTH_TOKEN. The path is reachable by a PR from any listed actor, including the issue-resolution lane's App bot. The only control left is a human label click, and the verifier found nothing else blocking it.

claude-lane: claude-security-review

@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Oct 28, 2026
kyle-sexton and others added 2 commits October 27, 2026 23:51
…d unchecked plugins

select-plugins.mjs let any path under a selected plugin's evals/ through, and a
case's plugins: key can load a stub plugin there, or another plugin, whose
hooks run outside the sandbox and can read the eval credential.

- An evals/ path passes only as prompt.md, case.yaml, graders/*.md or
  samples/**/*.json. Scaffold scripts, fixtures (scaffold scripts run
  fixtures/ seed scripts) and stub plugin files are refused.
- A changed prompt.md or case.yaml keeps env, context (scaffold_script),
  growthbook_overrides and every other run key; case.yaml's execution block
  may change only prompt, max_turns, timeout_seconds and allowed_tools.
- Every case in a selected plugin loads only the plugin's root or an
  in-plugin directory with no changed path under it; anything outside the
  plugin is refused.

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-review: 1 inline finding(s) on 69ea304bcc518f6cda3cf640f3808b90a4c8d0e9; the summary is in this lane's pull request comment.

claude-lane: claude-review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-security-review: 2 inline finding(s) on 69ea304bcc518f6cda3cf640f3808b90a4c8d0e9; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

kyle-sexton and others added 3 commits October 28, 2026 00:03
…p and Glob denials

- The results artifact keeps its eval-browser-<run>-<attempt> name, the
  family .github/artifact-retention.json declares, so the retention check
  (lint-repo, test-bash) passes again.
- The pull request probe now also asks for a Grep of /proc/self/environ and
  a Glob under /proc/self, requires a recorded denial for each, and fails if
  the result holds the credential anywhere. Read deny rules reach Glob and
  Grep only best-effort, so a call the policy misses stops the job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…! line

A kept ! preprocessing line can start running when the text around it
changes, such as an enclosing fence removed, with the line itself
unchanged. A changed skill or agent file that already has one must keep
its body; safe frontmatter keys may still change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A changed prompt.md or case.yaml must keep allowed_tools, top level and
under execution, so a case changed in a pull request cannot ask for Bash or
another gated tool; such a change runs by dispatch. Unchanged cases that
list Bash get no Bash from the label either: a pull request run passes no
--allow-tools, and a case cannot widen that grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton removed do-not-merge Hard merge gate: do not merge while applied. needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. labels Oct 28, 2026
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 3c10daa Oct 28, 2026
78 of 79 checks passed
@kyle-sexton
kyle-sexton deleted the feat/plugin-eval-dispatch-and-label branch October 28, 2026 05:07
@github-actions

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 0, survived 1, timeout 0, no coverage 0, invalid 0, not run 7.
Covered-code score 0%; mutation score 0%.

File Killed Survived Timeout No coverage Invalid Not run
.github/scripts/plugin-eval/deterministic-suite.mjs 0 1 0 0 0 1
.github/scripts/plugin-eval/select-plugins.mjs 0 0 0 0 0 1
.github/scripts/plugin-eval/summarize.mjs 0 0 0 0 0 2
.github/scripts/pr-unblock/lane-hold.mjs 0 0 0 0 0 1
.github/scripts/pr-unblock/verify-notes.mjs 0 0 0 0 0 1
scripts/validate-plugin-contracts.mjs 0 0 0 0 0 1

Surviving mutants

Location Operator Disposition Cause
.github/scripts/plugin-eval/deterministic-suite.mjs:47 LCR productive input-gap

The run's summary has the full report.

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.

feat(ci): opt-in PR label trigger for the changed plugin's eval suite feat(ci): validate, gate and summarize dispatched plugin eval runs

1 participant