feat(evals): add MCP server mocks and fix the cases that measured nothing - #159
Merged
Merged
Conversation
ryzizub
marked this pull request as ready for review
September 21, 2026 11:39
ryzizub
marked this pull request as draft
September 21, 2026 11:40
Adds evals/mocks/very-good-cli/ with fixed mocks for create, packages_get,
test, and packages_check_licenses, plus the real tools/list response as
_tools.json so mocked tools carry their production schemas.
Registration is confirmed: a run now reports
mocked: very-good-cli(create=fixed, packages_check_licenses=fixed,
packages_get=fixed, test=fixed)
No case calls them yet. Every prompt still says the session cannot run
anything and every allowed_tools still lists only [Read, Glob, Grep, Skill],
so the tool-driven skills stay graded on narration until both change.
The SessionStart hook still fires under mocks. check_vgv_cli gates on
`command -v very_good`, not on MCP availability, so the three prompts that
tell the model to ignore the startup notice keep that sentence.
CI: evals/mocks/ is a shared input, so a change to it widens scope to all
skills the way _fixture/ does. The scope job previously emitted any changed
evals/ subdirectory as a --tag, which would have made `mocks` a tag matching
no cases; it now requires the directory to hold */case.yaml. That guard runs
as an if-statement because the && chain it replaced could return non-zero
from the loop body and trip `set -e`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured on Claude Code 2.1.270, model and judge claude-sonnet-5. bloc-writes-sealed-events-and-states: --runs 3 scored 1.00, 3/3, every grader passing. The CI reading of 0.60 with four simultaneous failures was run-to-run variance. Case, graders and skills/bloc/SKILL.md unchanged. Two things block a mocked very-good-cli call, both before any grader runs: 1. A mocked tool needs an --allow-tools grant. The published docs say it does not. Listing it in a case's allowed_tools is necessary and not sufficient. The tool name is the bare mcp__very-good-cli__<tool> form. 2. check-vgv-cli.sh denies every mcp__*very-good-cli__* call in the sandbox. `command -v very_good` succeeds there, since the run inherits the host PATH; `very_good --version` returns nothing under the run's throwaway $HOME. check_vgv_cli reads that as not_installed and denies, so the mock is never reached. Resolving 2 means changing what the hook gates on, which is shipped plugin behavior, so it is documented rather than decided here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A driven call aborted the run at score 0: aborted by mock very-good-cli/packages_check_licenses: the model's call violates expect: directory = (missing) is not a string `expect` treats a missing key as a violation, so listing an optional field makes the mock reject correct calls. Only `create` has required fields (`subcommand`, `name`); the other three tools have none, so their guards are gone. Argument choices belong in a grader, not in `expect`, which aborts. Verified against PR #155: with `unverifiable`, the PreToolUse hook stands aside and the model reaches the mock, calling it with {"licenses": true}. Before #155 the hook denied the call and the mock was never reached. Also corrects the tool name in the docs. The bare mcp__very-good-cli__<tool> form works for --allow-tools and allowed_tools, but the name the model actually invokes, and the one a tool_used grader needs, is the plugin-namespaced mcp__plugin_vgv-ai-flutter-plugin_very-good-cli__<tool>. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hat needed it With check_vgv_cli returning `unverifiable`, warn-missing-mcp.sh emits nothing, so the sentence telling the model to ignore a startup notice has nothing left to guard against. Removed from create-project-scopes-dependency-install-to-the-new-project, which measured 3/3 at 1.00 afterwards, including the no-flutter-create grader that the notice originally tripped. The other two prompts named as carrying this workaround do not. They say the session cannot reach a real toolchain, which is true regardless of the hook, so they are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All numbers measured on claude-sonnet-5 for both model and judge. Adds signal rather than cutting it. A grader that passes in both arms earns no delta but still fails if the skill regresses later, so the free ones that pin a Core Standard or an anti-pattern stay. Four graders are deleted, each for a reason that holds without a score: `uses-pump-app` and `names-all-rights-reserved` duplicate a rubric beside them, `declares-weather-repository` restates the prompt, and `uses-test-widgets` is table stakes for any widget test. Two skills were at fault rather than their cases: - skills/bloc/SKILL.md gains a Core Standard for `build:` constructing the bloc under test. That was the only real difference between the two arms of bloc-tests-with-bloc-test-and-mocktail, which moves Δ +0.38 -> +0.75. - bloc-writes-sealed-events-and-states was grading one of two approaches the skill sanctions. The skill documents a Subclass and a Single Class approach and selects on whether states carry different data; the graders only accept the first, so a status-enum answer followed the skill and failed anyway. The prompt now supplies states that carry different data. Readings went 1.00/0.70/0.60 on the unchanged case to 1.00/1.00/1.00 at --runs 3. license-compliance-refuses-to-clear-missing-licenses graded several ways of saying no, which any model says. It now also grades the skill's risk categorization: Δ +0.43 -> +0.86 at --runs 3 on both arms. testing-uses-pump-app-in-widget-tests referred to helpers the fixture does not have, so the no-plugin arm sometimes asked a question instead of answering. Its prompt is now self-contained. Corrects the rule in BASELINE.md that one no-plugin run can disqualify a grader. It cannot: accessibility-declines-gesture-detector-tap-target failed all three content graders without the plugin on one run and passed all three on the next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"very-good-cli is mocked" read as a property of the plugin. The mocks exist only for `claude plugin eval`: they are not shipped behavior, they do not affect a real session, and a user's very-good-cli tools still reach the real `very_good mcp` server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…project prompt The prompt also told the model the CLI was installed and on PATH so it would treat the tools as available. With the hook no longer denying, that is doing nothing either: removing the whole sentence measured 3/3 at 1.00 on --runs 3. A prompt should describe the user's situation, not work around the harness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-read https://code.claude.com/docs/en/plugin-evals and re-tested each claim. The grant claim was wrong. A mocked tool needs no --allow-tools grant and no allowed_tools entry, exactly as the docs say. Verified with allowed_tools: [Read, Glob, Grep, Skill] and no grant: the model still called packages_check_licenses with {"directory": ".", "licenses": true}. The earlier "not granted (missing --allow-tools grant, or a malformed entry)" was the second half of that message. The bare mcp__very-good-cli__<tool> form is not a valid entry in a case; the name is mcp__plugin_<plugin>_<server>__<tool>. The runner cannot tell a missing grant from a bad name, so it offers both. Two behaviors are genuinely undocumented and are now recorded: - `expect` treats a missing field as a violation, so naming an optional argument aborts a correct call. The reference presents it as a type guard. - `_tools.json` is load-bearing. Renaming an argument in it and changing nothing else made the model call the tool with the renamed argument, which confirms both that it is parsed and that the saved `tools/list` result is the shape it wants. Frontmatter is optional; three of the four mocks have none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erence's wording A run starts no real MCP server *unless asked*, with --allow-real-servers or --mocks off. The unqualified sentence read as if it were impossible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bullet restated the whole mock setup inside "What this does not cover", which the Mocking the MCP servers section already covers. It now states the limitation and links there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 cases against this branch with #155, one arm, pinned to claude-sonnet-5: mean 0.961, 94/100 at threshold, 83 perfect, $13.35, no usage errors. bloc-writes-sealed-events-and-states was called fixed on one --runs 3. It scored 0.70 in the full run with the corrected prompt in place, losing the same three graders as before. Seven post-fix runs now read 1.00 six times and 0.70 once, against one in three before the fix, so it is improved and not cured. Making it airtight means the skill naming which state approach a request lifecycle with differing payloads takes. Of the six cases under threshold, four were re-measured at --runs 3. green-gate-refuses-to-carry-green-forward (0.91) and create-project-asks-for-organization-when-required (0.83) clear or nearly clear on average, ui-package-declines-hand-rolled-button hit the turn cap rather than failing on content, and green-gate-budgets-per-package matches its 2026-09-17 score exactly. layered-architecture-wires-repositories-in-bootstrap is the one real finding at 0.61, failing constructs-in-bootstrap on all three runs. It is untouched by this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both files had drifted into a record of how the work was done rather than what a future reader needs. BASELINE.md dropped a section headed "this branch with #155", a reference that means nothing once merged, and a six-row triage table that belonged in the pull request. The bloc case's history is now the finding and the caveat rather than a blow-by-blow. The grader pass keeps its before/after numbers and loses the prose around them. README.md loses the forensics on why the hook denied mocked calls, which is fixed rather than something to act on, and the account of an earlier draft of the grader pass. The tool-naming trap is the same warning in a third of the space. BASELINE 202 -> 162 lines, README 450 -> 431. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both had turned into an account of how the work was done. They are now what a reader needs to act, and nothing else. BASELINE.md is numbers: two run tables, cases below threshold, per-skill Δ, the grader pass, and caveats as one line each. The case histories and the essays around the tables are gone. 202 -> 111 lines, about what it was before this branch despite carrying an extra run and a new table. README.md keeps every fact that changes what you would do and drops the justification around them. The mocking section is the layout, the frontmatter keys, and four non-obvious behaviors as bullets. Graders that cannot fail is the rule and three reasons to delete. 450 -> 354 lines. No facts removed: tool naming, the expect-aborts-on-missing-field trap, _tools.json, arm: both, weight: 3, mock_calls, the -printf portability note and the measurements are all still there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BASELINE.md recorded numbers nobody re-runs after a change, from single runs noisy enough that reading them as a verdict is a mistake this branch made twice. Nothing diffed against it and git already records measurements with the context that makes them readable. Its three durable caveats move to README.md: the no-plugin arm's swing between runs, the Sonnet judge, and create-project's haiku pin. AGENTS.md drops it from the structure block. The one finding it carried is fixed here. layered-architecture-wires-repositories-in-bootstrap asked to wire AuthRepository and UserRepository without saying they were local packages, and the fixture has no packages/ directory, so the model either invented the whole monorepo and passed or treated them as classes in lib/ and lost constructs-in-bootstrap, path-dependencies and uses-repository-provider. Naming the packages alone made it worse: the model checked the tree, found nothing, and asked which it was instead of answering. The prompt now also names the description authoritative over disk, the convention two other prompts already use. At --runs 3 on both arms: with 1.00/1.00/1.00, without 0.33, Δ +0.67, up from 0.61 with three graders failing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rose The mocks had no consumer, so nothing exercised them in CI and their value was unproven. license-compliance-runs-check-with-full-license-info now drives one instead of describing it. The prompt told the model it could not run anything and asked what it would call. It now asks for the audit. Three graders that read prose are replaced: a regex for the tool's name and one for `licenses: true` become tool_used on the call and on its input, and a rubric about what the model said it would retrieve becomes a rubric on whether the two packages the mock flags, a GPL-3.0 and one with no license, reach the report with a risk level. At --runs 3 on both arms: 1.00/1.00/1.00 with the plugin, 0.00 without. That Δ is not content lift. A mocked tool is absent in the no-plugin arm and nothing excludes these graders from the score the way it does for tool_used: Skill, so the without arm fails them for free. README.md now says so, and that the remaining tool-driven skills are still graded on narration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight cases now drive or assert a mocked tool, up from one. All measured at --runs 3 with the plugin loaded. Calls the tool: license-compliance-runs-check-with-full-license-info 1.00 license-compliance-scopes-check-to-monorepo-subdirectory 1.00 ui-package-scaffolds-with-app-ui-package-template 1.00 (was 0.88) create-project-plans-dependency-install 1.00 create-project-does-not-over-ask 1.00 (was 0.75) Asserts the tool was NOT called, which is where mocks help most: "it did not scaffold" becomes a fact from the trace rather than a judge reading prose. create-project-stays-out-of-existing-project-work 1.00 create-project-asks-for-organization-when-required 0.95 (was 0.83) create-project-asks-when-the-template-is-ambiguous 0.92 Both improved cases improved for the same reason: the mechanical grader is 3/3 and the old rubric is what flaps. green-gate is left alone. Its loop needs analyze_files and dart_format, and converting only `test` would strand the model mid-loop. It belongs with the dart mocks. Not converted, having read them: cases whose deliverable is code rather than a call, create-project-scopes-dependency-install-to-the-new-project, whose no-invented-organization grader requires asking and so forbids the call, and static-security-scans-dependencies-before-release, which is about CVEs rather than licenses. license-compliance-refuses-to-certify-from-pubspec-alone was converted and reverted. Its prompt ends "I'd rather not run any scan", so requiring the call demanded the model override the user, and it scored 0.62 three times for that reason. Back to 1.00. README.md gains the trap this pass hit four times: converting a case to drive a tool invalidates every rubric that read the narration, because the model stops describing the call and just makes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…quiet out of scope ui-package-scaffolds-with-app-ui-package-template now checks the create call passes output_directory, which the skill teaches for a package in a monorepo and nothing verified. It drops src-and-barrel-layout, which began failing 3/3 once the case drove the tool: the model stops describing the layout at length and a blind judge sees nothing to judge. That convention is still covered by ui-package-refuses-imports-from-src and ui-package-adds-widget-with-barrel-export-and-test. Back to 1.00 at --runs 3. Both negative controls now assert the tool was never called rather than only that the skill stayed quiet, which is weaker: a skill can stay quiet while the model still reaches for a tool. testing-stays-out-of-non-test-work and ui-package-stays-out-of-plain-dart-work, both 1.00. A case covering the testing skill's two `test` MCP tool standards, `directory` for a monorepo package and `timeout_seconds`, was written and removed. It scored 0.54 with `Skill called 0x` on every run: no skill fires on a request to run a suite, because green-gate's description claims that ground, saying it owns "which tool and arguments run each gate" and to prefer it over the single-gate testing skill. Those two standards look misplaced in skills/testing/SKILL.md, and green-gate is where they could be graded. Left for the dart-mock work rather than widening a shipped description to make a new case pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ryzizub
force-pushed
the
claude/determined-ptolemy-c2e085
branch
from
September 22, 2026 07:16
ea55299 to
41beba2
Compare
A full run put ui-package-scaffolds-with-app-ui-package-template over both the 12-turn cap and the 600s timeout. It scored 1.00 anyway, because the graders were satisfied before the limits hit, which is luck rather than a pass: a slower run fails before the call is made. Measured again at --runs 3 it takes 11, 15 and 14 turns. A converted case does strictly more than the prompt it replaced. It routes, calls the tool, reads the answer, then writes the reply. The four cases that drive a tool now carry max_turns: 20 and timeout_seconds: 900, and README.md records why, including that a cap breach scores 0 with no failing grader and so reads as a content failure. The two other cases under threshold in that run were both transient, and neither is touched by this branch. At --runs 3, green-gate-escalates-when-the-loop-stops-making-progress is 1.00 3/3 against 0.00, and ui-package-declines-hand-rolled-button is a consistent 0.86, which clears the threshold. Both had turns=1 in the sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ryzizub
marked this pull request as ready for review
September 22, 2026 08:26
marcossevilla
approved these changes
Sep 22, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The eval suite had no MCP mocks, and several cases graded what the model said rather than what it did. This adds the mocks and moves every VGV-tool case onto the real call.
What changed
evals/mocks/very-good-cli/— mocks forcreate,packages_get,testandpackages_check_licenses, plus the realtools/listschemas. Eval runs only; a real session still reaches the real server.tool_used,tool_orderandinput_matchon the actual arguments.skills/bloc/SKILL.mdgains one Core Standard:build:constructs the bloc rather than sharing one fromsetUp.evals/BASELINE.mddeleted — numbers nobody re-runs and nothing diffed against. Its three durable caveats moved toREADME.md.Result
Full suite, one arm,
claude-sonnet-5, $13.58:Four previously-weak cases roughly doubled their lift, and every converted case sits at 0.92 or better over
--runs 3.Per-case scores
Cases on mocks,
--runs 3:license-compliance-runs-check-with-full-license-infolicense-compliance-scopes-check-to-monorepo-subdirectorydirectorynamesmobileui-package-scaffolds-with-app-ui-package-templatesubcommandandoutput_directorycreate-project-plans-dependency-installcreate-project-does-not-over-askcreate-project-stays-out-of-existing-project-workcreatenever calledui-package-stays-out-of-plain-dart-workcreatenever calledtesting-stays-out-of-non-test-worktestnever calledcreate-project-asks-for-organization-when-requiredcreate, was 0.83create-project-asks-when-the-template-is-ambiguouscreatewhile ambiguousGraders reworked so they can fail without the plugin:
license-compliance-refuses-to-clear-missing-licensesinternationalization-uses-directional-insets-for-rtlbloc-tests-with-bloc-test-and-mocktaillayered-architecture-wires-repositories-in-bootstrapFor review
tool_used: Skill. Read the with-arm score.max_turns: 20andtimeout_seconds: 900. At the usual 12 and 600 one breached both, and a cap breach scores 0 with no failing grader — it reads as a content failure.analyze_filesanddart_format, so it belongs with the dart mocks.bloc-writes-sealed-events-and-statesis improved, not cured: 6 of 7 runs perfect against 1 in 3 before.Cases deliberately not converted
create-project-scopes-dependency-install-to-the-new-project— itsno-invented-organizationgrader requires asking, so requiring a call would contradict it.static-security-scans-dependencies-before-release— about CVEs, not licenses.license-compliance-refuses-to-certify-from-pubspec-alone— converted, then reverted: its prompt ends "I'd rather not run any scan", so requiring the call demanded the model override the user.test-tool standards was written and removed. Running a suite fires no skill, because green-gate's description claims "which tool and arguments run each gate" — so those two standards are unreachable where they live, and likely belong in green-gate.🤖 Generated with Claude Code