feat(run): the run step names Claude Code's run; owner=run is a built-in owner (A2)

The coupling to Claude Code's built-in run skill (wave 4, order A2,
decisions 54 and 64). The plan's "run the thing" step, the executor's
End to end part and the plan template each say in one sentence that the
app is seen and driven with run, and that the verdict is still the
End-to-end: command, because run gives no exit code. The template quotes
the name instead of backticking it: every backticked span on a template
step bullet is read as a runnable example.

R2: lib/skills/routing.mjs keeps a short fixed list of Claude Code's own
skills a quiet case may name as owner (BUILTIN_OWNERS, only run, seen in
2.1.303). For such an owner only "not taken" is checked, and the report
says so; ROUTING_NEGATIVE_WON stands, and an owner on no list is still
ROUTING_UNKNOWN_OWNER. skill-writing-criteria-quiet-2 gets owner=run.

The trekplan.md paragraph is reflowed to its old line count so the stats
line_ref there does not move (order B edits the same file); the
trekexecute Phase 9 line_ref fixture moves by two lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-19 18:26:10 +02:00
commit 1989656a9c
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
12 changed files with 141 additions and 22 deletions

View file

@ -20,6 +20,8 @@
// A case is tagged in its prompt.md frontmatter:
// tags: [skill-trigger | skill-no-trigger | skill-effect, skill=<name>,
// owner=<command|agent|skill name> (negatives), from=<event>]
// An owner may also be one of Claude Code's own skills on BUILTIN_OWNERS (R2);
// it stands outside the corpus, so only "the skill does not rank first" is checked.
// `from=` names the event the case came from (P10, R1): it is required on every
// case in evals/, and a case with no skill tag (the fixed test set) is not
// routed, only checked for `from=`.
@ -212,6 +214,16 @@ export function loadCases(root) {
// ---- the check ----
/**
* Claude Code's own skills a negative case may name as its owner (R2). They
* stand outside the corpus, so for such an owner only "not taken" is checked:
* the skill under test must not rank first. Kept short and fixed.
* run - the bundled "Launch and drive your app to see a change working"
* skill, seen in Claude Code 2.1.303 (code.claude.com/docs/en/skills,
* "Run and verify your app").
*/
export const BUILTIN_OWNERS = Object.freeze(['run']);
/**
* Run the whole routing check over a plugin root.
* @returns {{ errors: object[], warnings: object[], stats: object }}
@ -228,6 +240,7 @@ export function checkRouting(root, t = THRESHOLDS) {
const cases = all.filter((c) => c.kind);
const errors = [];
const warnings = [];
const notes = [];
let positives = 0; let rank1 = 0; let negatives = 0;
for (const c of collisions(corpus, t)) {
@ -265,6 +278,9 @@ export function checkRouting(root, t = THRESHOLDS) {
}
if (!c.owner) {
errors.push({ code: 'ROUTING_NEGATIVE_NO_OWNER', case: c.case, message: `${c.case}: a negative case names the owner that should take the prompt (owner=...)` });
} else if (!byName.has(c.owner) && BUILTIN_OWNERS.includes(c.owner)) {
notes.push({ code: 'ROUTING_OWNER_OUTSIDE_CORPUS', case: c.case,
message: `${c.case}: owner=${c.owner} is Claude Code's own; owner outside the corpus: only "not taken" checked` });
} else if (!byName.has(c.owner)) {
errors.push({ code: 'ROUTING_UNKNOWN_OWNER', case: c.case, message: `${c.case}: owner=${c.owner} is no skill, command or spawnable agent` });
} else {
@ -290,7 +306,7 @@ export function checkRouting(root, t = THRESHOLDS) {
if (positives && rate < t.MIN_RANK1_RATE) {
errors.push({ code: 'ROUTING_RANK1_FLOOR', message: `${rank1} of ${positives} positives rank their skill first (${(rate * 100).toFixed(0)}%); the floor is ${(t.MIN_RANK1_RATE * 100).toFixed(0)}%` });
}
return { errors, warnings, stats: { entries: entries.length, skills: skills.length, sourced: all.length, cases: cases.length, positives, negatives, rank1 } };
return { errors, warnings, notes, stats: { entries: entries.length, skills: skills.length, sourced: all.length, cases: cases.length, positives, negatives, rank1 } };
}
// ---- CLI ----
@ -308,6 +324,7 @@ if (isMain(import.meta.url)) {
else {
for (const e of r.errors) process.stdout.write(`FAIL ${e.code}: ${e.message}\n`);
for (const w of r.warnings) process.stdout.write(`warn ${w.code}: ${w.message}\n`);
for (const n of r.notes) process.stdout.write(`note ${n.code}: ${n.message}\n`);
process.stdout.write(`${r.stats.entries} descriptions, ${r.stats.sourced} cases checked for from=, ${r.stats.cases} routed, ${r.stats.rank1}/${r.stats.positives} positives first; ${r.errors.length} finding(s)\n`);
}
process.exit(r.errors.length ? 1 : 0);