feat(plugin): ship coder_eval as a Claude Code plugin + marketplace - #82
feat(plugin): ship coder_eval as a Claude Code plugin + marketplace#82uipreliga wants to merge 26 commits into
Conversation
|
Claude finished @uipreliga's task in 1m 33s —— View job Code Review in Progress
|
This comment was marked as outdated.
This comment was marked as outdated.
…st-privilege CI The blocker first: the bundled activation template declared no plugin source, so the sandboxed agent was never OFFERED the skill under test. Every positive row scored 0, `recall.yes` tripped the template's own `suite_thresholds`, and skill-check Step 7 then reported "the description under-claims" — a confident, fabricated diagnosis of a skill that was simply absent. The template now carries `agent.plugins` pointing at `$SKILL_SOURCE_PATH` (an env var, so a committed suite stays portable across machines and CI), skill-check Step 4 explains how to set it and warns that an unset variable is indistinguishable from a broken skill, and a test asserts the template names a plugin source — the existing expansion test passed with the skill unreachable. `analyze`'s output template opened a ```markdown fence containing a ```diff block. A closing fence may not carry an info string, so the inner opener closed the outer block early and the next bare fence opened one that never closed — burying 32 lines, including the entire Principles section. The outer fence is now four backticks, and every bundled Markdown file's fences are checked for balance. `analyze` also read agent-produced `error` / `output` text into a session holding Bash and Write with no untrusted-data framing, contrary to review-rubric item 14. It now treats everything a run recorded as evidence to quote rather than instructions to follow, and reports text that tries to direct it. The workflow the `ci` skill emits — copied verbatim into user repositories — had no `permissions:` block, so it inherited the consumer's default GITHUB_TOKEN scope (write-all in many orgs) while `actions/checkout` persisted that token into a workspace where agent-generated code executes. Now `contents: read` plus `persist-credentials: false`, with Step 7 explaining why so neither is dropped as boilerplate. Also: CE029 now scans `plugins/`, since the one surface whose job is teaching task-YAML schema was the one surface whose examples were never validated — it immediately caught an invalid `tags:` placeholder; `task` preflights `coder-eval --version` before writing files rather than failing mid-flow at the first of its two CLI calls, pinned by a test because both READMEs claim it; the bundled run-layout no longer attributes the run contract to two repo-local commands the plugin does not ship; the task template says `tempdir` is not a confinement boundary and points at `docker`; and release.yml's push step passes its version through `env:` like the step above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Make the repo a Claude Code plugin marketplace hosting one `coder-eval` plugin, so `/plugin marketplace add UiPath/coder_eval` works: - `.claude-plugin/marketplace.json` at the repo root, one plugin entry pointing at `./plugins/coder-eval` (no `version` here — plugin.json wins). - `plugins/coder-eval/.claude-plugin/plugin.json`, `version` pinned to pyproject's. `claude plugin validate --strict` rejects a manifest with no version, and pr-checks will run it as a gate. - `plugins/coder-eval/README.md` — install commands, the prerequisite CLI, and the five skills that land in the following phases. `release.yml`'s existing bump step now seds plugin.json alongside action.yml (same grep guard idiom), and `tests/test_action_version_pin.py` — already the owner of the "derived pins agree with pyproject" invariant — asserts the new pin at rest, so a skipped bump fails CI instead of stranding installed users on a cached copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… with CE032 An installed plugin is copied to ~/.claude/plugins/cache/ without its parent directories, so a skill cannot read docs/TASK_DEFINITION_GUIDE.md at runtime — the criterion vocabulary has to ship inside plugins/coder-eval/, where a hand-maintained copy would drift on the next criterion change. So it is generated: tests/lint/plugin_reference.py renders plugins/coder-eval/reference/criteria.md from the SuccessCriterion union, `make plugin-reference` writes it, and CE032 re-renders and diffs it. Same shape as tests/lint/doc_indexes.py + CE028, including the deliberate absence of a --check mode. Inherited base fields are excluded by *computing* them off BaseSuccessCriterion/LiveSuccessCriterion rather than listing names — the stop_early refactor is exactly the change a hardcoded list would have leaked into all 14 sections. The discriminator name is likewise read off the union's own Field(discriminator=...). Required fields get descriptions; optional fields get bare names, so no default_factory or `X | None` normalizing is needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/coder-eval:skill-check turns "does my skill actually trigger?" into a number: design labelled positive and distractor requests, run a real agent against each, and score whether the skill was engaged (recall/precision/F1 + confusion) so the frontmatter `description` can be edited against evidence instead of taste. Ships the canonical activation suite it copies — reference/templates/activation.yaml + activation-rows.jsonl (3 positive, 3 distractor) — gated on recall.yes / precision.yes, whose names are asserted against the real skill_triggered aggregate rather than a hardcoded list. New TestPluginArtifacts class carries the guards `claude plugin validate --strict` does not: it ignores skill frontmatter entirely, so a typo'd key or a `name:` that silently disables a skill would otherwise ship unnoticed. The frontmatter, model-invocation and repo-path-containment checks are parametrized over every skills/*/SKILL.md, so the remaining skills inherit them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/coder-eval:init scans a repository for what is actually worth evaluating
(skills, an MCP server, a CLI), reports the findings, then scaffolds one real
task rather than an empty suite — and hands skills off to
/coder-eval:skill-check, which builds an activation suite properly.
/coder-eval:task is the authoring loop from .claude/commands/coder-eval-task-create.md
made portable: no UiPath tags or directory conventions, `coder-eval plan` rather
than `uv run`, and the criterion field list replaced by a pointer to
${CLAUDE_PLUGIN_ROOT}/reference/criteria.md so there is one field list, not two.
Both inherit Phase 3's parametrized frontmatter, invocation-flag and
path-containment guards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/coder-eval:analyze ports the run-analysis reasoning intact — the seven
dimensions, cluster-before-deep-dive for suites over 20 tasks (with the
jq/python3 extraction that keeps it from reading multi-megabyte turns arrays),
the output caps, and the don't-recommend-what's-already-fixed check. It reads
the run-directory contract from ${CLAUDE_PLUGIN_ROOT}/reference/run-layout.md,
a verbatim mirror of .claude/shared/run-layout.md; the shared original now
points at its mirror and a byte-equality test is the sensor for the plan's one
hand-copied file.
/coder-eval:ci emits a workflow that gets the parts integrators get wrong: the
agent runtime the action deliberately does not install, credentials through the
env passthrough rather than inline, JUnit plus job summary, and a score floor
the user picks after seeing a baseline instead of a guess. It also warns against
pull_request_target with secrets, since evaluated tasks run agent-generated code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI now proves the marketplace is installable and that what skill-check scaffolds is real. The new plugin-validate job runs `claude plugin validate --strict` on both manifests, then copies the activation template into $RUNNER_TEMP and asserts it expands — outside the source tree, with no credentials, so it costs nothing per PR. The expansion assert is load-bearing: `coder-eval plan` exits 0 even when dataset.paths names a file that was never copied, so a plan-only check would have passed vacuously (verified both ways locally). CE026's doc scan now covers plugins/**/*.md, because the ci skill emits an Action snippet users copy verbatim — exactly the surface the rule exists to police. The same three clauses that keep README and docs/CI_GATE.md honest now keep that snippet from shipping without the agent runtime the action deliberately does not install. Plus the docs surface: docs/PLUGIN.md (nav + blurb + regenerated indexes), a README section and badge, CLAUDE.md's directory map and CE032, and test_every_declared_skill_ships so a deleted skill can no longer pass the parametrized guards by simply not being there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both external reviewers converged on one real gap: plugin.json's version pin had an at-rest VALUE test but no at-rest ANCHOR test, unlike action.yml's. The release sed matches a whole line including its trailing comma, so moving `version` to the last key or collapsing the JSON would have made the bump a silent no-op — surfacing only during a release. Asserted the line shape at rest (and verified the pattern rejects both of those reformattings). Also, from the same review: _summary() no longer escapes pipes, since it renders as body prose rather than a table cell (output byte-identical today — no criterion docstring's first line contains one); the common-field parity test now matches the two forms the render actually emits a field name in, so a criterion whose prose mentions "weight" can't fail it spuriously; and a SKILL.md with an unclosed frontmatter fence now reports that instead of raising from str.index. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The path-containment denylist allows `tasks/` and `.claude/skills/` on purpose — they are user-workspace paths the skills scaffold into — so a skill naming a file that exists only in this repo slips past it. The obvious "does this path exist at the repo root" rule false-positives on `init`'s correct advice to scan pyproject.toml, so this needs a real token classifier rather than a quick guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by running the skills against a scratch repo: /coder-eval:init scaffolded a task whose prompt said "use `pypdf` to read the fields" alongside a criterion grepping for `pypdf`. That criterion cannot fail — the agent was handed the answer — but it reads as coverage. Both skills already said "prompts instruct, criteria validate"; neither named this quieter form, where the leaked detail is a legitimate-looking requirement rather than an obvious restatement of the check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ria honestly Both found by running the skills against real inputs. analyze, pointed at a 947-task experiment run, got the headline exactly right (180 rows lost to a 120s timeout, 37.5%) but drifted on two secondary figures: it reported the 180 as 146 ERROR + 34 TIMEOUT when the run holds 144 + 36, and cited a 109s slowest success when the real maximum was 147.7s. A run analysis that reads as authoritative and is quietly off by two is worse than none, so the principles now require every count to come out of the extraction command rather than off the page — if you can't produce the command, don't state the number. task, asked for a `--json` flag, wrote a criterion grepping for `--json` at weight 1.5. The flag name has to appear in the prompt (it IS the request), so that criterion only proves the agent typed back what it was told. Named the pattern and pushed such smoke checks to a low weight, with the real weight on behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… inputs Nothing checked that a copy-pasteable Action snippet passes inputs the action actually declares. CE026 read action.yml only for `name:` (the slug clause), so renaming an input — `junit-path` to `junit`, say — would leave every snippet promising something the step no longer does. GitHub does not fail a workflow on an unknown `with:` key; it ignores it. The failure is therefore silent, and the worst copy of it is the plugin's `ci` skill, whose output lands in other people's repositories where our CI can never see it. The clause parses each yaml block, finds steps whose `uses:` references the action at any nesting depth (whole workflow, bare step list, or lone step — all three shapes appear across the pages), and checks every `with:` key against action.yml's inputs. Unparseable fragments are skipped; example validity is CE029's job, not this one. Verified by simulating the regression: renaming `model` fires on all four surfaces (README, CI_GATE, tutorial 02, the ci skill), and renaming `junit-path` fires on the skill, which is the only one that passes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fields Optional criterion fields rendered as a bare comma-separated list of names, so the bundled reference taught 14 criteria's field *names* while withholding what they mean — and what they mean is the half authors get wrong. `min_count: 0` lets a criterion pass when nothing matched; `weight: 0` makes it informational. Both were invisible. `_field_sections` now renders optional fields as a described table under an `Optional:` label, via a `_table` helper shared with the required branch. Defaults and types stay deliberately unrendered (the docstring keeps that rationale); descriptions are rendered in full rather than truncated or curated, since a curated subset would need a hardcoded name list and so a second declaration of the schema. 4,946 → 16,018 bytes. `LLMJudgeCriterion.temperature` was the one criterion field with no `description=`, which the new table would have shipped as an empty cell. A union-derived sensor now makes the next one a red test instead. `test_common_base_fields_are_not_repeated_per_criterion` filtered for lines starting `Optional: ` — a form the renderer no longer emits, which would have left it passing vacuously while checking nothing. That branch is deleted; the surviving table-row assertion now covers required and optional alike, verified red under mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin taught which criterion types exist but not how a criterion set fails
as an instrument. The audit's most common production defect was a task that
passes for the wrong reason: a `command_executed` crediting an invocation that
crashed, a pattern that also matches `--help`, a criterion set an agent doing
nothing satisfies.
`reference/task-rubric.md` is the checklist, bundled rather than a repo doc
because an installed plugin is copied without its parent directories. Five
sections: the framing question ("what is the cheapest thing an agent could do
that scores full marks?") plus six mechanical checks, self-reports vs behaviour,
judges complementing rather than carrying, scope match, and fixture lifecycle.
It declares checks only — no severity ladder. `task` never emits a severity; it
fixes what it finds. Severity is a property of a report, so it belongs to the
one skill that produces one.
Fixture lifecycle lives here and only here. `task` reaches it by pointer at two
points: before criteria are chosen (design-time, where it prevents things) and
again in a new Step 5 before `coder-eval plan` (review-time, where it catches
what was actually typed). §5's ordering contract was verified against the
`pre_run` / `post_run` field descriptions rather than restated from memory.
`require_success` was the bug this repo shipped while documenting it: both the
skill and the repo-local authoring command restated the permissive model default
as the recommendation. Both now lead with `true` for graded commands.
The repo-path containment guard covered `skills/*/SKILL.md` only, so the new
bundled reference — same runtime constraint — had none. Widened in place to
every text file under the plugin; body and message byte-for-byte unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`coder-eval plan` exiting 0 proved the YAML was well formed and nothing else. The skill treated that as finished, so it could hand back a task whose criteria were unsatisfiable, or satisfiable by doing almost nothing, and neither would surface until someone scheduled it. Step 6 now offers a run after `plan` passes — stating the task count, the agent and model, and that it costs real tokens, then asking. It never runs unprompted, which keeps it compatible with `skill-check`, the other skill that spends tokens. The interpretation is the point. A first run scoring 1.000 is treated as suspicious rather than as success, sending the author back to the framing question with a real trajectory in hand. A failing run is a layer diagnosis before it is a prompt edit: patching the prompt to route around a missing capability turns the score green and changes nothing for users. And a task that cannot pass yet is withdrawn or explained, not shipped — a permanently red task teaches everyone to ignore red. Step 7's table gains a run-verdict column, which is either a score or an explicit "not run" with the reason. An empty cell reads as a pass later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… existing tasks `task` authors tasks and applies the rubric to what it just wrote. Nothing applied it to the tasks a repository already has, which is where the audit found the defects: criteria that cannot fail, prompts that dictate what a criterion greps for, fixtures with no cleanup. This is that pass — read-only, free, no credentials, severity-ranked, with a concrete fix per finding. It is a separate skill rather than a mode of `task` because tool policy is declared per skill: authoring needs `Write` and a review pass must not have it. It restates none of the rubric's checks; it reads them at runtime and adds only the one axis that needs neighbours, near-duplicate detection, plus the severity ladder — which lives here because this is the only skill that reports one. Calibrating it against this repository's own 44 tasks is what made it correct, and it changed three things no test could have: - The severity arithmetic was a category error. There is no task-level weighted pass threshold — the gate is strict-AND over each scoring criterion's own threshold — so "a cheap path clearing the pass threshold" had no referent. Both the ladder and rubric check 3 now say what actually gates a run. - The rubric could not tell a framework fixture from a capability task, so applied literally it flagged every plumbing smoke test in this repo, and the plugin's own shipped activation template. New rubric section 0 establishes the subject first; the activation carve-out now names the exact checks it suspends instead of names the rubric does not use. - The rubric never declared the prompt-leak check this skill advertises finding — it lived only in `task`'s prose, which this skill does not read, so the two readers had already forked. It is now check 7. Both new frontmatter sensors passed against a broken skill until fixed: the read-only test passed with `allowed-tools` deleted, and the carve-out test passed with the carve-out inverted. Both are now mutation-verified, and the eight prose skill-counts this phase repaired by hand are guarded by a derived test so the seventh skill cannot ship with them wrong. The frontmatter allowlist grew to five keys and stopped blaming the specification for a restriction that is this plugin's own house style. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g in `analyze` `skill-check` taught that a low-recall result means the description under-claims. Two truncation mechanisms make that advice wrong often enough to matter, and following it means rewriting text the model never read. The first is per-skill: `description` and `when_to_use` are concatenated and cut at 1,536 characters, so trigger text past the cutoff cannot affect activation. The second is worse and was not in the audit at all — the whole listing has a budget near 1% of the context window, shared with every skill the user has installed, and on overflow descriptions are dropped starting with the skills invoked least. A freshly authored skill is by definition rarely invoked, so the eviction order is biased against precisely the skill someone is testing. Step 7 now rules out truncation and eviction before blaming the wording, and names `/doctor` and `/context` as the way to check rather than guess. Sibling-owned rows are the third row class: a request that legitimately belongs to a named other skill, labelled with that sibling. A plain distractor shows that a misfire happened; this shows where it went, which is what tells a boundary dispute between two descriptions from one vague description. Optional, because every row is a full agent run. `analyze` classified a failure as `prompt_gap` and left the fix implied, so the implied fix was always "edit the prompt". Dimension 1 now forces the layer question first: would a real user have said the missing thing, or should the skill or the tool have supplied it? In the second case the task was right to fail, and patching the prompt is updating a snapshot to match broken output. Dimension 6 gains the residue signature and the fact that shared-state races break in both directions — a false pass when the state already existed, a false failure when something undid it mid-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two independent sources — the skills specification and the installed CLI's own field text — say `disallowed-tools` is a hard deny that "clears when you send your next message". This skill's step 1 deliberately solicits such a message: it asks before linting a whole directory. So the frontmatter covers the first turn and nothing covered the rest, while the skill's description advertises "Read-only." unconditionally. The prose rule is what actually spans the review, so it now says so, and a sensor guards it. Answering "yes, lint all of them" widens what may be read and never grants permission to write. Also corrects a stale reference in the deferred harness candidates: the containment guard was renamed and widened to every shipped plugin file in this run, which closes that candidate's coverage half; its token-classifier problem is unchanged and still deferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A skill's advertised description promised a check no bundled reference declared, which forked the two rubric readers before the skill shipped. Mechanizing that check needs a shared claim vocabulary between description and rubric, not a token grep, so it is deferred rather than written now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cross-phase review found a consistency layer that had not caught up with the change. All of these are cases where two files in the same commit disagreed. The read-only story in docs/PLUGIN.md claimed tool policy is declared "per skill rather than per invocation", which is the inverse of the documented behaviour: `disallowed-tools` clears on the user's next message, and `lint-tasks` asks one. The doc now names all three mechanisms and says which of them actually spans a review, rather than implying the frontmatter does. The rubric declared itself the canonical home for its checks while three of them had live copies elsewhere. Two are resolved: the output-content check is now a rubric section rather than a rule `lint-tasks` exempted without any declaration to exempt it from, and `task`'s prompt-leak paragraphs now say the rubric carries the review-time version. `analyze`'s residue diagnosis is left in place with the rubric's claim narrowed — diagnosing a finished run is a different job from reviewing a file. `lint-tasks` forbade counting the rubric's sections and then pinned four ordinals into it. Adding the content-check section renumbered three of them, which is the failure mode exactly; the exemptions now name what they check. The rubric's own header said "two readers" when this change gave it a third. Replaced with the list convention the repo's other shared resource uses. The count sensor claimed to guard the sites hand-edited for the sixth skill but matched only five of seven — CLAUDE.md's "x 6" and PLUGIN.md's "The other four" both slipped through, and both are now covered and mutation-verified. Also: the gate arithmetic gains the `stop_early:` exception (an early-stopped run gates on the armed subset, weighted, so a cheap path buys more there); a per-task verdict is the max over all issues attributed to it, not just the ones still printed after clustering; the untrusted-input rule is flagged at the step that does the reading and now scopes reads to the resolved task directory; and the `coder-eval --version` claim names the two skills that actually preflight it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by running the plugin's own `lint-tasks` skill against this repository's
44 tasks — the calibration run the plugin-audit plan asked for.
`require_success` defaults to False, so a `command_executed` criterion credits an
invocation that crashed. On an unarmed criterion that is merely generous. On an
armed one three behaviours compose into a corrupted verdict:
1. `live_verdict` and `_check_impl` share `_matching_commands`, so a failed
command live-PASSES a positive criterion the moment it is observed;
2. `on_pass: stop` ends the run on that pass, and `decide_within` latches it —
a latched verdict is never re-polled, so the timeout can never fire either;
3. gating is FIRED-ONLY, so a run the watcher cut gates on the armed subset and
never consults an unarmed criterion.
Concretely, in early_stop_weighted_low_weight_absorbed.yaml an agent that ran
`python app.py` before creating app.py scored a weighted 1.0 over the armed subset
and reported SUCCESS — no app.py, crashed script — because the unarmed
`file_exists` was bypassed. The high-weight mirror had the same path, and in
decision_budget_exceeded a crashed script inside the budget latched a pass that
made `decide_within` unreachable.
CE034 makes it structural: an armed, pass-capable `command_executed` must set
`require_success: true`. Pass-capability is read off the model's own
`live_decidable_polarities()` rather than re-derived, so the rule cannot disagree
with the watcher about which criteria can live-pass. Fail-only negatives are
deliberately exempt — a curl that failed is still a curl that was called, and
requiring success there would blind the criterion to what it exists to forbid.
Also collapses two duplicated rubric checks now that the rubric declares them:
`init` reads the rubric before writing criteria instead of restating the
prompt-leak and content-check rules, and the repo authoring command keeps only the
conventions the rubric does not cover.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st-privilege CI The blocker first: the bundled activation template declared no plugin source, so the sandboxed agent was never OFFERED the skill under test. Every positive row scored 0, `recall.yes` tripped the template's own `suite_thresholds`, and skill-check Step 7 then reported "the description under-claims" — a confident, fabricated diagnosis of a skill that was simply absent. The template now carries `agent.plugins` pointing at `$SKILL_SOURCE_PATH` (an env var, so a committed suite stays portable across machines and CI), skill-check Step 4 explains how to set it and warns that an unset variable is indistinguishable from a broken skill, and a test asserts the template names a plugin source — the existing expansion test passed with the skill unreachable. `analyze`'s output template opened a ```markdown fence containing a ```diff block. A closing fence may not carry an info string, so the inner opener closed the outer block early and the next bare fence opened one that never closed — burying 32 lines, including the entire Principles section. The outer fence is now four backticks, and every bundled Markdown file's fences are checked for balance. `analyze` also read agent-produced `error` / `output` text into a session holding Bash and Write with no untrusted-data framing, contrary to review-rubric item 14. It now treats everything a run recorded as evidence to quote rather than instructions to follow, and reports text that tries to direct it. The workflow the `ci` skill emits — copied verbatim into user repositories — had no `permissions:` block, so it inherited the consumer's default GITHUB_TOKEN scope (write-all in many orgs) while `actions/checkout` persisted that token into a workspace where agent-generated code executes. Now `contents: read` plus `persist-credentials: false`, with Step 7 explaining why so neither is dropped as boilerplate. Also: CE029 now scans `plugins/`, since the one surface whose job is teaching task-YAML schema was the one surface whose examples were never validated — it immediately caught an invalid `tags:` placeholder; `task` preflights `coder-eval --version` before writing files rather than failing mid-flow at the first of its two CLI calls, pinned by a test because both READMEs claim it; the bundled run-layout no longer attributes the run contract to two repo-local commands the plugin does not ship; the task template says `tempdir` is not a confinement boundary and points at `docker`; and release.yml's push step passes its version through `env:` like the step above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plugin had a reference page but no tutorial, while every other major entry point in the product has one — so the six slash commands were documented as a table of what they are, with no walkthrough of using them together. Tutorial 07 runs the loop end to end: install, scaffold, author, adversarially review, run, analyze, and optionally measure whether a skill triggers. It deliberately carries no Action snippet, pointing at Tutorial 02 and CI_GATE.md instead, so CE026's prerequisite rules stay owned by the pages that already demonstrate them. Two gaps in PLUGIN.md were introduced by earlier commits on this branch and are fixed here: `$SKILL_SOURCE_PATH` was missing entirely. The activation template now needs it to make the skill reachable in the sandbox, so a reader following the worked example verbatim would have got recall 0.0 and no hint why. The worked example still taught "low recall means the description under-claims" — the exact fabricated diagnosis removed from the skill itself, since truncation at 1,536 characters and least-invoked-first listing eviction produce an identical number. It now names all three causes and points at `/doctor` and `/context` for telling them apart. Both new intra-doc anchors were verified against built HTML rather than by eye, per the anchor-slugger convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arration Two independent reviews of the new tutorial (factual accuracy against the code, and pedagogy against the existing six) found the same shape of problem: the page was an essay about the plugin rather than a walkthrough of it, at 114 words per line of code against a series range of 6-48, with no verification cue after the first step. Factual corrections: - "each one drives the same coder-eval CLI" was false for `lint-tasks`, which has no Bash and cannot invoke it, and for `ci`, which emits a workflow. - The cost framing said steps 1-3 were free and step 4 paid. Step 3 ends by offering a paid run, and the optional step 5 is one run PER ROW — 16 for an 8/8 suite, the most expensive thing on the page. - `analyze` writes `analysis.md` INTO the run directory, not next to it. - Systemic-pattern clustering only happens above 20 tasks, so it never fires on the single-task suite this tutorial builds; described as conditional now. - `coder-eval plan` soft-warns on an unknown top-level key rather than failing, so the reader is told to read the output rather than trust an exit code. Teaching fixes: - The reader now sees something at every step: `coder-eval --version` in the prerequisites, `ls`/`cat` on the scaffolded task, `ls runs/latest/` after the run, and the sections `analysis.md` actually contains. - Step 4 no longer contradicts the page's premise or re-runs what step 3 already offered — it opens from the run the reader already has and shows the by-hand equivalent, which demonstrates the driver claim instead of asserting it. - Step 5's `SKILL_SOURCE_PATH` export now precedes the command it gates, rather than following it where a copy-paste reader would already have failed. - Added a troubleshooting table for the four real failure modes, plus update and uninstall. - States which repository the reader is in, since steps 2+ leave the coder_eval clone the other tutorials use. - Dropped "What you learned" (no other tutorial has one, and it smuggled in tool policy never taught in the body) and the passages duplicated from PLUGIN.md. Also: `skill-check` now documents the directory-path argument both docs already used, the title matches the series' action form, and three British spellings are made American to match the rest of docs/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…first Installing the plugin does not install the CLI — a plugin ships skills and references, not packages, and plugin.json declares no install hook. So a user who runs `/plugin install` and then a skill hits a missing binary. The three skills that shell out to it detected that and stopped with a printed hint, leaving the user to copy a command out of the message. They now offer to install it and ask. Not silently: `uv tool install` writes outside the repository, so it is the user's call, and which installer to use depends on whether uv is present and whether they want it in an active venv. This mirrors the pattern the plugin already uses for the skills that spend tokens — state what will happen, then ask. The policy is declared once in `reference/cli-setup.md` rather than three times: offer both installer forms with the tradeoff, re-run `--version` afterwards because a silent install failure is worse than no install, stop if the user declines rather than failing later at an unrelated command, distinguish a PATH problem from a missing package, and treat version skew as a report rather than a workaround. Each skill keeps only the one-line check locally, so the action stays where it happens and the policy cannot fork. A sensor asserts both halves: the reference ships, every CLI-driving skill points at it, and no skill restates the install command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three semantic conflicts the textual rebase could not see: - ID collision: main shipped its own CE032 (`ce032_criteria_path_seam`), so this branch's plugin-reference parity rule moves to the next free id, CE033. - The bundled `reference/criteria.md` is generated from the criterion models; main added `cli_called` and glob-pattern path descriptions, so it is regenerated via `make plugin-reference`. - `plugin.json`'s version is a derived pin of pyproject's, which main bumped to 0.9.5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8846ff5 to
44044b5
Compare
The review's item-by-item response closed 14 of its 16 findings. This covers what was left, all of it verified against the real schema and CLI rather than by reading: - `analyze` (and its repo-local twin) taught a `task.json` field vocabulary the schema does not have: six of the thirteen keys in its jq summary — `turns`, `total_tokens`, `assistant_turn_count`, `max_turns`, `criteria_count`, `all_criteria_perfect` — do not exist at the top level, and `error_excerpt` read `output`/`Instructions`, which no criterion result carries. jq answers a missing key with `null`, so this shipped as a summary of nulls that reads like a run with no data, and it did so precisely on the >20-task path where the skill forbids falling back to whole files. Both copies now use the real paths (`iterations`, `total_token_usage.*`, `task_config.resolved.run_limits.*`, `criterion_type`, `error // details`), checked against a real task.json. Guarded by TestRunRecordFieldVocabulary, which resolves every field the two surfaces name against the models and is mutation-tested on the shipped block. - `init` told the agent to run `coder-eval plan <task-directory>` and iterate until it exits 0 — an unreachable loop condition, since `plan` takes files and rejects a directory. Now the glob form, with the no-argument form named as the alternative and the two failure modes distinguished. The general guard (CE035) is recorded in harness-candidates rather than built: the useful half is argument shape, not subcommand existence. - The deferred write()/check() duplication is extracted to tests/lint/generated.py and both generated-surface checkers route through it, keeping the more general create-the-target behaviour and pinning it with tests. Nit 8 (frontmatter allowlist framing) needed no change — it was fixed in a later commit than the review, and the message no longer claims spec authority. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Follow-up: the earlier response triaged 14 of this review's 16 findings. Two were never reached (nits 8 and 9), and one was deferred pending a "say the word". All three are now closed in 1. [Axis: What's Missing]
|
| Documented | Actual |
|---|---|
turns |
iterations |
total_tokens |
total_token_usage.input_tokens + .output_tokens |
assistant_turn_count |
total_assistant_turns |
max_turns |
task_config.resolved.run_limits.max_turns |
total_cost_usd |
total_token_usage.total_cost_usd |
criteria_count / all_criteria_perfect |
derived from success_criteria_results |
Two more beyond what was filed: failed_criteria named type rather than criterion_type, and error_excerpt was defined as the first ~200 chars of error / output / Instructions — but a criterion result carries only error and details; there is no output and no Instructions. That one matters in practice, because the common case has error: null and all its signal in details ("Matched 0/1 required commands (filters: …)"), so the excerpt that drives clustering in step 3 came back empty exactly when a criterion simply did not match.
Why it is worse than a typo: jq answers a missing key with null instead of failing, so this shipped as a summary full of nulls that reads like a run with no data — and it did so precisely on the >20-task path, which is the one place the skill explicitly forbids falling back to reading whole files.
Fixed in both copies — the reviewer's "Parallel paths" note was right that .claude/commands/coder-eval-run-analysis.md carries the same block and the same wrong names. The corrected program was run against a real passing task and a real failing one (7 failing criteria) to confirm every path resolves and failed_criteria populates with real diagnostics.
Guarded, since the same bug shipped twice: TestRunRecordFieldVocabulary resolves every field those two surfaces read off a run record against EvaluationResult / CriterionResult, and is mutation-tested against the exact block that shipped. Scoped deliberately — only fenced blocks mentioning success_criteria_results, and only the head of each dotted path, since task_config is a free-form dict and .task_config.resolved.run_limits.max_turns is unverifiable from the schema. That scoping is documented in the test's docstring rather than left implicit.
2. [Axis 8, nit 9] init instructs coder-eval plan <task-directory>, which the CLI always rejects — ✅ Fixed
Reproduced in a scratch repo: coder-eval plan tasks exits 1 with Expected a YAML task file but got a directory. "Iterate until it exits 0" was therefore an unreachable loop condition. Now the glob form, with coder-eval plan (no argument, discovers tasks/ recursively — verified exit 0) named as the alternative. The follow-on sentence carried the same wrong assumption, so the two outcomes are now distinguished: "reports no tasks" means the scaffold is empty, the directory error means the argument shape is wrong.
On the proposed CE035 ("documented coder-eval invocations must be executable as written"): recorded in .claude/harness-candidates.md rather than built, with the reasoning split out. The cheap half — does the subcommand exist in the Typer app — would not have caught this. The half that would is argument shape, which needs either a real invocation or a per-command arity model duplicating the CLI signature. The live-smoke counterpart the review also proposed is recorded alongside it, since that is the only form that proves argument shape rather than command existence.
3. [Nit 2] write()/check()/__main__ duplicate doc_indexes.py — ✅ Fixed (was deferred)
Taking you up on it. Extracted tests/lint/generated.py with write_all(rendered) / diff_all(rendered); both generated-surface checkers now contribute only their render. The two copies had diverged in exactly one respect — only the plugin-reference one created a missing target and its parents — and the shared engine keeps that more general behaviour, so a generated file that was never written reports as drift rather than raising. Four tests pin it, including the create-parents branch and the missing-file diff. make docs-indexes and make plugin-reference both still no-op.
This also closes the separate 🔵 note that plugin_reference.py::write() was untested — though only partly by this change: a test_write_is_idempotent for it had already landed in a later commit than the review.
Nit 8 — no change needed
name: in the skill-frontmatter allowlist. Verified at HEAD: this was fixed in a commit later than the review's baseline. The set is unchanged (still excludes name, deliberately), but the framing the finding actually objected to is gone — the message now reads "are outside the set this plugin deliberately restricts itself to … If one is genuinely needed, add it here with a reason", and the comment states outright that this is the plugin's house style and not the specification's limit. That is what the finding asked for.
Verification
make format / make check / make typecheck clean (0 errors; the one warning is pre-existing in agents/antigravity_agent.py, untouched here). make lint 312 passed, up from 305 — the seven new tests are the field-vocabulary guard and the generated-surface engine. make test 4048 passed, 4 skipped.
One note on history: this branch was rebased onto main first, which forced one rename reviewers may have referenced. main shipped its own CE032 (ce032_criteria_path_seam, a real BaseRule), colliding with this branch's plugin-reference parity rule of the same id — so ours moved to the next free id, CE033. The rebase also required regenerating reference/criteria.md (main added the in-tree cli_called criterion and glob-pattern path descriptions) and bumping the plugin manifest's derived version pin to 0.9.5.

What
A new way to use Coder Eval: from inside Claude Code, alongside the CLI and the GitHub
Action. This repo becomes a Claude Code plugin marketplace hosting one
coder-evalplugin, so the whole loop — scaffold a suite, author a task, check whether a skill
triggers, read the results, wire it into CI — runs in the agent.
That adds five slash commands:
/coder-eval:init,/coder-eval:skill-check,/coder-eval:task,/coder-eval:analyze,/coder-eval:ci. They drive thecoder-evalCLI, which stays the prerequisite — the plugin ships prompts and reference material, not a
second implementation.
Standing cost is ~464 tokens always-on for all five (
claude plugin details); bodiesload only on invoke.
What a reviewer should check
No runtime code changes.
git diff --stat <base>..HEAD -- src/is empty. No model,criterion, agent, CLI flag or merge layer moved, so no existing evaluation result can
change. Everything new is a distribution surface, one test-harness generator, one lint
clause, one CI job, and docs.
Three invariants carry the weight:
The bundled criteria reference is generated, not written (
CE032,tests/lint/plugin_reference.py). An installed plugin is copied to~/.claude/plugins/cache/without its parent directories, so a skill cannot readdocs/TASK_DEFINITION_GUIDE.mdat runtime — the criterion vocabulary has to shipinside
plugins/coder-eval/, where a hand-maintained copy would drift on the nextcriterion change.
make plugin-referencerenders it from theSuccessCriterionunion;CE032 re-renders and diffs. Inherited base fields and the discriminator name are
computed, never listed — the
stop_earlyrefactor is exactly the change a hardcodedlist would have leaked into all 14 sections.
The frontmatter guards are load-bearing, not belt-and-braces. Verified by spike:
claude plugin validate --strictdoes not inspect skill frontmatter at all — aSKILL.mdcarrying both an unsupportedname:and an invented key passes with exit 0and zero warnings.
TestPluginArtifactsis the only thing standing between a typo'dkey and a skill that silently never triggers. It also enforces path containment (no
skill may name a repo path that won't exist post-install) across every
SKILL.md.The CI assert expands, it does not just validate.
coder-eval planexits 0 evenwhen
dataset.pathsnames a file that was never copied — verified both ways — so aplan-only check would have passed vacuously.plugin-validatecopies the activationtemplate into
$RUNNER_TEMP, outside the source tree, and assertsexpand_datasetyields 6 row-tasks with both label polarities. No secrets, no agent run, no per-PR cost.
Also in here
plugins/**/*.md(theciskillemits a workflow users copy verbatim, so it is held to the same agent-runtime
prerequisite standard), and a fourth clause — every
with:key on a snippet'saction step must be a real
action.ymlinput. GitHub ignores unknown inputs rather thanfailing, so a rename would silently degrade every copied workflow; simulating a
modelrename fires on all four surfaces. Consequence to know: renaming an action input, or
changing the action's runtime prerequisites, now means updating the
ciskill too.Documented in CLAUDE.md.
plugin.jsoncarries aversionpinned topyproject.toml's, because--strictrejects a manifest without one.release.yml's existing bump step seds italongside
action.yml;tests/test_action_version_pin.py— already the owner of thatinvariant — guards both the value and the sed's line shape at rest.
Evidence the prompts were exercised, not just linted
Mechanical guards prove the plugin loads and is self-contained; they cannot prove the five
prompts are any good. So all five were run against a real scratch repo (two planted skills
analyze, a real 947-task two-variant experiment run — toolsallowlisted so no paid
coder-eval runcould fire.They behaved:
skill-checkdesigned 18 rows (10 positive / 8 distractor, none naming theskill, distractors genuinely adjacent), validated, and stopped to ask before spending;
initwrote.env.exampleand never.env;cichosepull_requestoverpull_request_targetwith explicit reasoning about fork secrets.Three prompt-quality defects were found and fixed:
analyzegot a 947-task run's headline exactly right (180 rows lost to a 120s timeout)but drifted on two secondary figures — it split them 146/34 where the run holds 144/36.
Its principles now require every count to come out of the extraction command.
initscaffolded a task whose prompt dictatedpypdfwhile a criterion grepped for it —a criterion that cannot fail but reads as coverage.
taskgave weight 1.5 to a criterion matching a literal (--json) the prompt has toname.
Out of scope
/coder-eval:compareand/coder-eval:triage(v2, once usage is observed); a secondcoder-eval-devplugin —.claude/commands/stays repo-local and is byte-for-byteunchanged; a duplicate example task under
tasks/— the plugin template is the singlecanonical copy.
Verification
make format/make check/make lint(207) /make test(3744 passed, 8 skipped,87.70% coverage) all clean. Both manifests pass
claude plugin validate --strictwithzero warnings.
Two things this branch cannot rehearse, both worth watching:
plugin-validatehas never run on a runner. Every step was verified locally,including the scaffold assert and its negative, but the job itself is new.
plugin.jsonsed for the firsttime. It fails loudly on a miss (
grep -qguard) and the at-rest test catches driftafterwards, but the release path can't be exercised beforehand.
🤖 Generated with Claude Code