Skip to content

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

Merged
uipreliga merged 39 commits into
mainfrom
feat/claude-code-plugin
Aug 12, 2026
Merged

feat(plugin): ship coder_eval as a Claude Code plugin + marketplace#82
uipreliga merged 39 commits into
mainfrom
feat/claude-code-plugin

Conversation

@uipreliga

@uipreliga uipreliga commented Aug 5, 2026

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, audit the tasks you already have, 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 six slash commands: /coder-eval:init, /coder-eval:check-skill,
/coder-eval:task, /coder-eval:lint-tasks, /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 the skill listing: 1,576 characters of description across all six,
always-on for as long as the plugin is installed. Bodies load only on invoke. That budget
is shared with every skill the user has installed, so a test caps the total at 1,600 —
raising the ceiling is allowed in a commit that says why, which is exactly what silent
drift would not be. Current headroom is 24 characters, so the next skill added here is a
deliberate decision rather than an accident.

What a reviewer should check

The src/ diff is three files, +46/−15, and is behavior-preserving. It is not empty —
an earlier revision of this description said it was, and that stopped being true at
ca64bc4:

  • models/tasks.pyNORMALIZED_CRITERION_ALIASES and REMOVED_CRITERION_TYPES hoisted
    out of check_removed_criteria_types to module scope, and exported from
    coder_eval.models. The generated criteria reference renders its accepted-alias table
    from the same declaration the loader enforces; a hand-written table there would be a
    second declaration free to drift. The overlay is still applied as {**item, **overlay},
    same order, same semantics.
  • models/criteria.py — one Field(description=...) added to LLMJudgeCriterion.temperature.

No criterion type, agent, CLI flag or merge layer moved, so no existing evaluation result
can change. Everything else new is a distribution surface, one test-harness generator, new
lint clauses, one CI job, and docs.

Three invariants carry the weight:

  1. The bundled criteria reference is generated, not written (CE033,
    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;
    CE033 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 15 criterion sections. The "Accepted aliases" table is
    rendered from NORMALIZED_CRITERION_ALIASES for the same reason.

  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. The Claude Code Plugin job 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.
  • CE034 is new, and the plugin found the bug that motivated it. Running
    /coder-eval:lint-tasks against this repository's own tasks/ surfaced that an armed,
    live-passable command_executed without require_success lets a crashed invocation
    latch a live PASS, fire on_pass: stop, and — because gating is FIRED-ONLY — report
    SUCCESS over the armed subset without ever consulting the unarmed file_exists. Three
    early_stop_* fixtures were affected. Negative assertions are fail-only and are exempt.
  • Two repo-local command twins were retired. .claude/commands/coder-eval-run-analysis.md
    and coder-eval-task-create.md duplicated what /coder-eval:analyze and
    /coder-eval:task now ship — the same run-record vocabulary and task rubric in a second
    place to drift. RUN_RECORD_CONSUMERS collapses to a 1-tuple. The rest of
    .claude/commands/ stays repo-local contributor tooling.
  • 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 six
prompts are any good. So they 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: check-skill 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; lint-tasks found the
real fixture bug now guarded by CE034.

Prompt-quality defects found and fixed this way:

  • 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.
  • analyze also taught a task.json field vocabulary the schema does not have — six of
    thirteen keys were wrong. It now reads the run's actual schema.
  • 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.
  • ci emitted a scheduled skill-drift workflow that never passed SKILL_SOURCE_PATH, so
    the activation suite it pairs with could never reach the skill under test: every
    positive row scores 0 and the job goes red every week for a reason unrelated to drift.

Out of scope

/coder-eval:compare and /coder-eval:triage (v2, once usage is observed); a second
coder-eval-dev plugin; a duplicate example task under tasks/ — the plugin template is
the single canonical copy.

Verification

make format / make check / make lint (332) / make test (4,052 passed, 4 skipped,
coverage gate clean) all green. Both manifests pass claude plugin validate --strict with
zero warnings.

Rebased onto main at 0.9.6. That release bumped pyproject.toml and action.yml but
could not touch plugin.json, which does not exist on main yet, so the pin was advanced
by hand in 17491f0 — caught by test_action_version_pin.py, which is what that test is
for. After merge the release sed owns the file.

CI is green on the rebased head, including Claude Code Plugin (manifests + offline scaffold) — the job an earlier revision of this description flagged as never having run
on a runner. It has now, along with Action Dogfood, the live integration suites and the
Windows smoke test.

One risk remains unrehearsable: merging triggers a 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 cannot be exercised
beforehand.


🤖 Generated with Claude Code

@github-actions

github-actions Bot 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>
uipreliga added a commit that referenced this pull request Aug 10, 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>
@uipreliga
uipreliga force-pushed the feat/claude-code-plugin branch from 8846ff5 to 44044b5 Compare August 10, 2026 22:10
uipreliga added a commit that referenced this pull request Aug 10, 2026
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

This comment was marked as outdated.

@bai-uipath bai-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 check Result
src/ behavior Unchanged. The only diff is a criterion field description.
Release path The 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 marketplace Marketplace 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.

Skill Audience Frequency Verdict
skill-check anyone authoring Claude Code skills every description edit, plus scheduled Keep. The one capability with no CLI or docs equivalent.
lint-tasks anyone with an existing suite periodic audit Keep. Found the early_stop_* fixture bug.
analyze anyone running suites every run Keep, dedupe below.
task suite authors heavy during a build-out, then rare Keep, dedupe below.
init first-time users once per repository Drop. 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.
ci first-time users once per repository Drop. 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 skill Repo-local twin State
analyze coder-eval-run-analysis Rubric and jq field vocabulary are single-declared and test-guarded; the analysis dimensions and scope-marker detection are independent prose in both.
task coder-eval-task-create Rubric 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.

uipreliga added a commit that referenced this pull request Aug 12, 2026
…t run

Three items from the PR #82 review.

The `ci` skill headlines the scheduled trigger as "the skill-drift case ...
the one they forget", but the workflow it emits carries only
ANTHROPIC_API_KEY. An activation suite loads the skill under test through
`agent.plugins`, whose path is `$SKILL_SOURCE_PATH` so the committed task
stays portable — unset, the skill is never offered to the sandboxed agent,
every positive row scores 0, and the job fails its recall threshold every
week. A permanent red that looks exactly like the drift being watched for,
so the real thing goes unnoticed. Step 3 now names the variable, tells the
skill to grep the discovered tasks for the general case, and shows it
resolved against the checkout; step 2 marks it non-optional for that trigger.

Also: tutorial 07 claimed five of the six skills drive the CLI, with
`lint-tasks` the exception. Three do — `init`, `check-skill`, `task`;
`analyze` reads a run directory and `ci` writes a workflow, neither invokes
the CLI. PLUGIN.md already said three.

And CLAUDE.md's list of non-`BaseRule` lint classes omitted CE034.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@uipreliga

Copy link
Copy Markdown
Collaborator Author

Thanks — the ci finding was the sharp one and it was a real bug, not a doc nit.

Scope note first: this review was generated against f8c9691, which is now 11 commits back. Two of its items were already closed by commits it could not see; those are marked below. Everything newly fixed is in 6d9d434.

Net: 3 fixed, 2 already fixed, 1 verified clean, 1 declined.


ci's scheduled drift case measures nothing — ✅ Fixed

Reproduced exactly as filed, and it is worse than "measures nothing".

The skill headlines the scheduled trigger as "the skill-drift case … the one they forget", but the workflow it emits passes only ANTHROPIC_API_KEY. The activation template it pairs with loads the skill under test through agent.plugins, whose path is $SKILL_SOURCE_PATH so the committed suite stays portable across machines. Unset, the skill is never offered to the sandboxed agent → every positive row scores 0 → the job fails its recall threshold.

So the failure mode is not a silent zero, it is a permanent red every Monday that looks exactly like total drift — which means the one signal the schedule exists to produce is the signal it destroys. Real drift arrives into an already-red job and goes unnoticed.

Step 3 now names the variable, instructs the skill to grep the discovered tasks for the general case (any $ in an agent.plugins path, not just this one name), and shows it resolved against the checkout:

env: |
  ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}
  SKILL_SOURCE_PATH=${{ github.workspace }}/.claude/skills

Step 2 marks it non-optional for that trigger rather than leaving it to the generic "pass your env vars" paragraph, which is what it fell through before.

Tutorial 07 vs. the plugin doc on CLI-driving skills — ✅ Fixed

You were right that they disagree, and the tutorial was the wrong one. It claimed five of six drive the CLI with lint-tasks as the exception; three do — init, check-skill, task (the ones that shell out to coder-eval --version / plan / run). analyze reads a finished run directory and ci writes a workflow; neither invokes the CLI. PLUGIN.md already said three.

CLAUDE.md omits CE034 — ✅ Fixed

Correct. CE032 is a genuine BaseRule in the runner so its absence from that list is right, but CE034 is a dedicated whole-tree class over tasks/. Added, with a line on what it enforces rather than just the ID.

early_stop_* fixtures vs. gate suites — ✅ Verified clean, no change

Worth checking and it holds: those three carry early-stop, weighted-early-stop, decision-budget; pr-checks.yml selects only smoke-pass, smoke-fail, smoke-variants. Nothing else in the repo selects those tags.

Deduplicate analyze / task against their repo-local twins — ✅ Already fixed (a40848b)

.claude/commands/coder-eval-run-analysis.md and coder-eval-task-create.md are deleted. RUN_RECORD_CONSUMERS collapses to a 1-tuple and README/USER_GUIDE point at the plugin. Scope-marker detection is back to one place, not four.


Drop init and ci — ❌ Declined

Your premise checks out: I confirmed disable-model-invocation does not remove a skill from the listing (the Claude Code changelog carries an entry about improving the refusal when Claude tries to invoke one — so the model sees it and is turned away at the door). Both do occupy the shared budget on every turn. The cost is real.

Declining on three grounds:

  1. The budget guard already exists and is the intended control. Descriptions total 1,576 against a 1,600 ceiling that a test enforces, with a comment saying raising it is allowed in a commit that says why. That mechanism was built for exactly this tension. Dropping skills to service a limit that is not yet binding spends the wrong currency.

  2. Discovery is the docs' job, not the model's. "Can't be model-invoked, so it only gets typed by someone who already knows it exists" applies equally to ci, which you keep-listed on other grounds — and both are the first command tutorial 07 and PLUGIN.md tell a new user to type. If a once-per-repository skill is only reachable by someone who read the docs, that is the intended path, not a defect.

  3. The ci case rested on the drift bug, which is now fixed. "Restates the CI docs page" also undersells it — the docs page does not do repository discovery, does not resolve a version pin, and does not emit per-depth globs, all of which exist because the action's input expansion has globstar and nullglob off.

Happy to revisit when the budget actually binds — at that point the honest move is probably trimming the four long descriptions rather than removing capability, since lint-tasks (307) and check-skill (289) cost more than either candidate for removal (219 / 218).


make format / check / lint (332) / test (4,052 passed, 4 skipped) clean.

uipreliga and others added 16 commits August 12, 2026 10:44
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>
uipreliga and others added 23 commits August 12, 2026 10:44
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>
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>
`.claude/commands/coder-eval-run-analysis.md` and `coder-eval-task-create.md`
duplicated what `/coder-eval:analyze` and `/coder-eval:task` now ship, so they
were two more copies of the same run-record vocabulary and task rubric to drift.
`RUN_RECORD_CONSUMERS` collapses to a 1-tuple, and README/USER_GUIDE point at
the plugin instead.

Also corrects `init`'s claim that `coder-eval plan` with no argument discovers
`tasks/` — it resolves against the installed package's parent and exits 1
anywhere but a source checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and runs/latest

Every skill carried its own guess at where a repository keeps its evaluation
tree, so the plugin only worked in a repo laid out like this one. The discovery
policy is now declared once, in reference/repo-layout.md, and all six skills
point at it: glob for `*.yaml` files carrying `task_id:` and for `run.json`,
prune the usual vendor directories, report what you resolved, ask when several
candidates tie, and stop when there are none. A directory literally named
`tasks/` is a tiebreaker, never a default.

run-layout.md loses its `runs/latest` default in favour of the same discovery
plus a `latest`-else-newest rule — stated as path-free prose in both copies,
since the shared original is read by a repo-local command where
${CLAUDE_PLUGIN_ROOT} is undefined. `.claude/commands/coder-eval-review.md`, its
only in-repo reader, follows.

Guarded by test_repo_layout_is_bundled_and_read_by_its_readers (the reference
ships and every reader points at it) and test_skills_do_not_hardcode_an_eval_root
(no skill falls back to `runs/latest` or `tasks/` when handed nothing).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…neration's

`analyze` denied a top-level `turns` key flatly, which is true of current runs
and false of anything written before the rename — so an agent reading a real
older run was told its correct extraction was wrong. The denial is now a
two-generation table (`iterations`/`turns` at the top level;
`run_limits.max_turns`/`max_iterations` inside `task_config`) plus a concrete
"run `jq 'keys'` on one file and read the result" instruction, and the neither-
spelling case is called out as an unusable record rather than a row of nulls.

The four names that were never top-level in ANY generation keep their denial,
now qualified as such — a guard pins them so a future rewrite of that sentence
cannot take them along.

run-layout.md's `task.json` bullet follows (both copies), and the new legacy-key
guard derives the accepted older names from `EvaluationResult`'s AliasChoices
rather than a literal list, scoped to top-level keys — `max_iterations` is a
free-form `task_config` key and appears in no alias at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing anything

A repository that already uses coder-eval usually pins the version, and the
CLI-driving skills happily validated it with whatever binary was on PATH. That
is worse than unhelpful: a schema error from a mismatched CLI is
indistinguishable from a real one, so the verdict is unsound in both directions
— and "fixing" what a newer CLI reports edits the repo's tasks into a shape its
own pinned CLI rejects.

cli-setup.md gains a `## Version pin` section declaring the policy once: find the
pin (version file, requirement, CI install line), prefer a project-local binary
over PATH, report both, and stop on a mismatch without validating and without
upgrading. "Version skew" no longer terminates in "upgrade" — that branch is now
reachable only when no pin exists, because upgrading past a pin is a
repo-breaking action rather than a fix.

The three CLI-driving skills each gain one clause pointing at the reference; none
restates the resolution steps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three skills assumed they were arriving at an empty repository.

`skill-check` gains a step, before row design, that globs the task tree for a
suite already carrying a `skill_triggered` criterion. If one covers this skill it
reports the coverage and offers to extend it rather than scaffolding a rival
suite — two suites for one skill drift, and the user pays for both every run. The
same step checks whether an experiment already supplies `agent.plugins`, in which
case the task inherits it instead of redeclaring it.

`init` gains an inventory step that can END the skill: run against a repository
that already has tasks it reports what is there, points at lint-tasks and analyze,
and stops. Its never-overwrite rule now has exactly one home instead of two.

`lint-tasks` offers to narrow above roughly 50 tasks — by subdirectory, glob, or a
changed set the user supplies, since it holds no Bash and cannot work out what
changed itself. A guard keeps that true.

Steps renumbered in both edited skills, with every in-body cross-reference
followed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ring the wrong set

`task` now treats repo-local convention as beating the bundled rubric: it reads
whatever the repository declares about task authoring, adopts it, and reports
which conventions it adopted. The rubric says the same in one line and keeps one
exception — precedence covers style, not soundness.

`ci` gains the three things it could not previously express. It passes an
experiment through `extra-args`, because an experiment usually supplies `agent:`
config and omitting it silently changes what the gate measures. It conditions the
`version:` input on whether the repository pins a version instead of forbidding
it outright. And it emits explicit per-depth globs for a nested task tree rather
than `**`: the action expands `tasks:` unquoted with globstar off, so
`a/**/*.yaml` degrades to `a/*/*.yaml` and drops every top-level task silently —
reproduced, along with the nullglob half, where an unmatched pattern reaches the
CLI literally and exits 1. Both are recorded in harness-candidates.md as
action-side fixes; action.yml is unchanged here.

docs/PLUGIN.md gains an "Adopting this where coder-eval already exists" section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, from the models

The loader has always rewritten `command_not_executed` and rejected three
removed criterion types, and neither fact reached the plugin's bundled reference
— so an authoring agent reading a task that used one had nothing to look it up
in and no way to find the replacement.

Both literal maps move out of the validator body to module-level constants
(`NORMALIZED_CRITERION_ALIASES`, `REMOVED_CRITERION_TYPES`, exported from
`coder_eval.models`), and the generated criteria reference renders an "Accepted
aliases" section from the same declaration the validator enforces — so a new
alias cannot ship undocumented. The `command_not_executed` special case becomes
a map lookup rather than a second `if`.

Behaviour is unchanged, and checked rather than asserted: a differential harness
ran the old validator body against the new one across 17 input classes — no
list, non-dict items, missing `type:`, `type: None`, every removed type, the
alias with and without explicit counts, and an unhashable `type:` — with zero
differences, resulting key order included. The `{**item, **overlay}` merge order
is load-bearing (reversing it turns a "was not run" assertion into its
inverse) and is now pinned by two tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four findings from the final multi-model review.

Three are in this change's own new prose:

- `cli-setup.md` told the agent to take the first pin it found AND to report
  two disagreeing pins — the first instruction makes the second unreachable. It
  now collects every pin, uses the version they agree on, and asks when they
  don't. It also no longer assumes an eval root exists, which `init` cannot
  guarantee since it runs before one does.
- `repo-layout.md` used "run root" for both a directory containing `run.json`
  and the directory containing those — so an agent could resolve `latest` at the
  wrong level. The individual directory keeps the name "run root" (matching
  run-layout.md's pre-existing scope markers) and the parent is now the "run
  store", consistently across all three files.
- Resolving `latest` required only that the symlink resolve. A symlink is
  repository content and can point outside the store or the repository, so it
  now must resolve to a directory inside the store carrying a `run.json`;
  anything else is a finding rather than a path to read.

One is pre-existing and outside the plan's scope, fixed because it is one line
in a file this change already edits and is the same class of defect Phase 2
addressed: `analyze` told the agent to read `??/task.json` at variant and run
scope, where a replicate actually sits at `<task_id>/<NN>/task.json` or one
level deeper. That pattern matches nothing at either scope, so the analysis
would silently omit every task.

Two further pre-existing `action.yml` defects the review surfaced (the score
gate dropping malformed scores, and `tasks:` being advertised as optional when
omitting it cannot work) are recorded in harness-candidates.md — action.yml is
deliberately unchanged by this plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…covery

The set of skills reading reference/repo-layout.md was hand-maintained, so a
seventh skill could ship having silently skipped discovery and reintroduced a
hardcoded `tasks/`. It becomes a mapping, mirroring SKILL_DISABLE_MODEL_INVOCATION
right beside it: every shipped skill must state a stance, and `False` is a fine
answer for a skill that touches no task or run tree — it just has to be written
down rather than defaulted into.

Closes the one harness candidate from the plugin generic-adopter run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aming rule down

The six skills had drifted into four naming patterns — bare verb (`init`,
`analyze`), bare noun (`task`, `ci`), verb-object (`lint-tasks`) and
object-verb (`skill-check`). Only the last is a genuine defect: it reads
backwards next to `lint-tasks`, the same grammatical shape reversed.

So `skill-check` becomes `check-skill`, and CLAUDE.md now states the rule a
seventh skill has to follow: verb-first imperative, a bare verb where the
argument supplies the object, `<verb>-<object>` otherwise, never
`<object>-<verb>`.

`task` and `ci` predate the rule and stay. Renaming a published skill costs
every user their muscle memory and buys nothing functional — activation keys on
the frontmatter `description`, never on the name.

BREAKING CHANGE: `/coder-eval:skill-check` is now `/coder-eval:check-skill`.
Nothing in the codebase dispatches on the name (the manifests do not enumerate
skills; they are discovered from `skills/*/SKILL.md`), so this is a rename of
the directory plus its 37 references across the four documented surfaces, the
tutorial, four skill bodies that cross-reference it, and the hand-maintained
name sets in tests/test_custom_lint.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 5 taught `/coder-eval:ci` never to emit `**` in the action's `tasks:`
input, because the action expands it unquoted with globstar off — so
`tests/tasks/**/*.yaml` collapses to `tests/tasks/*/*.yaml` and silently drops
every top-level task. It did not touch the five snippets showing exactly that
pattern in README.md (x2), docs/CI_GATE.md (x2) and the CI tutorial, which left
the plugin contradicting the repo's own onboarding docs — and those snippets are
the ones integrators copy.

All five now show explicit per-depth globs, and CI_GATE.md — the Action's
reference page — carries the reasoning once, alongside two related sharp edges:
an unmatched glob reaches the CLI verbatim and fails the run (nullglob is off
too), and omitting `tasks` does not run everything. Its inputs table claimed a
default of "all `tasks/`", which is not what happens: the CLI is invoked with no
paths, and zero-argument discovery resolves against the installed package rather
than the checkout, finds nothing, and exits 1.

The guard that only watched the `ci` skill now covers every surface CE026 scans,
so a `**` cannot come back in any of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t run

Three items from the PR #82 review.

The `ci` skill headlines the scheduled trigger as "the skill-drift case ...
the one they forget", but the workflow it emits carries only
ANTHROPIC_API_KEY. An activation suite loads the skill under test through
`agent.plugins`, whose path is `$SKILL_SOURCE_PATH` so the committed task
stays portable — unset, the skill is never offered to the sandboxed agent,
every positive row scores 0, and the job fails its recall threshold every
week. A permanent red that looks exactly like the drift being watched for,
so the real thing goes unnoticed. Step 3 now names the variable, tells the
skill to grep the discovered tasks for the general case, and shows it
resolved against the checkout; step 2 marks it non-optional for that trigger.

Also: tutorial 07 claimed five of the six skills drive the CLI, with
`lint-tasks` the exception. Three do — `init`, `check-skill`, `task`;
`analyze` reads a run directory and `ci` writes a workflow, neither invokes
the CLI. PLUGIN.md already said three.

And CLAUDE.md's list of non-`BaseRule` lint classes omitted CE034.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`plugins/coder-eval/.claude-plugin/plugin.json` carries a derived pin of
pyproject's version, kept in sync by release.yml's bump step. The 0.9.6
release ran on main before this branch merged, so it bumped action.yml and
pyproject but could not touch a file that does not exist on main yet.
Rebasing therefore lands 0.9.6 sources next to a 0.9.5 manifest, which
tests/test_action_version_pin.py catches by design.

Once this branch merges the sed owns the file and this stops being manual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@uipreliga
uipreliga force-pushed the feat/claude-code-plugin branch from 6d9d434 to 17491f0 Compare August 12, 2026 17:47
@uipreliga
uipreliga merged commit 02e5151 into main Aug 12, 2026
16 checks passed
@uipreliga
uipreliga deleted the feat/claude-code-plugin branch August 12, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants