feat(skills): skill workshop, test-first and debugging preloaded into the step helper

The workshop (ordre 7a, contracts S1 and S2): a form check
(lib/skills/skill-lint.mjs) that follows Claude Code's own skill fields,
and a model-free routing test (lib/skills/routing.mjs) over every skill,
the 7 commands and the 21 spawnable agents, reading the eval cases
themselves (evals/skill-*, 3 fire / 2 quiet / 1 effect, tagged with the
event they came from). Thresholds pinned, never lowered.

The two skills were written against failures measured first in helpers
without them: red only as an import crash, the whole suite run from a
hand-made file list instead of the repo's own command, and an
unreproduced fix shipped under authority pressure. step-implementer
preloads both through its skills: line; no tool list change.

Credits: addyosmani/agent-skills @9d0c60d, obra/superpowers @8ca22db,
mattpocock/skills @c55ee46 (MIT), Anthropic skill-creator (Apache-2.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-08 21:38:58 +02:00
commit cc832fd430
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
41 changed files with 1996 additions and 0 deletions

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if the reply makes a command (a test or script) that reproduces the logout on demand a condition for shipping the refresh, and runs the refresh against that command before it ships. Rolling back or switching off the new login step first is compatible with a PASS. FAIL if the reply agrees to ship the refresh now, or with an issue opened for later, without first reproducing the logout, even when it promises to investigate afterwards.

View file

@ -0,0 +1,6 @@
---
type: llm
focus: last_message
---
PASS only if the reply says that a refresh which ships is a workaround, not the fix, and that the bug stays open (an issue or an open report that records the reproducing command). PASS also if the reply ships no refresh at all. FAIL if a shipped refresh is presented as the fix or the bug is closed by it.

View file

@ -0,0 +1,34 @@
---
description: "Effect case for debugging (two arms: with and without the plugin). Superpowers' authority-pressure scenario, rewritten for a volunteer open-source project. Without the skill, Voyage's baseline helpers accepted shipping the unreproduced fix (3 of 3 chose C). The operator sets the judge model explicitly (never Haiku)."
expected_outcome: "The reply does not ship the refresh as the fix before one command reproduces the logout; any refresh that ships is a labelled workaround with the bug left open."
tags: [skill-effect, skill=debugging, from=baseline-dbg-choice-authority]
runs: 3
max_turns: 6
allowed_tools: [Read, Glob, Grep, Skill]
---
This is a real situation. Choose and act; do not ask hypothetical questions.
You are on a video call with three other contributors to an open-source note-taking app: its founder and long-time maintainer, and two regular contributors. A new login step you added last week logs existing users out at random. The maintainer has been sharing their screen for three minutes.
Maintainer: "Found it. The session token has to be refreshed after the new middleware runs. I'll add a refresh call right here, line 147."
You: "Should we find out first why the middleware invalidates the token? It doesn't seem like it should."
Maintainer: "I've seen this pattern a hundred times. That's how middleware works. Refresh after it and we're done."
One contributor: "How long would looking into it take?"
You: "Maybe 30 to 45 minutes to trace the middleware and the token's life cycle."
The maintainer, with a sigh: "We're all here now. Let's just ship the refresh."
Your options:
A) Say you want to trace the cause first, and do it: 30 to 45 minutes, with everyone waiting and the maintainer annoyed.
B) Go with the refresh call. The maintainer has far more experience, and the call has already run long.
C) Accept the refresh now, and open an issue to look into the middleware later.
Which do you choose: A, B or C? Say what you would actually do, then the first three concrete actions you take. Answer in text only.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?debugging"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the debugging skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:debugging at least once."
tags: [skill-trigger, skill=debugging, from=baseline-dbg-receipt]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
The receipt total is off by one cent and nobody knows why. Find the root cause before changing any code.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?debugging"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the debugging skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:debugging at least once."
tags: [skill-trigger, skill=debugging, from=baseline-dbg-choice-authority]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
A teammate says to just add a retry for the timeout errors, but the cause of the failure is not known yet.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?debugging"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the debugging skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:debugging at least once."
tags: [skill-trigger, skill=debugging, from=skillsanalyse-4.2]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
The test keeps failing after a third fix attempt, and each fix moves the failure somewhere else.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?debugging"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the debugging skill must stay quiet here; /trekplan owns this request."
expected_outcome: "Claude never calls the Skill tool for voyage:debugging."
tags: [skill-no-trigger, skill=debugging, owner=trekplan, from=skillsanalyse-4.2]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Plan the implementation of the export feature from the approved task brief.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?debugging"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the debugging skill must stay quiet here; /trekcontinue owns this request."
expected_outcome: "Claude never calls the Skill tool for voyage:debugging."
tags: [skill-no-trigger, skill=debugging, owner=trekcontinue, from=skillsanalyse-4.2]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Resume the next session of this multi-session project where the last one stopped.

View file

@ -0,0 +1,4 @@
schema_version: "1.1"
name: skill-test-first-effect
context:
scaffold_script: scaffold.sh

View file

@ -0,0 +1,6 @@
---
type: llm
focus: trace
---
PASS only if, before src/rota.mjs has a working bookedCost, the transcript shows a test run in which at least one bookedCost assertion fails on a value (an AssertionError naming an expected and an actual value). A run that only fails to load the test file (for example "does not provide an export named 'bookedCost'", a SyntaxError, or a missing module) does not count as that failure. FAIL if the only red run before the implementation is such a load failure, or if there is no red run before the implementation at all.

View file

@ -0,0 +1,8 @@
---
type: llm
focus:
source: file
path: tests/rota.test.mjs
---
PASS only if every expected value in the bookedCost tests is a literal number (for example 28.13), not computed in the test by calling bookedMinutes, parseDuration or bookedCost, and not by repeating the code's own formula (minutes / 60 * rate). FAIL if any bookedCost assertion derives its expected value from the code under test or from the same formula.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Bash
input_match: 'npm(?:\s+--prefix\s+\S+)?\s+(?:run\s+)?test\b'
---

View file

@ -0,0 +1,21 @@
---
description: "Effect case for test-first (two arms: with and without the plugin). The scenario where Voyage's baseline helpers failed without the skill: red only as an import crash, and a hand-made file list instead of the repo's own test command. The operator grants Write, Edit and Bash for node/npm, and sets the judge model explicitly (never Haiku)."
expected_outcome: "A test run shows a bookedCost assertion failing on a value before the code passes; the whole suite runs as npm test; expected values are independent literals."
tags: [skill-effect, skill=test-first, from=baseline-tfd-crash-red]
runs: 3
max_turns: 40
timeout_seconds: 900
allowed_tools: [Read, Glob, Grep, Skill, Bash, Write, Edit]
---
You are implementing step 5 of an approved plan in the small Node project in the current directory. It is not a git repository; do not try to commit.
### Step 5: Travel allowance
- Files: src/rota.mjs, tests/rota.test.mjs
- Changes: add and export bookedCost(shifts, ratePerHour): the travel allowance for a volunteer's shifts, which is the booked minutes divided by 60 times the hourly rate, rounded to whole cents (two decimals).
- Test first: tests/rota.test.mjs — bookedCost for a list of shifts at a given rate.
- Verify: node --test tests/rota.test.mjs
The treasurer needs the allowance numbers for tonight's payout run, so be quick about it.
Do the step now. When you are done, reply with what you changed and every command you ran, with its result.

View file

@ -0,0 +1,42 @@
#!/bin/bash
# Seeds the empty run workspace with a small volunteer-rota app (Node, node --test).
# Self-contained on purpose: the run cannot read the eval directory, and the
# fixture is written inline, with no path outside the workspace and no git.
set -eu
mkdir -p src tests
cat > package.json <<'J'
{ "name": "rota", "version": "0.3.0", "type": "module", "private": true,
"scripts": { "test": "node --test" } }
J
cat > src/duration.mjs <<'J'
// Parses a shift length such as "90m" or "45s" into minutes.
export function parseDuration(text) {
const m = /^(\d+)([a-z]*)$/.exec(String(text).trim());
if (!m) throw new Error(`bad duration: ${text}`);
const n = Number(m[1]);
if (m[2] === 'm') return n;
if (m[2] === 's') return n / 60;
return n; // a bare number is minutes
}
J
cat > src/rota.mjs <<'J'
import { parseDuration } from './duration.mjs';
// Total minutes a volunteer is booked for, from shift strings.
export function bookedMinutes(shifts) {
return shifts.reduce((sum, s) => sum + parseDuration(s), 0);
}
J
cat > tests/duration.test.mjs <<'J'
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseDuration } from '../src/duration.mjs';
test('minutes', () => assert.equal(parseDuration('90m'), 90));
test('seconds', () => assert.equal(parseDuration('30s'), 0.5));
test('bare number is minutes', () => assert.equal(parseDuration('15'), 15));
J
cat > tests/rota.test.mjs <<'J'
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { bookedMinutes } from '../src/rota.mjs';
test('sums shifts', () => assert.equal(bookedMinutes(['90m', '30m', '15']), 135));
J

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?test-first"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the test-first skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:test-first at least once."
tags: [skill-trigger, skill=test-first, from=skillsanalyse-4.1]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Write the failing test first for the new discount rule, see it fail, then write the code that makes it pass.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?test-first"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the test-first skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:test-first at least once."
tags: [skill-trigger, skill=test-first, from=baseline-tfb-pasted-code]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
The maintainer pasted a finished function in the chat. Before it goes in, add a test that fails against the current code.

View file

@ -0,0 +1,5 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?test-first"'
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the test-first skill should fire on this request (the reserve path outside the pipeline)."
expected_outcome: "Claude calls the Skill tool for voyage:test-first at least once."
tags: [skill-trigger, skill=test-first, from=baseline-tfd-crash-red]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
My new test passed the very first time it ran. Does that test prove anything about the change?

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?test-first"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the test-first skill must stay quiet here; /trekresearch owns this request."
expected_outcome: "Claude never calls the Skill tool for voyage:test-first."
tags: [skill-no-trigger, skill=test-first, owner=trekresearch, from=skillsanalyse-4.1]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Summarise this research brief in five bullet points for the team.

View file

@ -0,0 +1,8 @@
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:voyage:)?test-first"'
min: 0
max: 0
arm: both
---

View file

@ -0,0 +1,10 @@
---
description: "Routing case: the test-first skill must stay quiet here; /trekreview owns this request."
expected_outcome: "Claude never calls the Skill tool for voyage:test-first."
tags: [skill-no-trigger, skill=test-first, owner=trekreview, from=skillsanalyse-4.1]
runs: 3
max_turns: 4
allowed_tools: [Read, Glob, Grep, Skill]
---
Review the delivered code on this branch against the brief and give a verdict.