The repo distributes to three kinds of surface, and treats all skills as belonging to all of them:
Claude Code — plugin from the marketplace this repo hosts. Hosts: koala (Linux), flamingo (MacBook Air M3), iguana (Mac Studio M2 Ultra).
Crush / Pi / OpenCode — one directory symlink each, via install.sh.
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.
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.
#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.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
The repo distributes to three kinds of surface, and treats all skills as belonging to all of them:
install.sh.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-worktreescannot fire there. A flat mirror would install ~35 skills that can never trigger, degrading selection for the ones that should.Conversely
antons-jobbskandare-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:Allowed values:
claude-code,library,chat. Knowledge lives once, next to the skill it describes — same reasoning that makes a hand-maintainedSKILLS_INDEX.mdthe wrong shape.Action
surfaces:to all skill frontmatter. Default[claude-code, library]unless there is a reason otherwise.task checkgate: missing or unknownsurfaces:value fails, same as a missingname:/description:today.install.shlink creation onsurfacescontaininglibrary.SKILLS_INDEX.mdfrom frontmatter rather than maintaining it by hand. This subsumes #15 (missingdreamandexperiment-specrows) — a generated index cannot drift.chatset. Current claude.ai install isdream,own-the-task,grill-me,close-session,antons-jobbsk,are-sasongsscout; that set looks right as-is.Interaction with #27
#27 proposes making
~/.agents/skillsa real directory of per-skill symlinks from multiple sources. That change and this one touch the same code path ininstall.sh. #27 should land first — filtering bysurfacesis 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. Thecheckgate must validate values against the allowed set, not just presence, andbin/doctorshould report the expected count for that host's surface rather than a global count.Definition of done
task checkfails on a skill with nosurfaces:;install.shlinks onlylibraryskills;SKILLS_INDEX.mdis generated and matchesskills/exactly.Done across
90a58f0(the field, the gate, the filter) andc6787e0(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-taskandgrill-mealso carrychat, matching what is installed on claude.ai today.claude plugin validatepasses with the extra key.Gate validates values, not presence.
bin/check-surfacesfails 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-indexrebuilds the table between markers;task checkruns it with--check;task indexregenerates. Everything outside the markers — wiring docs, relationships tree, trigger table — is prose and is left alone.The
chatset. 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 —descriptionis a paragraph written for model selection, not a table cell. So the curation moved into the frontmatter assummary:anduse_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-indexand its test are deleted. A generated table cannot drift in either direction, sogenerate-index --checksubsumes 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
c6787e0shows red, and itsStructure gatestep genuinely failed — but onplugin.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 ind7775aa; run #36 covers the same tree and is green.