Found from the other end: the koala cluster's CIHeadUncovered{repo=skills}
alert had been firing for 85 hours, and it is a true positive.
mathias/skills main HEAD f031c2b
workflow runs for that sha 0
Not a one-off. The previous release-bump commit has the same property:
6cc224f chore(release): plugin.json version 0.19.4 -> 0 runs
f031c2b chore(release): plugin.json version 0.19.5 -> 0 runs
The `tag` job pushes the manifest bump itself with secrets.GITEA_TOKEN, and a
push authenticated with the Actions token does not trigger workflows -- that is
deliberate platform behaviour to stop CI recursing. The `Skip-Release: true`
trailer stops the TAGGER recursing and works; nothing stopped the whole run,
`gate` included, from never firing on that commit.
So after every release, main's HEAD is a commit no CI has seen, until an
unrelated human push moves it. CIHeadUncovered therefore cannot clear on its
own -- and an alert that can only be cleared by doing something unrelated is
worse than a noisy one, because it teaches the reader that red is the normal
state of that rule.
The commit is not cosmetic either. plugin.json's version is what Claude Code
gates plugin updates on, `task check` asserts it is not behind the latest tag,
and the sed-and-retry logic that writes it already shipped a mismatched
manifest once -- v0.10.0 tagged with the manifest still reading 0.9.0, per the
comment above that very loop.
Fix: run `task check` on the bump commit before pushing it. The commit is
produced in that step, so it can be validated in that step -- one check instead
of a second full CI run, and no recursion risk to reintroduce.
Considered and rejected: re-triggering CI with a different credential after the
push (costs a full run per release and brings back what the token behaviour
exists to prevent), and teaching CIHeadUncovered to ignore manifest-only
commits (narrowing the measure to fit the system, and it would hide a real gap
too).
The assertion in tests/ci_gate_test.sh had to be anchored with ^[^#]*: the tag
job's own prose says "`task check` asserts the manifest is not behind the
latest tag", so a bare grep matched the comment explaining the problem and
reported the problem fixed.
Closes #46.
skills
Engineering skills library — markdown SKILL.md files that plug into any
AI-coding harness.
For Claude Code this repo is a plugin, installed from the marketplace it
also hosts; that path carries the hooks and MCP servers too. For Crush, Pi
and OpenCode it is a plain library wired by install.sh as one directory
symlink per harness. See SKILLS_INDEX.md.
What is a skill?
A skill is a single directory containing one or more markdown files. The
canonical entrypoint is <name>/SKILL.md. Additional reference material
lives alongside in the same directory (e.g.
clean-code/references/code-smells.md). Harnesses load skills on
demand — keep SKILL.md short and triggerable; push depth into
references.
See SKILLS_INDEX.md for the full catalogue with
descriptions and "use when" triggers.
Install
Claude Code — the plugin
/plugin marketplace add https://git.d-ma.be/mathias/skills.git
/plugin install skills@mathias
The plugin ships skills, the secret-guard + pre-task-ritual hooks, and the
brain/gitea/infra MCP servers as one versioned unit. Skills are namespaced
(skills:tdd). Updates land when .claude-plugin/plugin.json's version
moves — CI bumps it on every release.
Crush, Pi, OpenCode — install.sh
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
Idempotent. It clones or fast-forwards the checkout, then populates one managed directory and gives each harness a single link to it:
| Path | Target |
|---|---|
~/.agents/skills/ |
a real directory, one symlink per skill (Pi reads this path directly) |
~/.agents/skills/<skill> |
$SKILLS_CHECKOUT_DIR/skills/<skill>, or another configured source |
~/.config/crush/skills |
~/.agents/skills |
~/.config/opencode/skills |
~/.agents/skills |
Total links are N + one per harness — not N x harnesses, which is what #23 was actually about.
More than one source of skills
~/.agents/skills used to be a single link straight at this checkout. That
encoded an assumption nobody had stated — that this checkout is the only source
of skills on the host — and on flamingo the only way to install this library
was to displace Anthropic's official set, which lived in Crush's directory.
Real capability loss dressed up as a tidy-up (#27).
Point SKILLS_EXTRA_SOURCES at any number of extra directories, colon
separated, each holding <skill>/SKILL.md:
SKILLS_EXTRA_SOURCES=~/.config/crush/skills.anthropic bash install.sh
- The checkout is always first and always wins a name collision — but the collision is logged, never resolved in silence. Quietly picking one is how stale content wins.
- Removing a skill from a source removes its link on the next run.
- Anything in the managed directory that is not a symlink is reported and left alone. install.sh does not delete content it did not create.
- The source list is recorded at
~/.agents/skills-sourcessobin/doctorcan tell a link into a known source from a link into a forgotten one.
Where the checkout must live
SKILLS_CHECKOUT_DIR defaults to ~/.local/share/skills, and that default is
load-bearing: it is host-local, outside any synced or shared directory.
On this fleet ~/dev is an iCloud folder shared by flamingo and iguana, and it
spans two path roots — /Users/mathias on the Macs, /home/mathias on koala.
A checkout there is a git working tree with two concurrent writers, and any
absolute symlink stored there is broken on one side by construction. That is
what produced 243 dangling links across 12 repos (#23). Do not "fix" the
default to point into ~/dev.
Per-host env-var overrides
| Env var | Default |
|---|---|
SKILLS_REPO_URL |
https://git.d-ma.be/mathias/skills.git |
SKILLS_REF |
main (set to e.g. v0.4.0 to pin a release) |
SKILLS_CHECKOUT_DIR |
$HOME/.local/share/skills |
SKILLS_SKIP_FETCH |
unset (1 wires an existing checkout without touching HEAD) |
AGENTS_SKILLS_DIR |
$HOME/.agents/skills |
CRUSH_SKILLS_DIR |
$HOME/.config/crush/skills |
OPENCODE_SKILLS_DIR |
$HOME/.config/opencode/skills |
ANTIGRAVITY_SKILLS_JSON |
$HOME/.gemini/config/skills.json (registers AGENTS_SKILLS_DIR as a path entry; needs jq) |
SKILLS_EXTRA_SOURCES |
unset (colon-separated extra source directories) |
The Mistral Vibe target was removed — unused. Antigravity's was removed for the same reason and re-added once it came back into use.
Checking a host
task doctor # or bin/doctor
Reports dangling links, links into a source nobody configured, skills in the checkout with no link, stray entries, leftover per-skill Claude symlinks, and a Claude Code plugin sitting on an older generation than the host already has. Non-zero exit on drift. It also runs at session start via the plugin, so a drifting host says so rather than waiting to be discovered.
All of that compares copies already on the machine — deliberately, since it runs in a session hook where latency shows. That leaves one case invisible: a host where the checkout, the marketplace clone and the installed plugin all agree, and all three are a release behind origin.
task doctor -- --online # or bin/doctor --online
One git ls-remote against origin, opt-in. Belongs in a scheduled per-host job,
not the session hook. An unreachable remote is reported as its own fault rather
than as staleness — "I could not check" and "you are behind" are different
answers and only one of them is about the host.
Versioning
Every commit to main is auto-tagged as a semver release by the
.gitea/workflows/release.yml action. Bump direction follows the commit
footer:
Bump-Type: major→X.0.0Bump-Type: minor→0.Y.0- anything else →
0.0.Z(patch, the default)
Consumer projects can pin a version by setting SKILLS_REF=v0.1.0
before task skills:install; default is main for always-current.
Contributing a skill
-
Add a directory
skills/<short-kebab-name>/. -
Create
skills/<short-kebab-name>/SKILL.mdwith this frontmatter — all four keys are gated bytask check, and the index table is generated from the last three:--- name: <short-kebab-name> surfaces: [claude-code, library] # any of claude-code | library | chat summary: "<one line for the index table>" use_when: "<the trigger, for the index table>" description: <the paragraph the model selects on> ---Then: one-paragraph purpose, triggers ("use when …"), mechanics.
-
Run
task indexand commit the regeneratedSKILLS_INDEX.md. Do not hand-edit the table —task checkregenerates it and fails on a diff. -
Pressure-test it before shipping: run the scenario against a subagent that does not have the skill, write the skill against what that run actually did, then re-run with it. See
superpowers-writing-skillsand #19. -
Commit directly to
main. Conventional-commit prefix (feat:/fix:/docs:). AddBump-Type: minorfooter when adding a new skill or expanding scope; default patch covers edits.
This repo is markdown-only and public — never include secrets, client names, or anything under NDA in a skill.
Mode 2 routing candidates
These notes used to sit in each SKILL.md as a ## Mode 2 Routing Note section.
They changed nothing about how the skill is invoked — several said so outright —
and cost context on every load, so they live here instead. Nothing in the repo
depends on this table; it is a record of which skills looked mechanical enough
to route to a local model, should a routing layer ever exist.
| Skill | Note |
|---|---|
cad-babysit |
Phases 2-4 (reading comments/logs, classifying fault vs outcome) are judgment-heavy and should stay Mode 1. Phase 1 (running cad stuck and grouping its output) is mechanical and a Mode 2 routing candidate once Plan 6 ships the routing pod. |
code-review |
This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes. |
debug |
This skill produces high-volume mechanical output (hypothesis enumeration) and is a candidate for Mode 2 routing to a local model in the future. Until Plan 6 ships the routing pod, treat as Mode 1 only. The hypothesis format and discipline are identical regardless of which model generates them. |
feature-spec |
Spec writing requires interpretation of intent and tradeoff reasoning — work where Claude generally outperforms a 9-13B local model. This skill is intentionally not a Mode 2 routing candidate. In Mode 2, the routing layer should leave feature-spec calls on Claude. |
session-retrospective |
Reading long session logs and pulling themes is high-volume, mechanical pattern-recognition work — a strong Mode 2 routing candidate. Until Plan 6 ships the routing pod, treat as Mode 1 only. |
tdd |
This skill is invoked identically whether the agent is running in Mode 1 (cloud Claude, no routing) or Mode 2 (client-local, supervisor routing layer). The routing pod (Plan 6) does not exist yet; until it does, treat this skill as Mode 1 only. The discipline does not change between modes — only the model behind the call. |
trainer |
Trainer is the same shape as session-retrospective — long-context reading, mechanical filtering — and is a Mode 2 routing candidate. Until Plan 6, treat as Mode 1 only. |