Skip to content

feat(plugin): ship coder_eval as a Claude Code plugin + marketplace - #82

Open
uipreliga wants to merge 26 commits into
mainfrom
feat/claude-code-plugin
Open

feat(plugin): ship coder_eval as a Claude Code plugin + marketplace#82
uipreliga wants to merge 26 commits into
mainfrom
feat/claude-code-plugin

Conversation

@uipreliga

Copy link
Copy Markdown
Collaborator

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-eval
plugin, 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.

/plugin marketplace add UiPath/coder_eval
/plugin install coder-eval@coder-eval

That adds five slash commands: /coder-eval:init, /coder-eval:skill-check,
/coder-eval:task, /coder-eval:analyze, /coder-eval:ci. They drive the coder-eval
CLI, 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); bodies
load 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:

  1. 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 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. make plugin-reference renders it from the SuccessCriterion union;
    CE032 re-renders and diffs. Inherited base fields and the discriminator name are
    computed, never listed — the stop_early refactor is exactly the change a hardcoded
    list would have leaked into all 14 sections.

  2. The frontmatter guards are load-bearing, not belt-and-braces. Verified by spike:
    claude plugin validate --strict does not inspect skill frontmatter at all — a
    SKILL.md carrying both an unsupported name: and an invented key passes with exit 0
    and zero warnings. TestPluginArtifacts is the only thing standing between a typo'd
    key 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.

  3. The CI assert expands, it does not just validate.coder-eval plan exits 0 even
    when dataset.paths names a file that was never copied — verified both ways — so a
    plan-only check would have passed vacuously. plugin-validate copies the activation
    template into $RUNNER_TEMP, outside the source tree, and asserts expand_dataset
    yields 6 row-tasks with both label polarities. No secrets, no agent run, no per-PR cost.

Also in here

  • CE026 gains two things: its doc scan now covers plugins/**/*.md (the ci skill
    emits 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's
    action step must be a real action.yml input. GitHub ignores unknown inputs rather than
    failing, so a rename would silently degrade every copied workflow; simulating a model
    rename fires on all four surfaces. Consequence to know: renaming an action input, or
    changing the action's runtime prerequisites, now means updating the ci skill too.
    Documented in CLAUDE.md.
  • plugin.json carries a version pinned to pyproject.toml's, because
    --strict rejects a manifest without one. release.yml's existing bump step seds it
    alongside action.yml; tests/test_action_version_pin.py — already the owner of that
    invariant — 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

  • a stub CLI) and, for analyze, a real 947-task two-variant experiment run — tools
    allowlisted so no paid coder-eval run could fire.

They behaved: skill-check designed 18 rows (10 positive / 8 distractor, none naming the
skill, distractors genuinely adjacent), validated, and stopped to ask before spending;
init wrote .env.example and never .env; ci chose pull_request over
pull_request_target with explicit reasoning about fork secrets.

Three prompt-quality defects were found and fixed:

  • analyze got 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.
  • init scaffolded a task whose prompt dictated pypdf while a criterion grepped for it —
    a criterion that cannot fail but reads as coverage.
  • task gave weight 1.5 to a criterion matching a literal (--json) the prompt has to
    name.

Out of scope

/coder-eval:compare and /coder-eval:triage (v2, once usage is observed); a second
coder-eval-dev plugin — .claude/commands/ stays repo-local and is byte-for-byte
unchanged; a duplicate example task under tasks/ — the plugin template is the single
canonical 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 --strict with
zero warnings.

Two things this branch cannot rehearse, both worth watching:

  • plugin-validate has never run on a runner. Every step was verified locally,
    including the scaffold assert and its negative, but the job itself is new.
  • Merging triggers a minor release, which runs the new plugin.json sed for the first
    time.
    It fails loudly on a miss (grep -q guard) and the at-rest test catches drift
    afterwards, but the release path can't be exercised beforehand.

🤖 Generated with Claude Code

@github-actions

github-actionsBot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @uipreliga's task in 1m 33s —— View job


Code Review in Progress

Todo List:

  • Read .github/code_review.md for review standards
  • Read CLAUDE.md for project conventions
  • Get full PR diff from origin/main
  • Review all 27 changed files with full context
  • Verify the three claimed invariants
  • Check cross-file consistency
  • Perform "what's missing" analysis
  • Format and post final review

Starting review now...

uipreliga

This comment was marked as outdated.

@uipreliga

This comment was marked as outdated.

uipreliga added a commit that referenced this pull request Aug 5, 2026
…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>
uipreligaand others added 23 commits August 10, 2026 15:01
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>
uipreligaand others added 2 commits August 10, 2026 15:03
…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>
@uipreliga
uipreligaforce-pushed the feat/claude-code-plugin branch from 8846ff5 to 44044b5CompareAugust 10, 2026 22:10
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>
@uipreliga

Copy link
Copy Markdown
CollaboratorAuthor

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 f8c9691, each verified against the real schema and CLI rather than by reading.


1. [Axis: What's Missing] analyze teaches a task.json field vocabulary the schema does not have — ✅ Fixed

The sharpest of the remaining findings, and it reproduced exactly as filed. Six of the thirteen keys in the jq summary template do not exist at the top level of a real task.json:

DocumentedActual
turnsiterations
total_tokenstotal_token_usage.input_tokens + .output_tokens
assistant_turn_counttotal_assistant_turns
max_turnstask_config.resolved.run_limits.max_turns
total_cost_usdtotal_token_usage.total_cost_usd
criteria_count / all_criteria_perfectderived 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.

@bai-uipathbai-uipath left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve — no runtime behavior change, so the blast radius on existing CLI and Action users is effectively nil.

Blast-radius checkResult
src/ behaviorUnchanged. The only diff is a criterion field description.
Release pathThe new plugin.json sed runs before the tag push, wheel build and PyPI job, so a miss fails the run with nothing published.
Repo-root marketplaceMarketplace registration is always explicit, so contributors working in this repo don't silently load the six skills.

Two of the six don't earn a permanent slot in the skill listing.

SkillAudienceFrequencyVerdict
skill-checkanyone authoring Claude Code skillsevery description edit, plus scheduledKeep. The one capability with no CLI or docs equivalent.
lint-tasksanyone with an existing suiteperiodic auditKeep. Found the early_stop_* fixture bug.
analyzeanyone running suitesevery runKeep, dedupe below.
tasksuite authorsheavy during a build-out, then rareKeep, dedupe below.
initfirst-time usersonce per repositoryDrop. Punts to skill-check for the highest-value case, and can't be model-invoked, so it only gets typed by someone who already knows it exists.
cifirst-time usersonce per repositoryDrop. Restates the CI docs page, and the workflow it emits never carries the skill-source variable, so its headline scheduled-drift case measures nothing.

Unlike a slash command, a skill's description occupies the listing on every turn for as long as the plugin is installed, on a budget shared with the user's own skills. Two once-per-repository entries is a poor trade for that.

Deduplicate the two skills that already have repo-local twins.

Plugin skillRepo-local twinState
analyzecoder-eval-run-analysisRubric and jq field vocabulary are single-declared and test-guarded; the analysis dimensions and scope-marker detection are independent prose in both.
taskcoder-eval-task-createRubric shared, the rest restated.

Scope-marker detection now lives in four places. Fix: extend what the rubric already does here. The plugin can't read .claude/ from its install cache, so the direction has to be repo-local pointing at the bundled copy.

Minor: the three early_stop_* fixtures got stricter, so confirm no gate suite selects that tag; tutorial 07 and the plugin doc disagree on how many skills drive the CLI; CLAUDE.md's list of non-BaseRule lint classes omits CE034.

Sign up for freeto 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.

2 participants

@uipreliga@bai-uipath