Declare a surfaces: field per skill; gate in check, filter install.sh, generate SKILLS_INDEX.md #29

Closed
opened 2026-08-26 17:32:48 +00:00 by mathias · 1 comment
Owner

Problem

The repo distributes to three kinds of surface, and treats all skills as belonging to all of them:

  1. Claude Code — plugin from the marketplace this repo hosts. Hosts: koala (Linux), flamingo (MacBook Air M3), iguana (Mac Studio M2 Ultra).
  2. Crush / Pi / OpenCode — one directory symlink each, via install.sh.
  3. claude.ai — manual upload, no fetch path.

Surface 3 is not a fourth host to mirror. It has no repo checkout, no koala, no browser, no shell into the fleet. gitea-ci, playwright, web-shot, superpowers-using-git-worktrees cannot fire there. A flat mirror would install ~35 skills that can never trigger, degrading selection for the ones that should.

Conversely antons-jobbsk and are-sasongsscout (see #28) belong on claude.ai and nowhere else.

Nothing in the repo currently expresses this, so every distribution decision is made in the installer or by hand.

Proposal

Add a frontmatter field to every SKILL.md:

---
name: tdd
description: ...
surfaces: [claude-code, library]
---

Allowed values: claude-code, library, chat. Knowledge lives once, next to the skill it describes — same reasoning that makes a hand-maintained SKILLS_INDEX.md the wrong shape.

Action

  • Add surfaces: to all skill frontmatter. Default [claude-code, library] unless there is a reason otherwise.
  • Extend the task check gate: missing or unknown surfaces: value fails, same as a missing name:/description: today.
  • Filter install.sh link creation on surfaces containing library.
  • Generate SKILLS_INDEX.md from frontmatter rather than maintaining it by hand. This subsumes #15 (missing dream and experiment-spec rows) — a generated index cannot drift.
  • Decide the chat set. Current claude.ai install is dream, own-the-task, grill-me, close-session, antons-jobbsk, are-sasongsscout; that set looks right as-is.

Interaction with #27

#27 proposes making ~/.agents/skills a real directory of per-skill symlinks from multiple sources. That change and this one touch the same code path in install.sh. #27 should land first — filtering by surfaces is a small addition to a per-skill loop and an awkward one to bolt onto the current single directory link.

Risk

Low, but fleet-wide. Getting a surfaces: value wrong silently removes a skill from a harness rather than erroring. The check gate must validate values against the allowed set, not just presence, and bin/doctor should report the expected count for that host's surface rather than a global count.

Definition of done

task check fails on a skill with no surfaces:; install.sh links only library skills; SKILLS_INDEX.md is generated and matches skills/ exactly.

## Problem The repo distributes to three kinds of surface, and treats all skills as belonging to all of them: 1. **Claude Code** — plugin from the marketplace this repo hosts. Hosts: koala (Linux), flamingo (MacBook Air M3), iguana (Mac Studio M2 Ultra). 2. **Crush / Pi / OpenCode** — one directory symlink each, via `install.sh`. 3. **claude.ai** — manual upload, no fetch path. Surface 3 is not a fourth host to mirror. It has no repo checkout, no koala, no browser, no shell into the fleet. `gitea-ci`, `playwright`, `web-shot`, `superpowers-using-git-worktrees` cannot fire there. A flat mirror would install ~35 skills that can never trigger, degrading selection for the ones that should. Conversely `antons-jobbsk` and `are-sasongsscout` (see #28) belong on claude.ai and nowhere else. Nothing in the repo currently expresses this, so every distribution decision is made in the installer or by hand. ## Proposal Add a frontmatter field to every `SKILL.md`: ```yaml --- name: tdd description: ... surfaces: [claude-code, library] --- ``` Allowed values: `claude-code`, `library`, `chat`. Knowledge lives once, next to the skill it describes — same reasoning that makes a hand-maintained `SKILLS_INDEX.md` the wrong shape. ## Action - [ ] Add `surfaces:` to all skill frontmatter. Default `[claude-code, library]` unless there is a reason otherwise. - [ ] Extend the `task check` gate: missing or unknown `surfaces:` value fails, same as a missing `name:`/`description:` today. - [ ] Filter `install.sh` link creation on `surfaces` containing `library`. - [ ] Generate `SKILLS_INDEX.md` from frontmatter rather than maintaining it by hand. This subsumes #15 (missing `dream` and `experiment-spec` rows) — a generated index cannot drift. - [ ] Decide the `chat` set. Current claude.ai install is `dream`, `own-the-task`, `grill-me`, `close-session`, `antons-jobbsk`, `are-sasongsscout`; that set looks right as-is. ## Interaction with #27 #27 proposes making `~/.agents/skills` a real directory of per-skill symlinks from multiple sources. That change and this one touch the same code path in `install.sh`. #27 should land first — filtering by `surfaces` is a small addition to a per-skill loop and an awkward one to bolt onto the current single directory link. ## Risk Low, but fleet-wide. Getting a `surfaces:` value wrong silently removes a skill from a harness rather than erroring. The `check` gate must validate values against the allowed set, not just presence, and `bin/doctor` should report the expected count for that host's surface rather than a global count. ## Definition of done `task check` fails on a skill with no `surfaces:`; `install.sh` links only `library` skills; `SKILLS_INDEX.md` is generated and matches `skills/` exactly.
mathias added the risk/mediumsize/mtype/feat labels 2026-08-27 05:25:45 +00:00
Author
Owner

Done across 90a58f0 (the field, the gate, the filter) and c6787e0 (the generated index). Green on run #36, which is the first run whose gate covers all of it — see the note at the bottom about run #35.

All five clauses:

surfaces: on every skill. Default [claude-code, library]; dream, own-the-task and grill-me also carry chat, matching what is installed on claude.ai today. claude plugin validate passes with the extra key.

Gate validates values, not presence. bin/check-surfaces fails on an unknown surface, on an empty list, and on a missing field, because — as this issue's Risk section says — a wrong value silently removes a skill from a harness rather than erroring. Six cases.

install.sh filters on library. A source that declares nothing at all is treated as foreign and linked as-is; otherwise adding Anthropic's set (#27) would link nothing. bin/doctor's missing-link check skips non-library skills for the same reason — a chat-only skill absent from a Crush directory is correct, not drift.

The index is generated. bin/generate-index rebuilds the table between markers; task check runs it with --check; task index regenerates. Everything outside the markers — wiring docs, relationships tree, trigger table — is prose and is left alone.

The chat set. Left exactly as this issue proposed. The three claude.ai-only skills stay out until #28 gets their contents; that issue is blocked on an export from the claude.ai UI, which no host here can do.

One deviation, decided rather than assumed

Generating from description: would have replaced 42 curated rows with truncated prose — description is a paragraph written for model selection, not a table cell. So the curation moved into the frontmatter as summary: and use_when:, carried over verbatim, and the table is generated from those. That is this issue's own argument ("knowledge lives once, next to the skill it describes") applied to the columns as well as the field. Two more invented keys was the price; Mathias chose that over losing the "use when" column.

Rows are alphabetical now — the old thematic ordering could not survive generation, and the conceptual grouping lives in the relationships tree, which is a better home for it. A fourth column shows each skill's surfaces, so the new field is visible and not merely enforced.

bin/check-index and its test are deleted. A generated table cannot drift in either direction, so generate-index --check subsumes both of its assertions — the #15 gate lived for about two hours and was superseded by something stronger.

Note on run #35

The run for c6787e0 shows red, and its Structure gate step genuinely failed — but on plugin.json version (0.9.0) is behind the latest tag (0.10.0), a broken-tagger state inherited from three earlier queued runs, not on anything in this change. Fixed in d7775aa; run #36 covers the same tree and is green.

Done across `90a58f0` (the field, the gate, the filter) and `c6787e0` (the generated index). Green on run [#36](https://git.d-ma.be/mathias/skills/actions/runs/4853), which is the first run whose gate covers all of it — see the note at the bottom about run #35. All five clauses: **`surfaces:` on every skill.** Default `[claude-code, library]`; `dream`, `own-the-task` and `grill-me` also carry `chat`, matching what is installed on claude.ai today. `claude plugin validate` passes with the extra key. **Gate validates values, not presence.** `bin/check-surfaces` fails on an unknown surface, on an empty list, and on a missing field, because — as this issue's Risk section says — a wrong value silently removes a skill from a harness rather than erroring. Six cases. **install.sh filters on `library`.** A source that declares nothing at all is treated as foreign and linked as-is; otherwise adding Anthropic's set (#27) would link nothing. `bin/doctor`'s missing-link check skips non-library skills for the same reason — a chat-only skill absent from a Crush directory is correct, not drift. **The index is generated.** `bin/generate-index` rebuilds the table between markers; `task check` runs it with `--check`; `task index` regenerates. Everything outside the markers — wiring docs, relationships tree, trigger table — is prose and is left alone. **The `chat` set.** Left exactly as this issue proposed. The three claude.ai-only skills stay out until #28 gets their contents; that issue is blocked on an export from the claude.ai UI, which no host here can do. ## One deviation, decided rather than assumed Generating from `description:` would have replaced 42 curated rows with truncated prose — `description` is a paragraph written for model selection, not a table cell. So the curation moved *into* the frontmatter as `summary:` and `use_when:`, carried over verbatim, and the table is generated from those. That is this issue's own argument ("knowledge lives once, next to the skill it describes") applied to the columns as well as the field. Two more invented keys was the price; Mathias chose that over losing the "use when" column. Rows are alphabetical now — the old thematic ordering could not survive generation, and the conceptual grouping lives in the relationships tree, which is a better home for it. A fourth column shows each skill's surfaces, so the new field is visible and not merely enforced. `bin/check-index` and its test are deleted. A generated table cannot drift in either direction, so `generate-index --check` subsumes both of its assertions — the #15 gate lived for about two hours and was superseded by something stronger. ## Note on run #35 The run for `c6787e0` shows red, and its `Structure gate` step genuinely failed — but on `plugin.json version (0.9.0) is behind the latest tag (0.10.0)`, a broken-tagger state inherited from three earlier queued runs, not on anything in this change. Fixed in `d7775aa`; run #36 covers the same tree and is green.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mathias/skills#29