Nine skills documented the payload. None named `session_id`, which is the tool's only required field. So every documented call was one the tool rejects — an agent following any of them either failed the call or invented an id, and invented ids do not group, which means the pass-rate the /pass-rate endpoint computes was being assembled from entries that cannot be joined. Found by the #19 pressure test against `debug`, which invented `debug-signed-artifact-missing-2026-08-29` and said so: "my three entries are only groupable by luck of naming". The grep afterwards showed it was fleet-wide, not a `debug` bug. Also removed: `duration_ms: <wall-clock>` in eight of them. An agent does not observe a clock. The same run omitted it "rather than fabricate", which is the good outcome; the bad outcome is the run that fabricates, and a made-up number in a metrics stream is worse than a missing one. It is an optional field and is now simply absent. bin/check-session-log gates both, with tests. This is exactly the case #19 has in mind for a small tool over prose: the rule is a grep, so it should be a gate, not a note asking people to remember. Closes part of #36 Bump-Type: patch
159 lines
6.6 KiB
YAML
159 lines
6.6 KiB
YAML
version: '3'
|
|
|
|
# Skills library Taskfile.
|
|
#
|
|
# This repo is two things at once:
|
|
# 1. A Claude Code PLUGIN (skills + hooks + MCP servers), distributed through
|
|
# the marketplace this same repo hosts at .claude-plugin/marketplace.json.
|
|
# 2. A plain skills library for harnesses with no plugin system — Crush, Pi,
|
|
# OpenCode — wired by install.sh as ONE directory symlink per harness.
|
|
#
|
|
# The wiring loops that used to live here were five near-identical copies that
|
|
# drifted; install.sh owns wiring now and this file delegates to it.
|
|
|
|
vars:
|
|
REPO_URL: 'https://git.d-ma.be/mathias/skills.git'
|
|
SKILLS_DIR: '{{.TASKFILE_DIR}}/skills'
|
|
PLUGIN_MANIFEST: '{{.TASKFILE_DIR}}/.claude-plugin/plugin.json'
|
|
|
|
tasks:
|
|
|
|
default:
|
|
cmds:
|
|
- task: list
|
|
|
|
list:
|
|
desc: 'Print every skill in this repo'
|
|
cmds:
|
|
- 'ls -1 {{.SKILLS_DIR}}'
|
|
silent: true
|
|
|
|
install:
|
|
desc: 'Wire THIS checkout into Crush / Pi / OpenCode. Claude Code uses the plugin instead.'
|
|
cmds:
|
|
- SKILLS_SKIP_FETCH=1 SKILLS_CHECKOUT_DIR='{{.TASKFILE_DIR}}' bash '{{.TASKFILE_DIR}}/install.sh'
|
|
|
|
update:
|
|
desc: 'Fast-forward this checkout and re-wire.'
|
|
cmds:
|
|
- git -C '{{.TASKFILE_DIR}}' pull --ff-only
|
|
- task: install
|
|
|
|
doctor:
|
|
desc: 'Report drift across every harness on this host. Non-zero exit means something is wrong.'
|
|
cmds:
|
|
- '{{.TASKFILE_DIR}}/bin/doctor'
|
|
|
|
validate:
|
|
desc: 'Validate the plugin + marketplace manifests with the Claude Code CLI.'
|
|
cmds:
|
|
- claude plugin validate '{{.TASKFILE_DIR}}'
|
|
|
|
test:
|
|
desc: 'Run the shell test suite (hooks + doctor).'
|
|
cmds:
|
|
- |
|
|
fail=0
|
|
for t in '{{.TASKFILE_DIR}}'/tests/*_test.sh; do
|
|
[ -f "$t" ] || continue
|
|
echo "--- $(basename "$t")"
|
|
sh "$t" || fail=1
|
|
done
|
|
exit $fail
|
|
|
|
index:
|
|
desc: 'Regenerate SKILLS_INDEX.md''s table from skill frontmatter.'
|
|
cmds:
|
|
- "'{{.TASKFILE_DIR}}/bin/generate-index' '{{.TASKFILE_DIR}}'"
|
|
|
|
check:
|
|
desc: 'Structure gate — layout, frontmatter, surfaces, generated index, manifest/tag parity. Runs in CI.'
|
|
cmds:
|
|
- |
|
|
fail=0
|
|
|
|
# 1. No stray entries at the repo root. A loose foo.md where
|
|
# skills/foo/SKILL.md was meant loads in NO harness and errors in
|
|
# none either — it just silently does nothing. (See #23.)
|
|
for entry in "{{.TASKFILE_DIR}}"/*; do
|
|
base=$(basename "$entry")
|
|
case "$base" in
|
|
install.sh|Taskfile.yml|README.md|SKILLS_INDEX.md|skills|hooks|bin|tests) continue ;;
|
|
esac
|
|
echo "ERROR: unexpected entry at repo root: $base" >&2
|
|
fail=1
|
|
done
|
|
|
|
# 2. Every skill is a directory with a SKILL.md carrying name + description.
|
|
count=0
|
|
for dir in "{{.SKILLS_DIR}}"/*; do
|
|
name=$(basename "$dir")
|
|
if [ ! -d "$dir" ]; then
|
|
echo "ERROR: skills/$name is not a directory" >&2; fail=1; continue
|
|
fi
|
|
if [ ! -f "$dir/SKILL.md" ]; then
|
|
echo "ERROR: skills/$name missing SKILL.md" >&2; fail=1; continue
|
|
fi
|
|
head -n 20 "$dir/SKILL.md" | grep -q '^name:' || { echo "ERROR: skills/$name SKILL.md has no 'name:' frontmatter" >&2; fail=1; }
|
|
head -n 20 "$dir/SKILL.md" | grep -q '^description:' || { echo "ERROR: skills/$name SKILL.md has no 'description:' frontmatter" >&2; fail=1; }
|
|
count=$((count + 1))
|
|
done
|
|
|
|
# 3. SKILLS_INDEX.md's table is generated from frontmatter, so the
|
|
# assertion is that regenerating it changes nothing. This replaces
|
|
# bin/check-index: a generated table cannot drift in either
|
|
# direction, so there is no separate rule left to check.
|
|
sh '{{.TASKFILE_DIR}}/bin/generate-index' --check '{{.TASKFILE_DIR}}' || fail=1
|
|
|
|
# 4. Every skill says which surfaces it ships to, and names only
|
|
# surfaces that exist — a wrong value silently drops a skill from a
|
|
# harness instead of erroring.
|
|
sh '{{.TASKFILE_DIR}}/bin/check-surfaces' '{{.TASKFILE_DIR}}' || fail=1
|
|
|
|
# 5. A documented session_log call must be one the tool would accept:
|
|
# session_id is its only required field, and nine skills omitted it.
|
|
sh '{{.TASKFILE_DIR}}/bin/check-session-log' '{{.TASKFILE_DIR}}' || fail=1
|
|
|
|
# 6. Plugin manifest version must match the latest tag, or installers
|
|
# never see the update (Claude Code gates updates on this field).
|
|
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
|
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
|
tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
|
|
# Ordering, not equality. The fault this guards is the manifest
|
|
# failing to MOVE — a release tag whose manifest still names the old
|
|
# version, so no installed host ever sees the update. A manifest
|
|
# ahead of the newest tag this checkout knows about is the normal
|
|
# state between the tagger's bump commit and a tag fetch (#34).
|
|
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ] \
|
|
&& [ "$(sh '{{.TASKFILE_DIR}}/bin/newer-version' "$manifest_v" "$tag_v")" = "$tag_v" ]; then
|
|
echo "ERROR: plugin.json version ($manifest_v) is behind the latest tag ($tag_v) — installed hosts never see the update" >&2
|
|
fail=1
|
|
fi
|
|
else
|
|
echo "ERROR: missing .claude-plugin/plugin.json" >&2; fail=1
|
|
fi
|
|
|
|
if [ $fail -ne 0 ]; then
|
|
echo "check FAILED" >&2; exit 1
|
|
fi
|
|
echo "check OK — $count skills, manifest matches tag"
|
|
|
|
release:
|
|
desc: 'Tag HEAD per Bump-Type in the latest commit footer. Normally CI handles this.'
|
|
cmds:
|
|
- |
|
|
latest=$(git -C "{{.TASKFILE_DIR}}" describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
major=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/')
|
|
minor=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\2/')
|
|
patch=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\3/')
|
|
bump=$(git -C "{{.TASKFILE_DIR}}" log -1 --pretty=%B | grep -iE "^Bump-Type:" | head -1 | awk '{print tolower($2)}')
|
|
case "$bump" in
|
|
major) major=$((major + 1)); minor=0; patch=0 ;;
|
|
minor) minor=$((minor + 1)); patch=0 ;;
|
|
*) patch=$((patch + 1)) ;;
|
|
esac
|
|
next="v${major}.${minor}.${patch}"
|
|
echo "tagging $next (previous $latest, bump=${bump:-patch})"
|
|
git -C "{{.TASKFILE_DIR}}" tag -a "$next" -m "release $next"
|
|
echo "now: git push origin $next"
|