Compare commits

...
18 Commits
Author SHA1 Message Date
mathias d7775aac57 fix(ci): tag and bump separately — a queued run pushed a stale main
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Three runs failed with `! [rejected] HEAD -> main (non-fast-forward)`. The
runner is maxParallel=1 and shared with mathias/infra, so those jobs started
five minutes and three commits after their own push. The tagger checked out
its commit and pushed that stale HEAD to main, which main had long left behind.

My own change made it worse: pairing the manifest bump and the tag in a single
`git push` turned a partial failure into a total one, so no tag was created
either. Except once — v0.10.0 landed through the no-bump-needed branch while
the manifest stayed at 0.9.0, which is precisely the "installed hosts never
see the release" state, and which #34's ordering check then caught on main.

The tag now goes up first and alone: it names the commit this run checked out
and cannot be rejected however far main has moved. The bump is a separate
concern that must land on whatever main is NOW, so it re-fetches, rebuilds the
commit on origin/main and retries up to three times, and steps aside entirely
if a later run has already bumped past it.

plugin.json is set to 0.10.0 in this commit so the gate can pass at all — main
was red on manifest-behind-tag, which would have blocked the tag job that fixes
the tagger.

Refs #22
Bump-Type: patch
2026-08-29 11:25:20 +02:00
mathias c6787e04d7 feat(index): generate the skills table from frontmatter
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
Second half of #29. The index was hand-maintained and had already drifted
once: dream and experiment-spec shipped with no row, their rows lost to a
squash-merge after a directory reorg. bin/check-index caught that after the
fact; generating removes the class.

The curated summary and trigger move into each SKILL.md as `summary:` and
`use_when:`, carried over verbatim from the existing table rather than derived
from `description:` — description is a paragraph written for model selection,
and truncating it would have produced a worse table than the one it replaced.
Knowledge now lives once, next to the skill it describes, which is the argument
that made a hand-maintained index the wrong shape to begin with.

The table is written between markers, so the wiring docs, the relationships
tree and the trigger table below it are untouched — those are prose, not data.
Rows are alphabetical now; the conceptual grouping the old order carried lives
in the relationships tree, which is the better place for it. A fourth column
shows each skill's surfaces, which is what makes #29's field visible rather
than 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.
Verified by mutation — deleting the tdd row turns `task check` red with
"SKILLS_INDEX.md is out of date".

Closes #29
Bump-Type: minor
2026-08-29 11:23:14 +02:00
mathias 90a58f0b9e feat(skills): declare which surfaces each skill ships to
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
The repo distributes three ways — the Claude Code plugin, the symlinked
library for Crush/Pi/OpenCode, and claude.ai, which has no fetch path at all —
and treated every skill as belonging to all of them. claude.ai is not a fourth
host to mirror: gitea-ci, playwright and web-shot have no shell and no cluster
there, so a flat mirror installs skills that can never fire and degrades
selection for the ones that can.

Every SKILL.md now carries `surfaces:`, defaulting to [claude-code, library];
dream, own-the-task and grill-me add `chat`, matching what is actually
installed on claude.ai today. Claude Code accepts the extra key —
`claude plugin validate` passes and the skills still load.

bin/check-surfaces validates values, not just presence, because the failure
mode here is silent: a wrong surface removes a skill from a harness rather than
erroring. install.sh links only skills whose surfaces include `library`, and a
source that declares nothing at all is treated as foreign and linked as-is —
Anthropic's official set has no reason to know this convention. doctor's
missing-link check skips non-library skills for the same reason.

Refs #29
Bump-Type: minor
2026-08-29 10:09:52 +02:00
mathias 15737321ac fix(check): fail when the manifest is behind the tag, not merely different
CI / Check / Test (push) Failing after 0s
CI / Tag release (push) Skipped
`task check` asserted plugin.json's version EQUALS the latest tag. `git
describe` is local-only, so any checkout that had not fetched the tag CI just
pushed went red on a commit that was correct — three times in one backlog
session, on the loop push → CI tags and pushes the bump → pull → red until you
also fetch tags.

The fault the assertion exists for is the manifest failing to move: a release
whose manifest still names the old version, so no installed host ever sees the
update. That is manifest < tag. Ahead is normal and is now quiet.

Ordering needed a real comparison, and bin/doctor had already grown one for
#30, so it moves to bin/newer-version with its own tests rather than existing
twice. Seven cases including the lexical trap (0.8.10 beats 0.8.9) and a
missing component counting as zero.

Verified by mutation: manifest 0.1.0 against tag v0.9.0 exits 201 naming both
versions; manifest 9.9.9 exits 0; the real tree exits 0.

Closes #34
Bump-Type: patch
2026-08-29 10:07:27 +02:00
mathias 025f01aa80 feat(install): serve several sources instead of displacing them
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Failing after 1s
~/.agents/skills was 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. On flamingo it was not: Crush's directory held Anthropic's
official set, and the only way to install this library was to move that set
aside. A capability loss dressed up as a tidy-up.

The canonical location becomes a real directory of per-skill symlinks, fed by
the checkout plus anything in SKILLS_EXTRA_SOURCES. Harnesses keep their one
directory link, so the count is N + one per harness rather than N x harnesses,
which is what #23 was actually about.

The three requirements #27 named, each with a test:
- collisions are logged with both paths and the checkout wins, never resolved
  in silence;
- removing a skill from a source prunes its link;
- anything in the managed directory that is not our symlink is reported and
  left in place — the installer does not delete content it did not create.

doctor learns the same model: sources are recorded at ~/.agents/skills-sources,
a link into a configured source is healthy, a link into an unknown one is not,
and the old count check becomes a per-skill check because a per-skill layout
can lose one skill without changing any total. The pre-#27 wiring is named as
legacy rather than reported as damage. count_skills needed find -L; without it
symlinked skill directories count as zero.

Verified live on koala: migrated in place to 42 per-skill links, then a second
source added a 43rd alongside them, then a re-run without that source pruned it
back to 42. Only doctor finding throughout was the genuine stale plugin.

Closes #27
Bump-Type: minor
2026-08-29 10:05:46 +02:00
mathias eee62e4fc0 chore(release): plugin.json version 0.9.0
Skip-Release: true
2026-08-29 08:01:01 +00:00
mathias a2afb35d1d feat(doctor): report the installed plugin generation, not just its presence
CI / Tag release (push) Successful in 0s
CI / Check / Test (push) Successful in 1s
Every other check in doctor is about symlink paths. That is the wrong shape
for Claude Code, which has no symlinks — it has the plugin, and it gates
updates on plugin.json's version. A host that never picked up a release loads
every skill successfully and is simply a generation behind, silently.

Found live while writing this: koala had skills@mathias 0.6.0 installed, a
marketplace clone at 0.7.0 and a checkout at 0.8.4, and doctor said "clean".

doctor now compares the version recorded in installed_plugins.json against the
newest manifest already on the host — the active checkout and any marketplace
clone, filtered to plugin.json files that actually name this plugin — and fails
naming both versions. Offline, like the rest of doctor: "available" never means
a fetch.

A host with no install record is a valid symlink-only configuration and stays
clean. --json grows a "plugin":{"installed","available"} object so per-host
results can be collected without parsing prose.

Four new fixture cases: match, stale, absent record, newer marketplace clone.
Live output on koala:

  FAIL plugin-version: Claude Code has skills@mathias 0.6.0 installed but
  0.8.4 is available on this host

Closes #30
Bump-Type: minor
2026-08-29 10:00:11 +02:00
mathias ad17a7c6c0 chore(release): plugin.json version 0.8.5
Skip-Release: true
2026-08-29 07:56:02 +00:00
mathias 21dbb35526 chore(skills): delete superpowers-test-driven-development
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 0s
Verified duplicate rather than assumed one. Section for section it is the same
skill as `tdd` — Overview, When to Use, Iron Law, Red-Green-Refactor with the
same dot graph, Good Tests, Common Rationalizations, Red Flags, Verification
Checklist, When Stuck, Final Rule — with TypeScript examples and obra's "your
human partner" phrasing instead of ours. `tdd` carries all of it plus the
Go-specific material (table-driven tests, t.Run, testify, go test -race), the
brain MCP integration and the Mode 2 routing note.

Its one non-SKILL.md file, testing-anti-patterns.md, is byte-identical to
skills/tdd/references/testing-anti-patterns.md, which `tdd` already points at.
Nothing was salvageable because nothing was different.

No references anywhere outside its own directory except the index row, removed
here. `atdd` is untouched — acceptance-level is a genuinely different altitude
and it already cross-refs `tdd` correctly.

Incidentally the first live catch by bin/check-index (#15): deleting the
directory turned `task check` red with "SKILLS_INDEX.md lists
'superpowers-test-driven-development', which has no ...SKILL.md" before I got
to the row.

42 skills now. Closes #13
Bump-Type: patch
2026-08-29 09:55:53 +02:00
mathias 8ad2079f86 chore(release): plugin.json version 0.8.4
Skip-Release: true
2026-08-29 07:54:53 +00:00
mathias b188965161 docs(index): place experiment-spec in the relationships diagram
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
Missed in 83b069a. #15 asked for three things and the table row was only two
of them: experiment-spec also belongs in the Skill Relationships tree, parallel
to feature-spec and downstream of spec-driven-dev, since it is the research/ML
substitute for feature-spec rather than a separate lifecycle.

dream needs no edge, same as trainer and session-retrospective — the issue said
so and that part was right.

Refs #15
Bump-Type: patch
2026-08-29 09:54:23 +02:00
mathias ae66840a03 chore(release): plugin.json version 0.8.3
Skip-Release: true
2026-08-29 07:53:48 +00:00
mathias e7e514390b docs(web-shot): resolve shot.sh from the install, not a checkout path
CI / Check / Test (push) Successful in 1s
CI / Tag release (push) Successful in 1s
The usage block pointed at ~/dev/.skills/web-shot/shot.sh four times. That
directory has not existed since the skills moved into this repo, and the
failure mode is worse than a broken link: web-shot exists for agents with no
local browser, so a 404 sends them to hand-roll the Job instead of looking
harder. One caller burned three attempts on playwright module resolution
before finding the script that documents that exact gotcha.

Replaced with a resolution line covering both delivery paths — the Claude Code
plugin and the ~/.agents/skills symlink — so no absolute checkout path is left
to rot. Verified on koala: resolves to the .agents link, and degrades to an
empty string plus a clear message where the skill is not installed.

The brain entry headless-browser-via-k3s-playwright-job carried the same stale
path in three places and referenced the retired wire-skills.sh wiring; updated
to match.

Closes #26
Bump-Type: patch
2026-08-29 09:53:33 +02:00
mathias 087685f2e3 chore(release): plugin.json version 0.8.2
Skip-Release: true
2026-08-29 07:52:16 +00:00
mathias 83b069afba docs(index): add dream + experiment-spec rows, and gate the index
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Successful in 1s
Both skills shipped without an index row. The rows existed — on the feature
branches deleted in #22 — and were dropped by the squash-merge that followed
a directory reorg: the SKILL.md landed under skills/, the row did not. Nothing
errored, the index just under-reported the library to every harness reading it.

Adding two rows by hand fixes today and not tomorrow, so the assertion goes in
`task check` via bin/check-index, following the same pattern as
bin/should-skip-release: a real script with a real test, not untested logic
buried in a Taskfile. It checks both directions — a skill with no row, and a
row naming a skill that no longer exists.

Verified by mutation: deleting the `dream` row turns `task check` red and the
report names it.

Closes #15
Bump-Type: patch
2026-08-29 09:52:03 +02:00
mathias a699ad8cbd chore(release): plugin.json version 0.8.1
Skip-Release: true
2026-08-29 07:50:52 +00:00
mathias a42ec18f40 fix(secret-guard): treat env as a dump only when no command follows
CI / Tag release (push) Successful in 1s
CI / Check / Test (push) Successful in 1s
`env -u VAR cmd` and `env FOO=bar cmd` exec a command with a modified
environment and print nothing themselves. The old rule matched any `env`
token followed anywhere later by a pipe into a filter, so it blocked the
documented 1Password write path and forced a `(unset VAR; cmd)` workaround.
False positives only — never a leak — but a guard people route around is a
guard on its way out.

The rule now denies only when everything after `env` is exhausted by options
and VAR=value assignments. That is strictly the dumping shape, and it also
closes a hole the two previous rules left open: `env FOO=bar` with no command
word prints the whole environment and was allowed.

Three new deny/allow pairs pin it, including the 1Password write shape that
motivated the report. 20 cases now, up from 14, all passing.

Closes #25
Bump-Type: patch
2026-08-29 09:50:08 +02:00
mathias 46ff1acdfe chore(release): plugin.json version 0.8.0
Skip-Release: true
2026-08-29 07:47:07 +00:00
61 changed files with 1272 additions and 819 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "skills", "name": "skills",
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.", "description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
"version": "0.7.0", "version": "0.10.0",
"author": { "author": {
"name": "Mathias", "name": "Mathias",
"url": "https://git.d-ma.be/mathias" "url": "https://git.d-ma.be/mathias"
+39 -14
View File
@@ -94,26 +94,51 @@ jobs:
git config user.name "${GITEA_ACTOR}" git config user.name "${GITEA_ACTOR}"
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be" git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
# Claude Code gates plugin updates on plugin.json's version field: # The tag goes up FIRST and on its own. It names the commit this run
# if it does not move, installed hosts never see the new tag. Bump # checked out and can never be rejected, however far main has moved.
# it in the same commit train so the tag and the manifest agree — git tag -a "$TAG" -m "release $TAG"
# `task check` enforces that equality on every run. git push origin "$TAG"
echo "pushed $TAG"
# The manifest bump is a separate concern and must land on whatever
# main is NOW. The runner is maxParallel=1 and shared with other
# repos, so a job routinely starts several commits after its own —
# pushing this run's stale HEAD to main is then rejected
# non-fast-forward, which is how v0.10.0 ended up tagged with the
# manifest still reading 0.9.0.
#
# Claude Code gates plugin updates on this field: if it does not
# move, installed hosts never see the release. `task check` asserts
# the manifest is not behind the latest tag, so a failure here is a
# red main, not a cosmetic miss.
manifest=.claude-plugin/plugin.json manifest=.claude-plugin/plugin.json
version="${TAG#v}" version="${TAG#v}"
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
git tag -a "$TAG" -m "release $TAG"
if [ "$current" != "$version" ]; then for attempt in 1 2 3; do
git fetch --quiet origin main
git checkout --quiet -B release-bump origin/main
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
if [ "$current" = "$version" ]; then
echo "manifest already at $version"
exit 0
fi
# A later run may have bumped past us while this one waited.
if [ "$(sh bin/newer-version "$current" "$version")" = "$current" ]; then
echo "manifest is $current, already newer than $version — leaving it"
exit 0
fi
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest" sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
git add "$manifest" git add "$manifest"
# The trailer below stops this push from re-triggering the tagger. # The trailer below stops this push from re-triggering the tagger.
git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true" git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true"
# Commit and tag go up in ONE push: the gate job runs `task check` if git push origin HEAD:main; then
# on the bump commit, and that asserts manifest == latest tag. Two
# separate pushes leave a window where it legitimately fails.
git push origin HEAD:main "$TAG"
echo "bumped $manifest to $version" echo "bumped $manifest to $version"
else exit 0
git push origin "$TAG"
fi fi
echo "pushed $TAG" echo "main moved while bumping — retry $attempt"
done
echo "could not land the manifest bump after 3 attempts" >&2
exit 1
+32 -5
View File
@@ -40,16 +40,42 @@ moves — CI bumps it on every release.
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
``` ```
Idempotent. It clones or fast-forwards the checkout, then creates **three** Idempotent. It clones or fast-forwards the checkout, then populates one
symlinks — one directory link per harness, never one per skill: **managed directory** and gives each harness a single link to it:
| Link | Target | | Path | Target |
|---|---| |---|---|
| `~/.agents/skills` | `$SKILLS_CHECKOUT_DIR/skills` (Pi reads this path directly) | | `~/.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/crush/skills` | `~/.agents/skills` |
| `~/.config/opencode/skills` | `~/.agents/skills` | | `~/.config/opencode/skills` | `~/.agents/skills` |
Adding a skill wires it everywhere with no re-run. 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-sources` so `bin/doctor`
can tell a link into a known source from a link into a forgotten one.
### Where the checkout must live ### Where the checkout must live
@@ -74,6 +100,7 @@ default to point into `~/dev`.
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` | | `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` | | `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
| `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` | | `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` |
| `SKILLS_EXTRA_SOURCES` | unset (colon-separated extra source directories) |
Antigravity and Mistral Vibe targets were removed — unused. Antigravity and Mistral Vibe targets were removed — unused.
+73 -59
View File
@@ -2,49 +2,56 @@
This index lists all available engineering skills. Load the full SKILL.md on demand. This index lists all available engineering skills. Load the full SKILL.md on demand.
| Skill | Description | Use when | The table is **generated** from each skill's frontmatter (`summary`, `use_when`,
|-------|-------------|----------| `surfaces`) by `task index`, and `task check` fails if it is out of date. Edit
| `tdd` | Test-Driven Development enforcement | Starting any new feature or fixing a bug | the SKILL.md, not the table.
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code |
| `solid` | SOLID principles with Go examples | Design review, architecture decisions | <!-- BEGIN skills-table -->
| `code-review` | Code smell detection + Go-specific checklist | Pre-merge review | | Skill | Description | Use when | Surfaces |
| `refactoring` | Systematic refactoring methodology (Fowler) | When improving existing code | |-------|-------------|----------|----------|
| `test-design` | Dave Farley test design scoring (Farley Index) | Reviewing or writing tests | | `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria | claude-code, library |
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring | | `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present | claude-code, library |
| `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task | | `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code | claude-code, library |
| `user-stories` | Elephant Carpaccio story slicing | Breaking down features | | `code-review` | Code smell detection + Go-specific checklist | Pre-merge review | claude-code, library |
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria | | `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring | claude-code, library |
| `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements | | `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious | claude-code, library |
| `planning` | Planning and task breakdown | Sprint planning, task decomposition | | `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | claude-code, library |
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | | `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index | claude-code, library, chat |
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious | | `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature | claude-code, library |
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | | `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | | `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | claude-code, library |
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost | | `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | claude-code, library, chat |
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation | | `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; \"take this off my plate\"; before a plan exists | claude-code, library, chat |
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | | `planning` | Planning and task breakdown | Sprint planning, task decomposition | claude-code, library |
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists | | `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | claude-code, library |
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry | | `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task | claude-code, library |
| `secret-hygiene` | Stop credentials rendering into the session transcript; safe rotation chain when one leaks | Before any kubectl/SOPS/token command; after a confirmed leak | | `refactoring` | Systematic refactoring methodology (Fowler) | When improving existing code | claude-code, library |
| `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data | | `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data | claude-code, library |
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | | `secret-hygiene` | Stop credentials rendering into the session transcript; safe rotation chain when one leaks | Before any kubectl/SOPS/token command; after a confirmed leak | claude-code, library |
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review | | `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost | claude-code, library |
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists | | `solid` | SOLID principles with Go examples | Design review, architecture decisions | claude-code, library |
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present | | `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements | claude-code, library |
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas | | `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review | claude-code, library |
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding | | `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas | claude-code, library |
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion | | `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents | claude-code, library |
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents | | `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion | claude-code, library |
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents | | `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR | claude-code, library |
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically | | `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments | claude-code, library |
| `superpowers-test-driven-development` | TDD discipline (superpowers variant) | Red → green → refactor on a change | | `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review | claude-code, library |
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete | | `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents | claude-code, library |
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review | | `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically | claude-code, library |
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments | | `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree | claude-code, library |
| `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR | | `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills | claude-code, library |
| `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree | | `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete | claude-code, library |
| `superpowers-writing-skills` | Authoring new SKILL.md skills | Creating a new reusable skill | | `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding | claude-code, library |
| `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills | | `superpowers-writing-skills` | Authoring new SKILL.md skills | Creating a new reusable skill | claude-code, library |
| `tdd` | Test-Driven Development enforcement | Starting any new feature or fixing a bug | claude-code, library |
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry | claude-code, library |
| `test-design` | Dave Farley test design scoring (Farley Index) | Reviewing or writing tests | claude-code, library |
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation | claude-code, library |
| `user-stories` | Elephant Carpaccio story slicing | Breaking down features | claude-code, library |
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists | claude-code, library |
<!-- END skills-table -->
## Wiring into tools ## Wiring into tools
@@ -69,17 +76,21 @@ everything load twice.
### Crush, Pi, OpenCode — run `install.sh` ### Crush, Pi, OpenCode — run `install.sh`
These have no plugin system, so they get symlinks — but **one directory link These have no plugin system, so they get symlinks — but through one managed
each**, never one per skill: directory, never one link per skill per harness:
``` ```
~/.agents/skills -> $SKILLS_CHECKOUT_DIR/skills (Pi reads this directly) ~/.agents/skills/ (real directory; Pi reads it directly)
~/.agents/skills/tdd -> $SKILLS_CHECKOUT_DIR/skills/tdd
~/.agents/skills/xlsx -> $SKILLS_EXTRA_SOURCES entry
~/.config/crush/skills -> ~/.agents/skills ~/.config/crush/skills -> ~/.agents/skills
~/.config/opencode/skills -> ~/.agents/skills ~/.config/opencode/skills -> ~/.agents/skills
``` ```
Adding a skill therefore wires it everywhere with no re-run, and a broken link Total links are N + one per harness. `SKILLS_EXTRA_SOURCES` (colon-separated
is one broken link instead of 42. directories) lets a second library — Anthropic's official set, for instance —
live alongside this one instead of being displaced by it; name collisions are
logged rather than silently resolved, and the checkout wins (#27).
``` ```
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
@@ -93,10 +104,11 @@ absolute link stored there is broken on one side by construction. See #23.
### Check a host ### Check a host
`task doctor` (or `bin/doctor`) reports dangling links, links resolving into a `task doctor` (or `bin/doctor`) reports dangling links, links into a source
foreign checkout, count mismatches, stray entries, and leftover Claude nobody configured, skills in the checkout with no link, stray entries, leftover
symlinks. It also runs automatically at session start via the plugin, so drift Claude symlinks, and a Claude Code plugin sitting on an older generation than
announces itself instead of waiting to be found. the host already has. It also runs automatically at session start via the
plugin, so drift announces itself instead of waiting to be found.
## Skill Relationships ## Skill Relationships
@@ -107,14 +119,16 @@ problem-analysis
│ │
├── spec-driven-dev (PM-kickoff lifecycle) ├── spec-driven-dev (PM-kickoff lifecycle)
│ │ │ │
│ └── feature-spec (per-feature, mid-implementation) │ ├── feature-spec (per-feature, mid-implementation)
│ │ │
│ │ └── planning
│ │ │
│ │ └── atdd ──→ tdd (foundation)
│ │ ├──→ debug (when tests fail)
│ │ ├──→ clean-code (REFACTOR phase)
│ │ └──→ test-design (review)
│ │ │ │
│ └── planning │ └── experiment-spec (research/ML phase, instead of feature-spec)
│ │
│ └── atdd ──→ tdd (foundation)
│ ├──→ debug (when tests fail)
│ ├──→ clean-code (REFACTOR phase)
│ └──→ test-design (review)
│ │
└── planning └── planning
+26 -4
View File
@@ -61,8 +61,13 @@ tasks:
done done
exit $fail exit $fail
index:
desc: 'Regenerate SKILLS_INDEX.md''s table from skill frontmatter.'
cmds:
- "'{{.TASKFILE_DIR}}/bin/generate-index' '{{.TASKFILE_DIR}}'"
check: check:
desc: 'Structure gate — layout, frontmatter, manifest/tag parity. Runs in CI.' desc: 'Structure gate — layout, frontmatter, surfaces, generated index, manifest/tag parity. Runs in CI.'
cmds: cmds:
- | - |
fail=0 fail=0
@@ -94,13 +99,30 @@ tasks:
count=$((count + 1)) count=$((count + 1))
done done
# 3. Plugin manifest version must match the latest tag, or installers # 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. Plugin manifest version must match the latest tag, or installers
# never see the update (Claude Code gates updates on this field). # never see the update (Claude Code gates updates on this field).
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1) 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 '') tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ]; then # Ordering, not equality. The fault this guards is the manifest
echo "ERROR: plugin.json version ($manifest_v) != latest tag ($tag_v)" >&2 # 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 fail=1
fi fi
else else
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
# check-surfaces — every skill declares which surfaces it ships to, and only
# names surfaces that exist.
#
# The repo distributes three ways: the Claude Code plugin, the symlinked
# library (Crush/Pi/OpenCode), and claude.ai, which has no fetch path at all.
# Treating every skill as belonging to all three installs skills that can never
# fire — gitea-ci, playwright and web-shot have no shell or cluster in a chat
# surface — and degrades selection for the ones that can (#29).
#
# Values are validated, not merely counted: a wrong surface silently removes a
# skill from a harness rather than erroring, which is the failure mode this
# whole file exists to prevent.
#
# Usage: check-surfaces [checkout-root]
set -u
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
ALLOWED='claude-code library chat'
fail=0
for dir in "$ROOT"/skills/*/; do
[ -f "$dir/SKILL.md" ] || continue
name=$(basename "$dir")
line=$(sed -n 's/^surfaces:[[:space:]]*//p' "$dir/SKILL.md" | head -1)
if [ -z "$line" ]; then
echo "ERROR: skills/$name declares no 'surfaces:' — say where it ships (${ALLOWED% *}, ${ALLOWED##* })" >&2
fail=1
continue
fi
values=$(printf '%s' "$line" | tr -d '[]' | tr ',' ' ')
count=0
for v in $values; do
count=$((count + 1))
case " $ALLOWED " in
*" $v "*) ;;
*) echo "ERROR: skills/$name names surface '$v', which does not exist — allowed: $ALLOWED" >&2; fail=1 ;;
esac
done
[ "$count" -gt 0 ] || {
echo "ERROR: skills/$name has an empty 'surfaces:' list — a skill that ships nowhere should be deleted, not declared" >&2
fail=1
}
done
exit $fail
+134 -26
View File
@@ -39,12 +39,21 @@ SELF_CHECKOUT=$(CDPATH= cd -- "$SELF_DIR/.." && pwd)
# copy doctor happens to be running from. When invoked as a plugin hook, $0 # copy doctor happens to be running from. When invoked as a plugin hook, $0
# lives in the plugin cache, which is legitimately a second copy: judging the # lives in the plugin cache, which is legitimately a second copy: judging the
# harness links against it would flag every one of them as "outside". # harness links against it would flag every one of them as "outside".
AGENTS_DIR="$HOME/.agents/skills"
SOURCES_FILE="$(dirname -- "$AGENTS_DIR")/$(basename -- "$AGENTS_DIR")-sources"
if [ -n "${DOCTOR_CHECKOUT:-}" ]; then if [ -n "${DOCTOR_CHECKOUT:-}" ]; then
CHECKOUT="$DOCTOR_CHECKOUT" CHECKOUT="$DOCTOR_CHECKOUT"
elif [ -d "$HOME/.agents/skills" ]; then elif [ -f "$SOURCES_FILE" ]; then
# Resolve the link FIRST, then take the parent. "cd X/.." resolves ".." # Since #27 the canonical location is a real directory fed by several
# lexically against the link path, which lands back in ~/.agents. # sources, so it can no longer be resolved to a checkout. install.sh records
_resolved=$(CDPATH= cd -- "$HOME/.agents/skills" 2>/dev/null && pwd -P) # the list; the first line is always this repo's skills/ directory.
CHECKOUT=$(dirname -- "$(head -n 1 "$SOURCES_FILE")")
elif [ -L "$AGENTS_DIR" ]; then
# Pre-#27 wiring: one link straight at the checkout. Resolve the link FIRST,
# then take the parent. "cd X/.." resolves ".." lexically against the link
# path, which lands back in ~/.agents.
_resolved=$(CDPATH= cd -- "$AGENTS_DIR" 2>/dev/null && pwd -P)
CHECKOUT=$(dirname -- "$_resolved") CHECKOUT=$(dirname -- "$_resolved")
else else
CHECKOUT="$SELF_CHECKOUT" CHECKOUT="$SELF_CHECKOUT"
@@ -66,9 +75,26 @@ add() {
} }
# count_skills <dir> — directories containing a SKILL.md, one level down. # count_skills <dir> — directories containing a SKILL.md, one level down.
# -L on purpose: since #27 the entries are per-skill symlinks, and find does
# not descend into a symlinked directory without it.
count_skills() { count_skills() {
[ -d "$1" ] || { printf '0'; return; } [ -d "$1" ] || { printf '0'; return; }
find "$1"/ -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ' find -L "$1"/ -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
}
# under_known_source <physical-path> — 0 when the path lies inside a source
# install.sh recorded, or inside the active checkout.
under_known_source() {
_path=$1
_list="$SKILLS_DIR"
[ -f "$SOURCES_FILE" ] && _list="$_list
$(cat "$SOURCES_FILE")"
printf '%s\n' "$_list" | while IFS= read -r _src; do
[ -n "$_src" ] || continue
_real=$(CDPATH= cd -- "$_src" 2>/dev/null && pwd -P) || continue
[ -n "$_real" ] || continue
case "$_path" in "$_real"/*|"$_real") printf 'yes'; break ;; esac
done | grep -q yes
} }
# --- the checkout itself ---------------------------------------------------- # --- the checkout itself ----------------------------------------------------
@@ -95,10 +121,54 @@ for entry in "$SKILLS_DIR"/*; do
fi fi
done done
# --- the canonical directory --------------------------------------------------
# Pre-#27 this was a symlink at the checkout. It is a real directory now, one
# per-skill link per skill, fed by every configured source — that is what lets
# a second library (Anthropic's official set on flamingo) coexist instead of
# being displaced.
if [ -L "$AGENTS_DIR" ]; then
add WARN legacy "$AGENTS_DIR is the pre-#27 single-link wiring — re-run install.sh to adopt the multi-source layout"
elif [ -d "$AGENTS_DIR" ]; then
for entry in "$AGENTS_DIR"/*; do
[ -e "$entry" ] || [ -L "$entry" ] || continue
name=$(basename "$entry")
if [ ! -L "$entry" ]; then
add FAIL unmanaged "$AGENTS_DIR/$name is not a link into any source — nothing manages it and nothing updates it"
continue
fi
if [ ! -e "$entry" ]; then
add FAIL dangling "$entry -> $(readlink "$entry") (target does not exist)"
continue
fi
resolved=$(CDPATH= cd -- "$entry" 2>/dev/null && pwd -P)
if [ -n "$resolved" ] && ! under_known_source "$resolved"; then
add FAIL outside "$entry resolves to $resolved, an unknown source — it works, so nothing complains, while serving content nobody updates"
fi
done
# A per-skill layout can lose one skill without changing any total, so the
# invariant is per skill, not a count.
for dir in "$SKILLS_DIR"/*/; do
[ -f "$dir/SKILL.md" ] || continue
name=$(basename "$dir")
# Only skills that ship to the library belong here. A chat-only skill
# missing from a Crush directory is correct, not drift (#29).
surfaces=$(sed -n 's/^surfaces:[[:space:]]*//p' "$dir/SKILL.md" | head -1)
if [ -n "$surfaces" ] \
&& ! printf '%s' "$surfaces" | tr -d '[]' | tr ',' ' ' | grep -qw library; then
continue
fi
[ -L "$AGENTS_DIR/$name" ] \
|| add FAIL missing "$name is in the checkout but has no link in $AGENTS_DIR — re-run install.sh"
done
fi
# --- harness wiring --------------------------------------------------------- # --- harness wiring ---------------------------------------------------------
# Canonical location first; Pi reads it directly and the others link to it. # Crush and OpenCode keep their single directory link at the canonical dir;
# Pi reads it directly and needs no link of its own.
for dir in \ for dir in \
"$HOME/.agents/skills" \
"$HOME/.config/crush/skills" \ "$HOME/.config/crush/skills" \
"$HOME/.config/opencode/skills" "$HOME/.config/opencode/skills"
do do
@@ -110,32 +180,23 @@ do
fi fi
# A real directory where a link belongs holds content nothing manages: it # A real directory where a link belongs holds content nothing manages: it
# cannot receive new skills and silently shadows the checkout. Distinguished # cannot receive new skills and silently shadows the canonical dir. Since
# from the foreign-checkout case below because the fix differs — move it # #27 the fix is not only "move it aside" — it can be added as an extra
# aside and re-run install.sh, rather than re-point a link. # source instead, which is how flamingo keeps the official Anthropic set.
if [ ! -L "$dir" ] && [ -d "$dir" ]; then if [ ! -L "$dir" ] && [ -d "$dir" ]; then
add FAIL unmanaged "$dir is a real directory, not a link into the checkout — it holds $(count_skills "$dir") unmanaged skill(s); move it aside and re-run install.sh" add FAIL unmanaged "$dir is a real directory, not a link into the canonical dir — it holds $(count_skills "$dir") unmanaged skill(s); add it to SKILLS_EXTRA_SOURCES and re-run install.sh, or move it aside"
continue continue
fi fi
# Resolve to a physical path and confirm it lands inside THIS checkout.
# A link that resolves to a different checkout is the dangerous case: it
# works, so nothing complains, while serving stale content.
resolved=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd -P) resolved=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd -P)
canonical=$(CDPATH= cd -- "$SKILLS_DIR" 2>/dev/null && pwd -P) canonical=$(CDPATH= cd -- "$AGENTS_DIR" 2>/dev/null && pwd -P)
if [ -n "$resolved" ] && [ "$resolved" != "$canonical" ]; then if [ -n "$resolved" ] && [ -n "$canonical" ] && [ "$resolved" != "$canonical" ]; then
add FAIL outside "$dir resolves to $resolved, outside the active checkout $canonical" add FAIL outside "$dir resolves to $resolved, not the canonical $canonical"
continue
fi
got=$(count_skills "$dir")
if [ "$got" != "$EXPECTED" ]; then
add FAIL count "$dir sees $got skills, checkout has $EXPECTED"
fi fi
done done
# --- dangling links anywhere under the harness dirs ------------------------- # --- dangling links anywhere under the harness dirs -------------------------
for dir in "$HOME/.agents/skills" "$HOME/.config/crush/skills" \ for dir in "$HOME/.config/crush/skills" \
"$HOME/.config/opencode/skills" "$HOME/.claude/skills" "$HOME/.config/opencode/skills" "$HOME/.claude/skills"
do do
[ -d "$dir" ] || continue [ -d "$dir" ] || continue
@@ -180,10 +241,57 @@ if [ -f "$INSTALLED" ]; then
|| add WARN plugin "skills@mathias is not installed for Claude Code — /plugin marketplace add https://git.d-ma.be/mathias/skills.git" || add WARN plugin "skills@mathias is not installed for Claude Code — /plugin marketplace add https://git.d-ma.be/mathias/skills.git"
fi fi
# --- Claude Code: which GENERATION of the plugin is installed ---------------
# Every check above is about symlink paths, which is the wrong shape for the
# plugin: it is the only channel Claude Code has, and Claude Code gates updates
# on plugin.json's version. A host that never picked up a release loads all the
# skills successfully and is simply a generation behind — silently. Found on
# koala 2026-08-29: installed 0.6.0, marketplace clone 0.7.0, repo 0.8.5,
# doctor said "clean" (#30).
#
# Offline by construction, like the rest of doctor: "available" means the
# newest manifest already on this host — the active checkout and any
# marketplace clone — never a fetch.
manifest_version() {
[ -f "$1" ] || return 1
grep -q '"name"[[:space:]]*:[[:space:]]*"skills"' "$1" || return 1
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1
}
# newer_version a b — echoes whichever sorts higher on major.minor.patch.
# Lives in bin/newer-version because `task check` needs the same ordering (#34)
# and a second copy is a second thing to get wrong.
newer_version() {
sh "$SELF_DIR/newer-version" "$1" "$2"
}
installed_v=''
if [ -f "$INSTALLED" ]; then
installed_v=$(sed -n '/"skills@mathias"/,/\]/p' "$INSTALLED" \
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
fi
available_v=''
for manifest in "$CHECKOUT/.claude-plugin/plugin.json" \
"$HOME"/.claude/plugins/marketplaces/*/.claude-plugin/plugin.json
do
v=$(manifest_version "$manifest") || continue
[ -n "$v" ] || continue
if [ -z "$available_v" ]; then
available_v=$v
else
available_v=$(newer_version "$available_v" "$v")
fi
done
if [ -n "$installed_v" ] && [ -n "$available_v" ] && [ "$installed_v" != "$available_v" ]; then
add FAIL plugin-version "Claude Code has skills@mathias $installed_v installed but $available_v is available on this host — /plugin update skills@mathias (a host stale enough to matter is running the old doctor, so the first detection may need this run by hand)"
fi
# --- output ----------------------------------------------------------------- # --- output -----------------------------------------------------------------
if [ "$JSON" -eq 1 ]; then if [ "$JSON" -eq 1 ]; then
printf '{"checkout":"%s","expected":%s,"findings":[%s]}\n' \ printf '{"checkout":"%s","expected":%s,"plugin":{"installed":"%s","available":"%s"},"findings":[%s]}\n' \
"$CHECKOUT" "$EXPECTED" "${report%,}" "$CHECKOUT" "$EXPECTED" "$installed_v" "$available_v" "${report%,}"
elif [ "$findings" -gt 0 ]; then elif [ "$findings" -gt 0 ]; then
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \ printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
"$findings" "$CHECKOUT" "$report" "$findings" "$CHECKOUT" "$report"
+85
View File
@@ -0,0 +1,85 @@
#!/bin/sh
# generate-index — rebuild SKILLS_INDEX.md's table from skill frontmatter.
#
# The index was hand-maintained and drifted: `dream` and `experiment-spec`
# shipped with no row at all, because their rows lived on branches that were
# squash-merged after a directory reorg (#15). A gate catches that after the
# fact; generating removes the class (#29).
#
# The curated summary and trigger live in each SKILL.md as `summary:` and
# `use_when:` rather than being derived from `description:` — description is a
# paragraph written for model selection, and truncating it produces a worse
# table than the one being replaced.
#
# Everything outside the markers is left untouched: the wiring docs, the
# relationships tree and the trigger table are prose, not data.
#
# generate-index [root] rewrite the table in place
# generate-index --check [root] exit 1 if the table is out of date
set -u
CHECK=0
if [ "${1:-}" = "--check" ]; then CHECK=1; shift; fi
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
INDEX="$ROOT/SKILLS_INDEX.md"
BEGIN='<!-- BEGIN skills-table -->'
END='<!-- END skills-table -->'
[ -f "$INDEX" ] || { echo "ERROR: missing $INDEX" >&2; exit 1; }
grep -qF "$BEGIN" "$INDEX" || { echo "ERROR: $INDEX has no $BEGIN marker" >&2; exit 1; }
grep -qF "$END" "$INDEX" || { echo "ERROR: $INDEX has no $END marker" >&2; exit 1; }
# field <file> <key> — a scalar frontmatter value, quotes stripped.
field() {
sed -n "s/^$2:[[:space:]]*//p" "$1" | head -1 | sed 's/^"//; s/"$//'
}
table=$(mktemp)
fail=0
{
printf '| Skill | Description | Use when | Surfaces |\n'
printf '|-------|-------------|----------|----------|\n'
} > "$table"
for dir in "$ROOT"/skills/*/; do
[ -f "$dir/SKILL.md" ] || continue
name=$(basename "$dir")
summary=$(field "$dir/SKILL.md" summary)
use_when=$(field "$dir/SKILL.md" use_when)
surfaces=$(field "$dir/SKILL.md" surfaces | tr -d '[]')
if [ -z "$summary" ] || [ -z "$use_when" ]; then
echo "ERROR: skills/$name has no 'summary:'/'use_when:' — the index table is generated from them" >&2
fail=1
continue
fi
printf '| `%s` | %s | %s | %s |\n' "$name" "$summary" "$use_when" "$surfaces" >> "$table"
done
if [ "$fail" -ne 0 ]; then
rm -f "$table"
exit 1
fi
out=$(mktemp)
awk -v begin="$BEGIN" -v end="$END" -v tablefile="$table" '
$0 == begin { print; while ((getline line < tablefile) > 0) print line; skip = 1; next }
$0 == end { print; skip = 0; next }
!skip { print }
' "$INDEX" > "$out"
rm -f "$table"
if [ "$CHECK" -eq 1 ]; then
if cmp -s "$out" "$INDEX"; then
rm -f "$out"
exit 0
fi
rm -f "$out"
echo "ERROR: SKILLS_INDEX.md is out of date — run 'task index' and commit the result" >&2
exit 1
fi
mv "$out" "$INDEX"
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# newer-version A B — echo whichever of two dotted versions sorts higher.
# Equal versions echo A. Missing components count as zero, so 0.9 == 0.9.0.
#
# Exists because two callers need ordering rather than equality:
# - bin/doctor, comparing the installed plugin generation against the newest
# manifest on the host (#30);
# - task check, which must fail when plugin.json is BEHIND the latest tag and
# stay quiet when it is ahead — the normal state on a checkout that has not
# fetched the tag CI just pushed (#34).
#
# sort -V would do it in one line and is not portable enough for this fleet.
set -u
[ $# -eq 2 ] || { echo "usage: newer-version A B" >&2; exit 2; }
printf '%s\n%s\n' "$1" "$2" | awk -F. '
{ raw[NR] = $0; for (i = 1; i <= 3; i++) n[NR, i] = ($i == "" ? 0 : $i + 0) }
END {
for (i = 1; i <= 3; i++) {
if (n[1, i] > n[2, i]) { print raw[1]; exit }
if (n[1, i] < n[2, i]) { print raw[2]; exit }
}
print raw[1]
}'
+12 -5
View File
@@ -50,11 +50,18 @@ if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+document\s+get\b'; then
fi fi
# Raw env/secret-file dumps (infra#228/#229 root cause). # Raw env/secret-file dumps (infra#228/#229 root cause).
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)env(\s|$).*\|.*grep'; then #
deny "Blocked: 'env | grep' prints matching values in full. Use an existence-only check: [ -n \"\$VAR\" ] && echo set" # `env` is a dump only when nothing but options and VAR=value assignments
fi # follow it. `env -u VAR cmd` and `env FOO=bar cmd` EXEC a command with a
if printf '%s' "$CMD" | grep -Eq '(^|[;&|])[[:space:]]*env[[:space:]]*($|[;&|])'; then # modified environment and print nothing themselves — piping those into grep
deny "Blocked: bare 'env' prints every variable's value, including secrets. Use an existence-only check: [ -n \"\$VAR\" ] && echo set" # filters the command's own output, not the environment (#25). The earlier
# pattern matched any `env` token followed anywhere later by a pipe into
# grep, which blocked the documented 1Password write path
# (`env -u OP_SERVICE_ACCOUNT_TOKEN op item create`) and cost a workaround.
# Denying only the "no command word follows" shape also catches `env FOO=bar`
# with no command, which the two previous rules let through.
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|[[:space:]])env([[:space:]]+-[iu][[:space:]]*[A-Za-z_][A-Za-z0-9_]*|[[:space:]]+-[iu]|[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*)*[[:space:]]*($|[|;&])'; then
deny "Blocked: this 'env' prints every variable's value, including secrets (no command word follows it, so it dumps rather than execs). Use an existence-only check: [ -n \"\$VAR\" ] && echo set. Note 'env -u VAR cmd' and 'env FOO=bar cmd' are allowed — they run a command."
fi fi
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set" deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
+122 -6
View File
@@ -20,14 +20,24 @@
# cannot. This script covers the harnesses with no plugin system: Crush, Pi # cannot. This script covers the harnesses with no plugin system: Crush, Pi
# and OpenCode. # and OpenCode.
# #
# Wiring model — one directory link per harness, never one per skill: # Wiring model — one managed directory, one link per harness:
# #
# ~/.agents/skills -> $CHECKOUT_DIR/skills (canonical; Pi reads it directly) # ~/.agents/skills/tdd -> $CHECKOUT_DIR/skills/tdd (canonical; Pi reads the dir directly)
# ~/.agents/skills/xlsx -> $OTHER_SOURCE/xlsx
# ~/.config/crush/skills -> ~/.agents/skills # ~/.config/crush/skills -> ~/.agents/skills
# ~/.config/opencode/skills -> ~/.agents/skills # ~/.config/opencode/skills -> ~/.agents/skills
# #
# Adding a skill to the repo therefore wires it everywhere with no re-run, # ~/.agents/skills was itself a link to this checkout until #27. That encoded
# and a broken link is one broken link rather than 42. # 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.
# A real directory of per-skill links serves several sources at once: total
# links are N + one per harness, still not N x harnesses (#23's actual point).
#
# Extra sources come from SKILLS_EXTRA_SOURCES, a colon-separated list of
# directories each holding <skill>/SKILL.md. The checkout is always first and
# always wins a name collision — but the collision is logged, never resolved
# in silence, because quietly picking one is how stale content wins.
# #
# CHECKOUT_DIR deliberately defaults OUTSIDE any file-synced or # CHECKOUT_DIR deliberately defaults OUTSIDE any file-synced or
# cross-platform-shared directory. Do not "fix" it to point into ~/dev: on a # cross-platform-shared directory. Do not "fix" it to point into ~/dev: on a
@@ -43,6 +53,10 @@ CHECKOUT_DIR="${SKILLS_CHECKOUT_DIR:-$HOME/.local/share/skills}"
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}" AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}" CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}" OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
EXTRA_SOURCES="${SKILLS_EXTRA_SOURCES:-}"
# Recorded beside the managed directory rather than inside it: a stray file in
# there would look like a skill to every harness reading the dir.
SOURCES_MANIFEST="$(dirname "$AGENTS_DIR")/$(basename "$AGENTS_DIR")-sources"
log() { printf '[skills] %s\n' "$*"; } log() { printf '[skills] %s\n' "$*"; }
@@ -74,6 +88,106 @@ count_skills() {
find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ' find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
} }
# sources — every configured source directory, primary first, one per line.
sources() {
printf '%s\n' "$CHECKOUT_DIR/skills"
[ -n "$EXTRA_SOURCES" ] || return 0
# printf '%s\n' deliberately: without the trailing newline `read` hits EOF
# on the final entry and the loop body never runs for it.
printf '%s\n' "$EXTRA_SOURCES" | tr ':' '\n' | while IFS= read -r src; do
[ -n "$src" ] || continue
if [ -d "$src" ]; then
printf '%s\n' "$src"
else
log "extra source $src does not exist — skipping"
fi
done
}
# prepare_agents_dir — the canonical location is a real directory now. A host
# still on the single-link wiring is migrated in place.
prepare_agents_dir() {
if [ -L "$AGENTS_DIR" ]; then
log "migrating $AGENTS_DIR from the single-link wiring to a per-skill directory"
rm -f "$AGENTS_DIR"
elif [ -e "$AGENTS_DIR" ] && [ ! -d "$AGENTS_DIR" ]; then
log "REFUSING to replace $AGENTS_DIR — it exists and is not a directory"
return 1
fi
mkdir -p "$AGENTS_DIR"
}
# wire_skills — one symlink per skill, from every configured source.
wire_skills() {
local src_list desired name src dir link target
src_list="$(mktemp)"
sources > "$src_list"
desired=""
while IFS= read -r src; do
[ -d "$src" ] || continue
for dir in "$src"/*/; do
[ -f "$dir/SKILL.md" ] || continue
name="$(basename "$dir")"
target="${dir%/}"
# A skill declares its surfaces; this installer serves exactly one of
# them. A chat-only skill linked into Crush can never fire and competes
# for selection with ones that can (#29). A source with no declaration
# is foreign — Anthropic's official set has no reason to know this
# convention — so silence means "link it".
surfaces="$(sed -n 's/^surfaces:[[:space:]]*//p' "$dir/SKILL.md" | head -1)"
if [ -n "$surfaces" ] && ! printf '%s' "$surfaces" | tr -d '[]' | tr ',' ' ' | grep -qw library; then
log "skipping $name — surfaces: $surfaces does not include the library"
continue
fi
if printf '%s' "$desired" | grep -qx -- "$name"; then
log "COLLISION: $name is offered by $target too; keeping $AGENTS_DIR/$name -> $(readlink "$AGENTS_DIR/$name" 2>/dev/null || echo '?')"
continue
fi
link="$AGENTS_DIR/$name"
if [ -L "$link" ]; then
if [ "$(readlink "$link")" = "$target" ]; then
desired="$desired$name
"
continue
fi
rm -f "$link"
elif [ -e "$link" ]; then
log "unmanaged: $link is not a symlink — left in place; move it aside to let $target take the name"
continue
fi
ln -s "$target" "$link"
desired="$desired$name
"
done
done < "$src_list"
rm -f "$src_list"
# Removing a skill from a source must remove its link, or the previous
# generation's problem (links resolving to nothing, invisible to git)
# returns in a new shape.
for link in "$AGENTS_DIR"/*; do
[ -e "$link" ] || [ -L "$link" ] || continue
name="$(basename "$link")"
if [ ! -L "$link" ]; then
log "unmanaged: $link is not a symlink and no source offers it — left in place"
continue
fi
printf '%s' "$desired" | grep -qx -- "$name" && continue
rm -f "$link"
log "pruned $name — no configured source offers it any more"
done
sources > "$SOURCES_MANIFEST"
# Reported through a global, not stdout: wire_skills logs collisions and
# unmanaged entries as it goes, and capturing its stdout for a count would
# swallow exactly the warnings this change exists to surface.
LINKED_COUNT=$(printf '%s' "$desired" | grep -c . || true)
}
# link_dir <link_path> <target_path> # link_dir <link_path> <target_path>
# Replaces whatever is there — including a directory left behind by the old # Replaces whatever is there — including a directory left behind by the old
# per-skill wiring — with a single symlink. # per-skill wiring — with a single symlink.
@@ -104,12 +218,14 @@ main() {
# Canonical location. Pi reads ~/.agents/skills directly, so it needs no # Canonical location. Pi reads ~/.agents/skills directly, so it needs no
# harness-specific link of its own. # harness-specific link of its own.
link_dir "$AGENTS_DIR" "$CHECKOUT_DIR/skills" prepare_agents_dir
LINKED_COUNT=0
wire_skills
link_dir "$CRUSH_DIR" "$AGENTS_DIR" link_dir "$CRUSH_DIR" "$AGENTS_DIR"
link_dir "$OPENCODE_DIR" "$AGENTS_DIR" link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
log "done — $(count_skills) skill(s) at ref=$REF, 3 links total" log "done — $LINKED_COUNT skill(s) linked from $(sources | wc -l | tr -d ' ') source(s), ref=$REF"
log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL" log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL"
} }
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: atdd name: atdd
surfaces: [claude-code, library]
summary: "Acceptance Test-Driven Development workflow"
use_when: "Feature development with acceptance criteria"
description: Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time. description: Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: cad-babysit name: cad-babysit
surfaces: [claude-code, library]
summary: "Focused human-in-the-loop session working CAD's stuck queue (read-only)"
use_when: "Picking up whatever `cad stuck` flags, with a human present"
description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present. description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: clean-code name: clean-code
surfaces: [claude-code, library]
summary: "Clean code principles with Go adaptation"
use_when: "Code review, refactoring, new code"
description: Write code that humans can read, understand, and safely change. Apply SOLID, GRASP, and Clean Code principles during the REFACTOR phase of TDD. description: Write code that humans can read, understand, and safely change. Apply SOLID, GRASP, and Clean Code principles during the REFACTOR phase of TDD.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: code-review name: code-review
surfaces: [claude-code, library]
summary: "Code smell detection + Go-specific checklist"
use_when: "Pre-merge review"
description: Systematic code smell detection and Go-specific review checklist. Load before any pre-merge review. description: Systematic code smell detection and Go-specific review checklist. Load before any pre-merge review.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: cognitive-load name: cognitive-load
surfaces: [claude-code, library]
summary: "Cognitive complexity analysis (CLI score)"
use_when: "Identifying hotspots, before refactoring"
description: Measure how much mental effort a codebase demands from developers. Use when identifying hotspots, before architectural decisions, or when code feels hard to understand. description: Measure how much mental effort a codebase demands from developers. Use when identifying hotspots, before architectural decisions, or when code feels hard to understand.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: debug name: debug
surfaces: [claude-code, library]
summary: "Hypothesis-first debugging discipline"
use_when: "Any test failure or bug whose cause is not immediately obvious"
description: Systematic hypothesis-first debugging. Generate 3-5 ranked hypotheses with verification steps before suggesting any fix. Use when encountering a bug, test failure, or unexpected behavior. description: Systematic hypothesis-first debugging. Generate 3-5 ranked hypotheses with verification steps before suggesting any fix. Use when encountering a bug, test failure, or unexpected behavior.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: discovery-framing name: discovery-framing
surfaces: [claude-code, library]
summary: "Problem-before-solution discovery/framing method"
use_when: "Framing/validating a problem before any solution; opportunity sizing"
description: Run the discovery / framing phase of product innovation — define and quantify a focused customer problem BEFORE any solution exists. Four movements: understand the problem space, learn about customers, identify and size problems, decide pursue/table/kill. Produces a problem brief per prioritized opportunity. Use when an idea is still vague, when someone is jumping to solutions before the problem is validated, when scoping early research, or when deciding which problems are worth pursuing. Trigger phrases include "frame the problem", "discovery", "problem statement", "is this worth building", "early research", "opportunity sizing", "we have an idea for", "how might we", "what problem are we solving". description: Run the discovery / framing phase of product innovation — define and quantify a focused customer problem BEFORE any solution exists. Four movements: understand the problem space, learn about customers, identify and size problems, decide pursue/table/kill. Produces a problem brief per prioritized opportunity. Use when an idea is still vague, when someone is jumping to solutions before the problem is validated, when scoping early research, or when deciding which problems are worth pursuing. Trigger phrases include "frame the problem", "discovery", "problem statement", "is this worth building", "early research", "opportunity sizing", "we have an idea for", "how might we", "what problem are we solving".
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: dream name: dream
surfaces: [claude-code, library, chat]
summary: "Reflective consolidation pass over an agent's memory directory"
use_when: "Pruning/merging memory files, rebuilding a memory index"
description: > description: >
Run a "dream" — a reflective memory consolidation pass over an agent's memory Run a "dream" — a reflective memory consolidation pass over an agent's memory
directory. Use this skill whenever the user says "dream", "run a dream", "consolidate directory. Use this skill whenever the user says "dream", "run a dream", "consolidate
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: experiment-spec name: experiment-spec
surfaces: [claude-code, library]
summary: "Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol"
use_when: "Scoping a research/ML phase instead of a feature"
description: Write a rigorous experiment spec for a research phase before any code or training runs. Use instead of feature-spec for scientific/ML research projects. Enforces falsifiable hypothesis, quantitative acceptance criteria, baseline comparison, and null-result protocol. description: Write a rigorous experiment spec for a research phase before any code or training runs. Use instead of feature-spec for scientific/ML research projects. Enforces falsifiable hypothesis, quantitative acceptance criteria, baseline comparison, and null-result protocol.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: feature-spec name: feature-spec
surfaces: [claude-code, library]
summary: "Implementation-level feature spec (tighter than spec-driven-dev)"
use_when: "Scoping one feature inside an already-specced project"
description: Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it. description: Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: gitea-ci name: gitea-ci
surfaces: [claude-code, library]
summary: "Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror"
use_when: "Setting up or debugging CI on this infra"
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, shared buildctl image-build script, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere. description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, shared buildctl image-build script, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: grill-me name: grill-me
surfaces: [claude-code, library, chat]
summary: "Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem"
use_when: "Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod"
description: Critically interrogates plans, ideas, and proposals to make them sharper, more robust, and better grounded — with the explicit goal of producing a meaningfully better plan by the end. Use this skill whenever the user wants to stress-test a plan, prepare for hard questions, check if an idea is ready to act on, validate a hypothesis, or explicitly asks to be challenged, questioned, or grilled. Also trigger when a conversation has been exploring an idea for a while and it's time to converge — use this skill to check if the idea is actually ready. Trigger phrases include "grill me", "challenge this", "poke holes", "am I missing something", "is this ready", "stress-test this", "devil's advocate", "what am I not seeing". description: Critically interrogates plans, ideas, and proposals to make them sharper, more robust, and better grounded — with the explicit goal of producing a meaningfully better plan by the end. Use this skill whenever the user wants to stress-test a plan, prepare for hard questions, check if an idea is ready to act on, validate a hypothesis, or explicitly asks to be challenged, questioned, or grilled. Also trigger when a conversation has been exploring an idea for a while and it's time to converge — use this skill to check if the idea is actually ready. Trigger phrases include "grill me", "challenge this", "poke holes", "am I missing something", "is this ready", "stress-test this", "devil's advocate", "what am I not seeing".
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: own-the-task name: own-the-task
surfaces: [claude-code, library, chat]
summary: "Delegation ownership — Intake → Brief → Plan → Execute"
use_when: "A task is handed over open-ended; \"take this off my plate\"; before a plan exists"
description: Take ownership of a delegated task like a hired employee — interview first, write a handover brief, propose a plan, then execute. Use whenever the user says "take this off my plate", "own this", "act as my AI employee", "interview me first", "ask me questions before you start", or delegates anything open-ended where the real goal, the why, and the definition of done are not yet on the table. Trigger even when "interview" is never said — any delegation where getting the intent wrong would waste the whole effort belongs here. description: Take ownership of a delegated task like a hired employee — interview first, write a handover brief, propose a plan, then execute. Use whenever the user says "take this off my plate", "own this", "act as my AI employee", "interview me first", "ask me questions before you start", or delegates anything open-ended where the real goal, the why, and the definition of done are not yet on the table. Trigger even when "interview" is never said — any delegation where getting the intent wrong would waste the whole effort belongs here.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: planning name: planning
surfaces: [claude-code, library]
summary: "Planning and task breakdown"
use_when: "Sprint planning, task decomposition"
description: Decompose work into small, verifiable, ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable units. description: Decompose work into small, verifiable, ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable units.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: playwright name: playwright
surfaces: [claude-code, library]
summary: "Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring"
use_when: "Adding browser tests to any project with a web UI"
description: Add and run Playwright E2E tests for any project with a web UI — covers setup, HTMX-specific patterns, SSE/streaming, Taskfile integration, and CI wiring. description: Add and run Playwright E2E tests for any project with a web UI — covers setup, HTMX-specific patterns, SSE/streaming, Taskfile integration, and CI wiring.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: problem-analysis name: problem-analysis
surfaces: [claude-code, library]
summary: "Deep problem understanding before coding"
use_when: "Starting any non-trivial task"
description: Deeply understand a problem before coding begins. Identify the real problem, stakeholders, constraints, and success criteria. Use before starting any non-trivial feature. description: Deeply understand a problem before coding begins. Identify the real problem, stakeholders, constraints, and success criteria. Use before starting any non-trivial feature.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: refactoring name: refactoring
surfaces: [claude-code, library]
summary: "Systematic refactoring methodology (Fowler)"
use_when: "When improving existing code"
description: Systematic refactoring methodology based on Martin Fowler's catalog. Apply during the REFACTOR phase of TDD, never while making code changes that modify behavior. description: Systematic refactoring methodology based on Martin Fowler's catalog. Apply during the REFACTOR phase of TDD, never while making code changes that modify behavior.
--- ---
@@ -1,5 +1,8 @@
--- ---
name: regulatory-risk-assessment name: regulatory-risk-assessment
surfaces: [claude-code, library]
summary: "Structured risk register for regulated-industry features"
use_when: "Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data"
description: Produce a structured regulatory risk assessment / risk-register entry for regulated-industry features (banking, payments, PSD2/PSR, DORA, AML). Use when filing a CAD issue that needs a Risk:LOW/MEDIUM/HIGH level, or designing features touching payments, auth, data storage, or external APIs ("what are the risks?", "compliance gate", "risk register"). description: Produce a structured regulatory risk assessment / risk-register entry for regulated-industry features (banking, payments, PSD2/PSR, DORA, AML). Use when filing a CAD issue that needs a Risk:LOW/MEDIUM/HIGH level, or designing features touching payments, auth, data storage, or external APIs ("what are the risks?", "compliance gate", "risk register").
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: secret-hygiene name: secret-hygiene
surfaces: [claude-code, library]
summary: "Stop credentials rendering into the session transcript; safe rotation chain when one leaks"
use_when: "Before any kubectl/SOPS/token command; after a confirmed leak"
description: Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong". description: Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong".
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: session-retrospective name: session-retrospective
surfaces: [claude-code, library]
summary: "Surface non-obvious learnings from a session log"
use_when: "After a coding session ends, before context is lost"
description: Review a completed coding session (from brain/sessions/*.jsonl) and surface non-obvious learnings worth preserving. Use after a session ends, before context is lost. description: Review a completed coding session (from brain/sessions/*.jsonl) and surface non-obvious learnings worth preserving. Use after a session ends, before context is lost.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: solid name: solid
surfaces: [claude-code, library]
summary: "SOLID principles with Go examples"
use_when: "Design review, architecture decisions"
description: Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions. description: Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: spec-driven-dev name: spec-driven-dev
surfaces: [claude-code, library]
summary: "Spec-driven development for PMs"
use_when: "Writing specs and requirements"
description: Write a structured specification before writing any code. Use when starting a new project, feature, or significant change. Adapted for a PM-first workflow where why comes before how. description: Write a structured specification before writing any code. Use when starting a new project, feature, or significant change. Adapted for a PM-first workflow where why comes before how.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: stage-gate-review name: stage-gate-review
surfaces: [claude-code, library]
summary: "Stage-gate product governance (go/no-go council)"
use_when: "Preparing a Product Council ask; building a go/no-go checklist; gate review"
description: Run or prepare a stage-gate product governance review — the go/no-go checkpoint where a council approves an initiative to advance through fixed innovation stages (Frame → Concept → Prototype → Validate → Scale), each gate guarded by a per-discipline readiness checklist. Use when preparing a Product Council / steering-group ask, building a go/no-go checklist, designing an innovation governance process, or auditing whether an initiative is genuinely ready to advance vs faking the gate. Trigger phrases include "stage gate", "product council", "go/no-go", "advance to the next stage", "is this ready to progress", "gate review", "phase gate", "innovation governance". description: Run or prepare a stage-gate product governance review — the go/no-go checkpoint where a council approves an initiative to advance through fixed innovation stages (Frame → Concept → Prototype → Validate → Scale), each gate guarded by a per-discipline readiness checklist. Use when preparing a Product Council / steering-group ask, building a go/no-go checklist, designing an innovation governance process, or auditing whether an initiative is genuinely ready to advance vs faking the gate. Trigger phrases include "stage gate", "product council", "go/no-go", "advance to the next stage", "is this ready to progress", "gate review", "phase gate", "innovation governance".
--- ---
@@ -1,5 +1,8 @@
--- ---
name: brainstorming name: brainstorming
surfaces: [claude-code, library]
summary: "Structured divergent brainstorming"
use_when: "Generating or expanding a set of ideas"
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation." description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
--- ---
@@ -1,5 +1,8 @@
--- ---
name: dispatching-parallel-agents name: dispatching-parallel-agents
surfaces: [claude-code, library]
summary: "Fan work out across parallel subagents"
use_when: "Decomposing work for concurrent agents"
description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
--- ---
@@ -1,5 +1,8 @@
--- ---
name: executing-plans name: executing-plans
surfaces: [claude-code, library]
summary: "Executing a written plan step by step"
use_when: "Running an approved plan to completion"
description: Use when you have a written implementation plan to execute in a separate session with review checkpoints description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
--- ---
@@ -1,5 +1,8 @@
--- ---
name: finishing-a-development-branch name: finishing-a-development-branch
surfaces: [claude-code, library]
summary: "Cleanly finishing/landing a dev branch"
use_when: "Wrapping a branch → merge/PR"
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
--- ---
@@ -1,5 +1,8 @@
--- ---
name: receiving-code-review name: receiving-code-review
surfaces: [claude-code, library]
summary: "Acting on received review feedback"
use_when: "Responding to review comments"
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
--- ---
@@ -1,5 +1,8 @@
--- ---
name: requesting-code-review name: requesting-code-review
surfaces: [claude-code, library]
summary: "Requesting a code review well"
use_when: "Asking another agent/human to review"
description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
--- ---
@@ -1,5 +1,8 @@
--- ---
name: subagent-driven-development name: subagent-driven-development
surfaces: [claude-code, library]
summary: "Subagent-driven development workflow"
use_when: "Building via orchestrated subagents"
description: Use when executing implementation plans with independent tasks in the current session description: Use when executing implementation plans with independent tasks in the current session
--- ---
@@ -1,5 +1,8 @@
--- ---
name: systematic-debugging name: systematic-debugging
surfaces: [claude-code, library]
summary: "Hypothesis-first systematic debugging"
use_when: "Diagnosing a failure methodically"
description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
--- ---
@@ -1,371 +0,0 @@
---
name: test-driven-development
description: Use when implementing any feature or bugfix, before writing implementation code
---
# Test-Driven Development (TDD)
## Overview
Write the test first. Watch it fail. Write minimal code to pass.
**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
**Violating the letter of the rules is violating the spirit of the rules.**
## When to Use
**Always:**
- New features
- Bug fixes
- Refactoring
- Behavior changes
**Exceptions (ask your human partner):**
- Throwaway prototypes
- Generated code
- Configuration files
Thinking "skip TDD just this once"? Stop. That's rationalization.
## The Iron Law
```
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
```
Write code before the test? Delete it. Start over.
**No exceptions:**
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Implement fresh from tests. Period.
## Red-Green-Refactor
```dot
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="Verify passes\nAll green", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="stay\ngreen"];
verify_green -> next;
next -> red;
}
```
### RED - Write Failing Test
Write one minimal test showing what should happen.
<Good>
```typescript
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
```
Clear name, tests real behavior, one thing
</Good>
<Bad>
```typescript
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
```
Vague name, tests mock not code
</Bad>
**Requirements:**
- One behavior
- Clear name
- Real code (no mocks unless unavoidable)
### Verify RED - Watch It Fail
**MANDATORY. Never skip.**
```bash
npm test path/to/test.test.ts
```
Confirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)
**Test passes?** You're testing existing behavior. Fix test.
**Test errors?** Fix error, re-run until it fails correctly.
### GREEN - Minimal Code
Write simplest code to pass the test.
<Good>
```typescript
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
for (let i = 0; i < 3; i++) {
try {
return await fn();
} catch (e) {
if (i === 2) throw e;
}
}
throw new Error('unreachable');
}
```
Just enough to pass
</Good>
<Bad>
```typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI
}
```
Over-engineered
</Bad>
Don't add features, refactor other code, or "improve" beyond the test.
### Verify GREEN - Watch It Pass
**MANDATORY.**
```bash
npm test path/to/test.test.ts
```
Confirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)
**Test fails?** Fix code, not test.
**Other tests fail?** Fix now.
### REFACTOR - Clean Up
After green only:
- Remove duplication
- Improve names
- Extract helpers
Keep tests green. Don't add behavior.
### Repeat
Next failing test for next feature.
## Good Tests
| Quality | Good | Bad |
|---------|------|-----|
| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |
| **Clear** | Name describes behavior | `test('test1')` |
| **Shows intent** | Demonstrates desired API | Obscures what code should do |
## Why Order Matters
**"I'll write tests after to verify it works"**
Tests written after code pass immediately. Passing immediately proves nothing:
- Might test wrong thing
- Might test implementation, not behavior
- Might miss edge cases you forgot
- You never saw it catch the bug
Test-first forces you to see the test fail, proving it actually tests something.
**"I already manually tested all the edge cases"**
Manual testing is ad-hoc. You think you tested everything but:
- No record of what you tested
- Can't re-run when code changes
- Easy to forget cases under pressure
- "It worked when I tried it" ≠ comprehensive
Automated tests are systematic. They run the same way every time.
**"Deleting X hours of work is wasteful"**
Sunk cost fallacy. The time is already gone. Your choice now:
- Delete and rewrite with TDD (X more hours, high confidence)
- Keep it and add tests after (30 min, low confidence, likely bugs)
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
**"TDD is dogmatic, being pragmatic means adapting"**
TDD IS pragmatic:
- Finds bugs before commit (faster than debugging after)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use code)
- Enables refactoring (change freely, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower.
**"Tests after achieve the same goals - it's spirit not ritual"**
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
## Common Rationalizations
| Excuse | Reality |
|--------|---------|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
## Red Flags - STOP and Start Over
- Code before test
- Test after implementation
- Test passes immediately
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "It's about spirit not ritual"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."
**All of these mean: Delete code. Start over with TDD.**
## Example: Bug Fix
**Bug:** Empty email accepted
**RED**
```typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
```
**Verify RED**
```bash
$ npm test
FAIL: expected 'Email required', got undefined
```
**GREEN**
```typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
```
**Verify GREEN**
```bash
$ npm test
PASS
```
**REFACTOR**
Extract validation for multiple fields if needed.
## Verification Checklist
Before marking work complete:
- [ ] Every new function/method has a test
- [ ] Watched each test fail before implementing
- [ ] Each test failed for expected reason (feature missing, not typo)
- [ ] Wrote minimal code to pass each test
- [ ] All tests pass
- [ ] Output pristine (no errors, warnings)
- [ ] Tests use real code (mocks only if unavoidable)
- [ ] Edge cases and errors covered
Can't check all boxes? You skipped TDD. Start over.
## When Stuck
| Problem | Solution |
|---------|----------|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
## Debugging Integration
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
Never fix bugs without a test.
## Testing Anti-Patterns
When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
- Testing mock behavior instead of real behavior
- Adding test-only methods to production classes
- Mocking without understanding dependencies
## Final Rule
```
Production code → test exists and failed first
Otherwise → not TDD
```
No exceptions without your human partner's permission.
@@ -1,299 +0,0 @@
# Testing Anti-Patterns
**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.
## Overview
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
**Core principle:** Test what the code does, not what the mocks do.
**Following strict TDD prevents these anti-patterns.**
## The Iron Laws
```
1. NEVER test mock behavior
2. NEVER add test-only methods to production classes
3. NEVER mock without understanding dependencies
```
## Anti-Pattern 1: Testing Mock Behavior
**The violation:**
```typescript
// ❌ BAD: Testing that the mock exists
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});
```
**Why this is wrong:**
- You're verifying the mock works, not that the component works
- Test passes when mock is present, fails when it's not
- Tells you nothing about real behavior
**your human partner's correction:** "Are we testing the behavior of a mock?"
**The fix:**
```typescript
// ✅ GOOD: Test real component or don't mock it
test('renders sidebar', () => {
render(<Page />); // Don't mock sidebar
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
// OR if sidebar must be mocked for isolation:
// Don't assert on the mock - test Page's behavior with sidebar present
```
### Gate Function
```
BEFORE asserting on any mock element:
Ask: "Am I testing real component behavior or just mock existence?"
IF testing mock existence:
STOP - Delete the assertion or unmock the component
Test real behavior instead
```
## Anti-Pattern 2: Test-Only Methods in Production
**The violation:**
```typescript
// ❌ BAD: destroy() only used in tests
class Session {
async destroy() { // Looks like production API!
await this._workspaceManager?.destroyWorkspace(this.id);
// ... cleanup
}
}
// In tests
afterEach(() => session.destroy());
```
**Why this is wrong:**
- Production class polluted with test-only code
- Dangerous if accidentally called in production
- Violates YAGNI and separation of concerns
- Confuses object lifecycle with entity lifecycle
**The fix:**
```typescript
// ✅ GOOD: Test utilities handle test cleanup
// Session has no destroy() - it's stateless in production
// In test-utils/
export async function cleanupSession(session: Session) {
const workspace = session.getWorkspaceInfo();
if (workspace) {
await workspaceManager.destroyWorkspace(workspace.id);
}
}
// In tests
afterEach(() => cleanupSession(session));
```
### Gate Function
```
BEFORE adding any method to production class:
Ask: "Is this only used by tests?"
IF yes:
STOP - Don't add it
Put it in test utilities instead
Ask: "Does this class own this resource's lifecycle?"
IF no:
STOP - Wrong class for this method
```
## Anti-Pattern 3: Mocking Without Understanding
**The violation:**
```typescript
// ❌ BAD: Mock breaks test logic
test('detects duplicate server', () => {
// Mock prevents config write that test depends on!
vi.mock('ToolCatalog', () => ({
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
}));
await addServer(config);
await addServer(config); // Should throw - but won't!
});
```
**Why this is wrong:**
- Mocked method had side effect test depended on (writing config)
- Over-mocking to "be safe" breaks actual behavior
- Test passes for wrong reason or fails mysteriously
**The fix:**
```typescript
// ✅ GOOD: Mock at correct level
test('detects duplicate server', () => {
// Mock the slow part, preserve behavior test needs
vi.mock('MCPServerManager'); // Just mock slow server startup
await addServer(config); // Config written
await addServer(config); // Duplicate detected ✓
});
```
### Gate Function
```
BEFORE mocking any method:
STOP - Don't mock yet
1. Ask: "What side effects does the real method have?"
2. Ask: "Does this test depend on any of those side effects?"
3. Ask: "Do I fully understand what this test needs?"
IF depends on side effects:
Mock at lower level (the actual slow/external operation)
OR use test doubles that preserve necessary behavior
NOT the high-level method the test depends on
IF unsure what test depends on:
Run test with real implementation FIRST
Observe what actually needs to happen
THEN add minimal mocking at the right level
Red flags:
- "I'll mock this to be safe"
- "This might be slow, better mock it"
- Mocking without understanding the dependency chain
```
## Anti-Pattern 4: Incomplete Mocks
**The violation:**
```typescript
// ❌ BAD: Partial mock - only fields you think you need
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' }
// Missing: metadata that downstream code uses
};
// Later: breaks when code accesses response.metadata.requestId
```
**Why this is wrong:**
- **Partial mocks hide structural assumptions** - You only mocked fields you know about
- **Downstream code may depend on fields you didn't include** - Silent failures
- **Tests pass but integration fails** - Mock incomplete, real API complete
- **False confidence** - Test proves nothing about real behavior
**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
**The fix:**
```typescript
// ✅ GOOD: Mirror real API completeness
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' },
metadata: { requestId: 'req-789', timestamp: 1234567890 }
// All fields real API returns
};
```
### Gate Function
```
BEFORE creating mock responses:
Check: "What fields does the real API response contain?"
Actions:
1. Examine actual API response from docs/examples
2. Include ALL fields system might consume downstream
3. Verify mock matches real response schema completely
Critical:
If you're creating a mock, you must understand the ENTIRE structure
Partial mocks fail silently when code depends on omitted fields
If uncertain: Include all documented fields
```
## Anti-Pattern 5: Integration Tests as Afterthought
**The violation:**
```
✅ Implementation complete
❌ No tests written
"Ready for testing"
```
**Why this is wrong:**
- Testing is part of implementation, not optional follow-up
- TDD would have caught this
- Can't claim complete without tests
**The fix:**
```
TDD cycle:
1. Write failing test
2. Implement to pass
3. Refactor
4. THEN claim complete
```
## When Mocks Become Too Complex
**Warning signs:**
- Mock setup longer than test logic
- Mocking everything to make test pass
- Mocks missing methods real components have
- Test breaks when mock changes
**your human partner's question:** "Do we need to be using a mock here?"
**Consider:** Integration tests with real components often simpler than complex mocks
## TDD Prevents These Anti-Patterns
**Why TDD helps:**
1. **Write test first** → Forces you to think about what you're actually testing
2. **Watch it fail** → Confirms test tests real behavior, not mocks
3. **Minimal implementation** → No test-only methods creep in
4. **Real dependencies** → You see what the test actually needs before mocking
**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.
## Quick Reference
| Anti-Pattern | Fix |
|--------------|-----|
| Assert on mock elements | Test real component or unmock it |
| Test-only methods in production | Move to test utilities |
| Mock without understanding | Understand dependencies first, mock minimally |
| Incomplete mocks | Mirror real API completely |
| Tests as afterthought | TDD - tests first |
| Over-complex mocks | Consider integration tests |
## Red Flags
- Assertion checks for `*-mock` test IDs
- Methods only called in test files
- Mock setup is >50% of test
- Test fails when you remove mock
- Can't explain why mock is needed
- Mocking "just to be safe"
## The Bottom Line
**Mocks are tools to isolate, not things to test.**
If TDD reveals you're testing mock behavior, you've gone wrong.
Fix: Test real behavior or question why you're mocking at all.
@@ -1,5 +1,8 @@
--- ---
name: using-git-worktrees name: using-git-worktrees
surfaces: [claude-code, library]
summary: "Isolating work with git worktrees"
use_when: "Parallel/isolated work in a worktree"
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
--- ---
@@ -1,5 +1,8 @@
--- ---
name: using-superpowers name: using-superpowers
surfaces: [claude-code, library]
summary: "Meta: orienting in the superpowers set"
use_when: "Learning how to use the superpowers skills"
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
--- ---
@@ -1,5 +1,8 @@
--- ---
name: verification-before-completion name: verification-before-completion
surfaces: [claude-code, library]
summary: "Verify behavior before claiming done"
use_when: "Before marking any task complete"
description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
--- ---
@@ -1,5 +1,8 @@
--- ---
name: writing-plans name: writing-plans
surfaces: [claude-code, library]
summary: "Writing implementation plans"
use_when: "Planning substantial work before coding"
description: Use when you have a spec or requirements for a multi-step task, before touching code description: Use when you have a spec or requirements for a multi-step task, before touching code
--- ---
@@ -1,5 +1,8 @@
--- ---
name: writing-skills name: writing-skills
surfaces: [claude-code, library]
summary: "Authoring new SKILL.md skills"
use_when: "Creating a new reusable skill"
description: Use when creating new skills, editing existing skills, or verifying skills work before deployment description: Use when creating new skills, editing existing skills, or verifying skills work before deployment
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: tdd name: tdd
surfaces: [claude-code, library]
summary: "Test-Driven Development enforcement"
use_when: "Starting any new feature or fixing a bug"
description: Write failing tests first, then minimal code to pass. Non-negotiable for all new features and bug fixes. description: Write failing tests first, then minimal code to pass. Non-negotiable for all new features and bug fixes.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: telos-load name: telos-load
surfaces: [claude-code, library]
summary: "Load TELOS intention substrate at session start"
use_when: "Starting any koala session; before architectural decisions; CAD pipeline entry"
description: Load TELOS — the intention substrate (problems, goals, strategies, status) — into a session at start so every decision traces to a goal. Use when starting any koala session, before architectural decisions, or as the CAD pipeline entry point ("load TELOS", "what are we optimizing toward?"). description: Load TELOS — the intention substrate (problems, goals, strategies, status) — into a session at start so every decision traces to a goal. Use when starting any koala session, before architectural decisions, or as the CAD pipeline entry point ("load TELOS", "what are we optimizing toward?").
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: test-design name: test-design
surfaces: [claude-code, library]
summary: "Dave Farley test design scoring (Farley Index)"
use_when: "Reviewing or writing tests"
description: Evaluate test quality using Dave Farley's 8 Properties of Good Tests. Use when reviewing or writing tests to ensure they provide genuine verification. description: Evaluate test quality using Dave Farley's 8 Properties of Good Tests. Use when reviewing or writing tests to ensure they provide genuine verification.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: trainer name: trainer
surfaces: [claude-code, library]
summary: "Two-phase brain curation (score candidates, write past quality gate)"
use_when: "Periodic brain pruning and curation"
description: Two-phase brain curation — scan session logs to score candidate learnings (READ), then write the ones that pass the quality gate as generalized knowledge entries (WRITE). Use periodically to keep the brain useful, not noisy. description: Two-phase brain curation — scan session logs to score candidate learnings (READ), then write the ones that pass the quality gate as generalized knowledge entries (WRITE). Use periodically to keep the brain useful, not noisy.
--- ---
+3
View File
@@ -1,5 +1,8 @@
--- ---
name: user-stories name: user-stories
surfaces: [claude-code, library]
summary: "Elephant Carpaccio story slicing"
use_when: "Breaking down features"
description: Decompose problems into thin, deliverable user stories using Elephant Carpaccio and INVEST criteria. Use after problem analysis, before planning and implementation. description: Decompose problems into thin, deliverable user stories using Elephant Carpaccio and INVEST criteria. Use after problem analysis, before planning and implementation.
--- ---
+17 -4
View File
@@ -1,5 +1,8 @@
--- ---
name: web-shot name: web-shot
surfaces: [claude-code, library]
summary: "Screenshot a running web UI via a koala k3s Playwright job"
use_when: "Visual verification of a running UI when no local browser exists"
description: Screenshot any running web UI by spawning a one-shot Playwright Job on the koala k3s cluster. Auto-detects the calling host's Tailscale name so a dev server on flamingo:8099, iguana:3000 or koala:8181 just works. Use when the user asks for a screenshot, visual verification, "what does the UI look like", or any time chromium / firefox / a browser is requested but none is installed on the host. description: Screenshot any running web UI by spawning a one-shot Playwright Job on the koala k3s cluster. Auto-detects the calling host's Tailscale name so a dev server on flamingo:8099, iguana:3000 or koala:8181 just works. Use when the user asks for a screenshot, visual verification, "what does the UI look like", or any time chromium / firefox / a browser is requested but none is installed on the host.
--- ---
@@ -22,18 +25,28 @@ Screenshot a locally-running web UI without installing a browser on the agent's
## Usage ## Usage
`shot.sh` ships beside this SKILL.md, so resolve it from wherever the harness
installed the skill rather than from a checkout path — an absolute `~/dev/...`
went stale the first time the repo moved and cost a caller three hand-rolled
Job attempts before they found the working script (#26).
```bash ```bash
# Claude Code installs the plugin; Crush/Pi/OpenCode get ~/.agents/skills.
SHOT=$(ls ~/.claude/plugins/marketplaces/*/skills/web-shot/shot.sh \
~/.agents/skills/web-shot/shot.sh 2>/dev/null | head -1 || true)
[ -n "$SHOT" ] || { echo "web-shot not installed on this host: run install.sh or the plugin install" >&2; exit 1; }
# auto-detect Tailscale hostname, screenshot port 8099 root # auto-detect Tailscale hostname, screenshot port 8099 root
~/dev/.skills/web-shot/shot.sh --port 8099 "$SHOT" --port 8099
# explicit URL # explicit URL
~/dev/.skills/web-shot/shot.sh --url http://flamingo:5173 "$SHOT" --url http://flamingo:5173
# capture a search state too (types into input[name=q]) # capture a search state too (types into input[name=q])
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad "$SHOT" --port 8181 --search agentsquad
# different output dir # different output dir
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots "$SHOT" --port 3000 --out /tmp/shots
``` ```
## Flags ## Flags
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# Tests for bin/check-surfaces.
#
# The repo ships to three surfaces — the Claude Code plugin, the symlinked
# library (Crush/Pi/OpenCode) and claude.ai, which has no fetch path at all —
# and used to treat every skill as belonging to all of them. gitea-ci,
# playwright and web-shot cannot fire in a chat surface; a flat mirror installs
# skills that can never trigger and degrades selection for the ones that can
# (#29).
#
# The risk this gate exists for: a wrong `surfaces:` value silently removes a
# skill from a harness rather than erroring. So presence is not enough — values
# are validated against the allowed set.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/bin/check-surfaces"
pass=0
fail=0
WORK=${TMPDIR:-/tmp}/check_surfaces_test.$$
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT INT TERM
# fixture <name> <surfaces-line-or-empty> — a checkout holding one skill.
fixture() {
_f="$WORK/$1"
mkdir -p "$_f/skills/alpha"
if [ -n "$2" ]; then
printf -- '---\nname: alpha\n%s\ndescription: a\n---\n' "$2" > "$_f/skills/alpha/SKILL.md"
else
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
fi
printf '%s' "$_f"
}
# expect <rc> <label> <fixture> [needle]
expect() {
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
if [ "$_rc" -ne "$_want" ]; then
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
fi
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
fail=$((fail + 1)); printf ' FAIL %s (rc ok, report never says %s)\n%s\n' "$_label" "$_needle" "$_out"; return
fi
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
}
expect 0 'the default pair is accepted' "$(fixture ok 'surfaces: [claude-code, library]')"
expect 0 'all three surfaces are accepted' "$(fixture three 'surfaces: [claude-code, library, chat]')"
expect 0 'a single surface is accepted' "$(fixture one 'surfaces: [chat]')"
expect 1 'a missing surfaces field fails' "$(fixture missing '')" 'alpha'
expect 1 'an unknown surface value fails' "$(fixture bogus 'surfaces: [claude-code, desktop]')" 'desktop'
expect 1 'an empty surfaces list fails' "$(fixture empty 'surfaces: []')" 'alpha'
printf '\ncheck_surfaces: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
+113 -22
View File
@@ -37,11 +37,21 @@ new_fixture() {
printf '%s' "$_f" printf '%s' "$_f"
} }
# wire_clean <fixture> — the correct wiring: one canonical link, harnesses # wire_clean <fixture> [extra-source-dir] — the correct wiring since #27: the
# pointing at it. # canonical location is a REAL directory of per-skill links, fed by one or more
# sources, with each harness keeping its single directory link to it.
wire_clean() { wire_clean() {
mkdir -p "$1/home/.agents" "$1/home/.config/crush" "$1/home/.config/opencode" mkdir -p "$1/home/.agents/skills" "$1/home/.config/crush" "$1/home/.config/opencode"
ln -sfn "$1/ck/skills" "$1/home/.agents/skills" for _d in "$1"/ck/skills/*/; do
ln -sfn "${_d%/}" "$1/home/.agents/skills/$(basename "$_d")"
done
printf '%s\n' "$1/ck/skills" > "$1/home/.agents/skills-sources"
if [ -n "${2:-}" ]; then
for _d in "$2"/*/; do
ln -sfn "${_d%/}" "$1/home/.agents/skills/$(basename "$_d")"
done
printf '%s\n' "$2" >> "$1/home/.agents/skills-sources"
fi
ln -sfn "$1/home/.agents/skills" "$1/home/.config/crush/skills" ln -sfn "$1/home/.agents/skills" "$1/home/.config/crush/skills"
ln -sfn "$1/home/.agents/skills" "$1/home/.config/opencode/skills" ln -sfn "$1/home/.agents/skills" "$1/home/.config/opencode/skills"
} }
@@ -79,27 +89,50 @@ assert_clean "$f" 'clean fixture exits 0'
# --- 2. dangling link (the 243-link case, gitignored and invisible) ---------- # --- 2. dangling link (the 243-link case, gitignored and invisible) ----------
f=$(new_fixture dangling); wire_clean "$f" f=$(new_fixture dangling); wire_clean "$f"
ln -sfn "$f/ck/skills-that-moved" "$f/home/.agents/skills" ln -sfn "$f/ck/skills/vanished" "$f/home/.agents/skills/alpha"
assert_fails "$f" 'dangling' 'dangling canonical link is reported' assert_fails "$f" 'dangling' 'dangling per-skill link is reported'
# --- 3. resolves, but to a DIFFERENT checkout (the silent-stale case) -------- # --- 3. resolves, but into a source nobody configured (silent-stale case) ----
# Worse than dangling: it works, so nothing complains, while serving content # Worse than dangling: it works, so nothing complains, while serving content
# from a checkout that is months behind. # from a checkout that is months behind. Since #27 a link may legitimately
# point outside the checkout — but only into a source the installer recorded.
f=$(new_fixture stale) f=$(new_fixture stale)
mkdir -p "$f/other/skills/alpha" "$f/other/skills/beta" mkdir -p "$f/other/alpha"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/skills/alpha/SKILL.md" printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/alpha/SKILL.md"
printf -- '---\nname: beta\ndescription: b\n---\n' > "$f/other/skills/beta/SKILL.md"
wire_clean "$f" wire_clean "$f"
ln -sfn "$f/other/skills" "$f/home/.agents/skills" ln -sfn "$f/other/alpha" "$f/home/.agents/skills/alpha"
assert_fails "$f" 'outside' 'link into a foreign checkout is reported' assert_fails "$f" 'unknown source' 'link into an unconfigured source is reported'
# --- 4. count mismatch (a harness seeing fewer skills than the checkout) ----- # --- 3b. a link into a CONFIGURED extra source is healthy -------------------
f=$(new_fixture count) # The whole point of #27: Anthropic's official set alongside this library.
mkdir -p "$f/home/.config/crush/skills/alpha" f=$(new_fixture multisource)
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md" mkdir -p "$f/other/xlsx"
mkdir -p "$f/home/.agents" printf -- '---\nname: xlsx\ndescription: x\n---\n' > "$f/other/xlsx/SKILL.md"
wire_clean "$f" "$f/other"
assert_clean "$f" 'a link into a configured extra source is healthy'
# --- 3c. the pre-#27 single-link wiring is named, not mistaken for damage ----
f=$(new_fixture legacy)
mkdir -p "$f/home/.agents" "$f/home/.config/crush" "$f/home/.config/opencode"
ln -sfn "$f/ck/skills" "$f/home/.agents/skills" ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
assert_fails "$f" 'count' 'skill-count mismatch is reported' ln -sfn "$f/home/.agents/skills" "$f/home/.config/crush/skills"
ln -sfn "$f/home/.agents/skills" "$f/home/.config/opencode/skills"
assert_fails "$f" 'single-link' 'legacy single-link wiring is reported as such'
# --- 4. a skill in the checkout with no link anywhere ------------------------
# The multi-source replacement for the old count check: with per-skill links a
# skill can be missing individually, which a total count would hide if an extra
# source happened to supply the same number.
f=$(new_fixture missing); wire_clean "$f"
rm -f "$f/home/.agents/skills/beta"
assert_fails "$f" 'beta' 'a checkout skill with no link is reported'
# ...but only for skills that ship to the library. A chat-only skill has no
# business in a Crush directory, so its absence is correct, not drift (#29).
f=$(new_fixture chatonly); wire_clean "$f"
mkdir -p "$f/ck/skills/gamma"
printf -- '---\nname: gamma\nsurfaces: [chat]\ndescription: g\n---\n' > "$f/ck/skills/gamma/SKILL.md"
assert_clean "$f" 'a chat-only skill needs no library link'
# --- 5. stray entry (the loose secret-hygiene.md that loaded nowhere) -------- # --- 5. stray entry (the loose secret-hygiene.md that loaded nowhere) --------
f=$(new_fixture stray); wire_clean "$f" f=$(new_fixture stray); wire_clean "$f"
@@ -119,9 +152,9 @@ assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
# resolution target, which is true but useless. It must say the dir is not a # resolution target, which is true but useless. It must say the dir is not a
# link at all, because the fix (move it aside, re-run install.sh) is different # link at all, because the fix (move it aside, re-run install.sh) is different
# from the fix for a link into a foreign checkout. # from the fix for a link into a foreign checkout.
f=$(new_fixture realdir) f=$(new_fixture realdir); wire_clean "$f"
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha" rm -f "$f/home/.config/crush/skills"
ln -sfn "$f/ck/skills" "$f/home/.agents/skills" mkdir -p "$f/home/.config/crush/skills/alpha"
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md" printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"' assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"'
@@ -133,5 +166,63 @@ mkdir -p "$f/home/.claude/skills"
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished" ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported' assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported'
# --- 8. Claude Code plugin pinned to an older version than is available -----
# The plugin is the ONLY delivery channel for Claude Code, and Claude Code
# gates updates on plugin.json's version field. A host that never picked the
# new one up loads every skill successfully — just the wrong generation. Found
# live on koala 2026-08-29: installed 0.6.0, marketplace 0.7.0, repo 0.8.5,
# and doctor said "clean" (#30).
# installed_state <fixture> <version> — the Claude Code install record.
installed_state() {
mkdir -p "$1/home/.claude/plugins"
cat > "$1/home/.claude/plugins/installed_plugins.json" <<JSON
{
"version": 2,
"plugins": {
"skills@mathias": [
{
"scope": "user",
"installPath": "/somewhere/$2",
"version": "$2"
}
]
}
}
JSON
}
# manifest <dir> <version> — a .claude-plugin/plugin.json under <dir>.
manifest() {
mkdir -p "$1/.claude-plugin"
printf '{\n "name": "skills",\n "version": "%s"\n}\n' "$2" > "$1/.claude-plugin/plugin.json"
}
f=$(new_fixture pluginmatch); wire_clean "$f"
installed_state "$f" 0.8.0
manifest "$f/ck" 0.8.0
assert_clean "$f" 'plugin on the current version stays silent'
f=$(new_fixture pluginstale); wire_clean "$f"
installed_state "$f" 0.7.0
manifest "$f/ck" 0.8.0
assert_fails "$f" '0.7.0' 'stale plugin version is reported'
assert_fails "$f" '0.8.0' 'stale plugin report names the available version'
# A host that only uses the symlink path has no install record at all. That is
# a valid configuration and must not fail.
f=$(new_fixture pluginabsent); wire_clean "$f"
manifest "$f/ck" 0.8.0
assert_clean "$f" 'no Claude Code install record is not a fault'
# The marketplace clone is what Claude Code compares against, so a newer
# version there means an update is waiting even when the local checkout agrees
# with what is installed.
f=$(new_fixture pluginmarket); wire_clean "$f"
installed_state "$f" 0.7.0
manifest "$f/ck" 0.7.0
manifest "$f/home/.claude/plugins/marketplaces/mathias" 0.9.0
assert_fails "$f" '0.9.0' 'a newer marketplace clone is reported'
printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail" printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ] [ "$fail" -eq 0 ]
+113
View File
@@ -0,0 +1,113 @@
#!/bin/sh
# Tests for bin/generate-index.
#
# SKILLS_INDEX.md was maintained by hand and drifted: two skills shipped with
# no row at all (#15), because their rows lived on branches squash-merged after
# a directory reorg. A gate caught drift after the fact; generating the table
# from frontmatter removes the class entirely (#29) — the curated summary and
# trigger now live next to the skill they describe, which is the same argument
# that made a hand-maintained index the wrong shape.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/bin/generate-index"
pass=0
fail=0
WORK=${TMPDIR:-/tmp}/generate_index_test.$$
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT INT TERM
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
no() { fail=$((fail + 1)); printf ' FAIL %s\n%s\n' "$1" "${2:-}"; }
# skill <root> <name> <surfaces> [summary] [use_when]
skill() {
mkdir -p "$1/skills/$2"
{
printf -- '---\nname: %s\nsurfaces: %s\n' "$2" "$3"
[ $# -lt 4 ] || printf 'summary: "%s"\n' "$4"
[ $# -lt 5 ] || printf 'use_when: "%s"\n' "$5"
printf -- 'description: d\n---\n'
} > "$1/skills/$2/SKILL.md"
}
# fixture <name> — a root with an index carrying the generated-table markers.
fixture() {
_f="$WORK/$1"
mkdir -p "$_f"
cat > "$_f/SKILLS_INDEX.md" <<'IDX'
# Engineering Skills Index
<!-- BEGIN skills-table -->
<!-- END skills-table -->
## Wiring
prose that must survive regeneration
IDX
printf '%s' "$_f"
}
f=$(fixture basic)
skill "$f" beta '[claude-code, library]' 'B summary' 'when B'
skill "$f" alpha '[chat]' 'A summary' 'when A'
out=$(sh "$SUT" "$f" 2>&1); rc=$?
if [ "$rc" -ne 0 ]; then
no 'generates a table' "$out"
elif ! grep -q '^| `alpha` | A summary | when A |' "$f/SKILLS_INDEX.md"; then
no 'generates a table' "$(cat "$f/SKILLS_INDEX.md")"
elif ! grep -q 'prose that must survive regeneration' "$f/SKILLS_INDEX.md"; then
no 'content outside the markers survives' "$(cat "$f/SKILLS_INDEX.md")"
elif [ "$(grep -n '`alpha`' "$f/SKILLS_INDEX.md" | cut -d: -f1)" -gt "$(grep -n '`beta`' "$f/SKILLS_INDEX.md" | cut -d: -f1)" ]; then
no 'rows are sorted' "$(cat "$f/SKILLS_INDEX.md")"
else
ok 'generates a sorted table and leaves the rest of the file alone'
fi
# The surfaces a skill ships to are the point of #29, so they are visible.
grep -q 'chat' "$f/SKILLS_INDEX.md" \
&& ok 'the row names the surfaces' \
|| no 'the row names the surfaces' "$(cat "$f/SKILLS_INDEX.md")"
# Idempotent: a second run changes nothing.
before=$(cat "$f/SKILLS_INDEX.md")
sh "$SUT" "$f" >/dev/null 2>&1
[ "$before" = "$(cat "$f/SKILLS_INDEX.md")" ] \
&& ok 'regeneration is idempotent' \
|| no 'regeneration is idempotent'
# --check is the CI gate: clean tree passes.
sh "$SUT" --check "$f" >/dev/null 2>&1 \
&& ok '--check passes on a generated index' \
|| no '--check passes on a generated index'
# ...and fails once a skill appears without regenerating.
skill "$f" gamma '[library]' 'G summary' 'when G'
if sh "$SUT" --check "$f" >/dev/null 2>&1; then
no '--check fails on a stale index'
else
ok '--check fails on a stale index'
fi
# A skill with no curated summary is an error, not an empty cell: the table
# cell is what a reader scans, and a blank one is worse than a missing row.
f=$(fixture nosummary)
skill "$f" alpha '[library]'
out=$(sh "$SUT" "$f" 2>&1); rc=$?
if [ "$rc" -eq 0 ]; then
no 'a missing summary is an error' "$out"
elif ! printf '%s' "$out" | grep -q alpha; then
no 'a missing summary is an error' "$out"
else
ok 'a missing summary is an error naming the skill'
fi
printf '\ngenerate_index: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
+187
View File
@@ -0,0 +1,187 @@
#!/bin/sh
# Tests for install.sh's wiring model.
#
# The regression these exist for: install.sh wired each harness with ONE
# directory symlink to this checkout, which quietly encoded "this checkout is
# the only source of skills on the host". On flamingo the Crush directory held
# Anthropic's official skill set, and the only way to install this library was
# to displace it — a real capability loss dressed up as a tidy-up (#27).
#
# Fixtures drive install.sh entirely through env seams, so no test touches the
# real host wiring.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/install.sh"
pass=0
fail=0
WORK=${TMPDIR:-/tmp}/install_test.$$
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT INT TERM
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
no() { fail=$((fail + 1)); printf ' FAIL %s\n%s\n' "$1" "${2:-}"; }
# skill <dir> <name> — a minimal valid skill.
skill() {
mkdir -p "$1/$2"
printf -- '---\nname: %s\ndescription: d\n---\n' "$2" > "$1/$2/SKILL.md"
}
# new_fixture <name> [extra-source-names...] — checkout with alpha+beta.
new_fixture() {
_f="$WORK/$1"
mkdir -p "$_f/ck/skills" "$_f/home"
skill "$_f/ck/skills" alpha
skill "$_f/ck/skills" beta
printf '%s' "$_f"
}
# run_install <fixture> [extra-sources] — echoes output, sets $rc.
run_install() {
_f=$1
_extra=${2:-}
_out=$(HOME="$_f/home" \
SKILLS_SKIP_FETCH=1 \
SKILLS_CHECKOUT_DIR="$_f/ck" \
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
SKILLS_EXTRA_SOURCES="$_extra" \
bash "$SUT" 2>&1)
rc=$?
printf '%s' "$_out"
}
# --- 1. the new shape: a real directory of per-skill links -------------------
f=$(new_fixture fresh)
out=$(run_install "$f")
A="$f/home/.agents/skills"
if [ ! -d "$A" ] || [ -L "$A" ]; then
no 'agents dir is a real directory' "$out"
elif [ ! -L "$A/alpha" ] || [ ! -L "$A/beta" ]; then
no 'each skill is its own link' "$out"
elif [ ! -L "$f/home/.config/crush/skills" ] || [ ! -L "$f/home/.config/opencode/skills" ]; then
no 'harnesses keep one directory link each' "$out"
else
ok 'fresh install builds per-skill links under a real agents dir'
fi
# --- 2. a second source is served WITHOUT displacing the first --------------
f=$(new_fixture twosource)
mkdir -p "$f/other"
skill "$f/other" xlsx
out=$(run_install "$f" "$f/other")
if [ -L "$f/home/.agents/skills/xlsx" ] && [ -L "$f/home/.agents/skills/alpha" ]; then
ok 'a second source is linked alongside the checkout'
else
no 'a second source is linked alongside the checkout' "$out"
fi
# --- 3. name collisions are REPORTED, never silently resolved ---------------
# Two sources offering `code-review` is a real possibility, and picking one
# quietly is how stale content wins.
f=$(new_fixture collide)
mkdir -p "$f/other"
skill "$f/other" alpha
out=$(run_install "$f" "$f/other")
if printf '%s' "$out" | grep -qi 'collision' \
&& printf '%s' "$out" | grep -q "$f/other/alpha"; then
target=$(readlink "$f/home/.agents/skills/alpha")
if [ "$target" = "$f/ck/skills/alpha" ]; then
ok 'a name collision is reported and the checkout wins'
else
no 'a name collision is reported and the checkout wins' "resolved to $target"
fi
else
no 'a name collision is reported and the checkout wins' "$out"
fi
# --- 4. removing a skill from a source removes its link ---------------------
# Without this the previous generation's problem returns in a new shape: links
# that resolve to nothing, invisible to git.
f=$(new_fixture prune)
out=$(run_install "$f")
rm -rf "$f/ck/skills/beta"
out=$(run_install "$f")
if [ -e "$f/home/.agents/skills/beta" ] || [ -L "$f/home/.agents/skills/beta" ]; then
no 'a removed skill loses its link' "$out"
else
ok 'a removed skill loses its link'
fi
# --- 5. migration from the old single-link shape ----------------------------
f=$(new_fixture migrate)
mkdir -p "$f/home/.agents"
ln -s "$f/ck/skills" "$f/home/.agents/skills"
out=$(run_install "$f")
if [ -L "$f/home/.agents/skills" ]; then
no 'the old single-link wiring is migrated in place' "$out"
elif [ -L "$f/home/.agents/skills/alpha" ]; then
ok 'the old single-link wiring is migrated in place'
else
no 'the old single-link wiring is migrated in place' "$out"
fi
# --- 6. unmanaged real content in the agents dir is never deleted -----------
f=$(new_fixture protect)
mkdir -p "$f/home/.agents/skills"
skill "$f/home/.agents/skills" handwritten
out=$(run_install "$f")
if [ -f "$f/home/.agents/skills/handwritten/SKILL.md" ] \
&& printf '%s' "$out" | grep -qi 'unmanaged'; then
ok 'unmanaged content is reported and left in place'
else
no 'unmanaged content is reported and left in place' "$out"
fi
# --- 7. the source list is recorded for doctor ------------------------------
f=$(new_fixture manifest)
mkdir -p "$f/other"
skill "$f/other" xlsx
out=$(run_install "$f" "$f/other")
if [ -f "$f/home/.agents/skills-sources" ] \
&& grep -q "$f/other" "$f/home/.agents/skills-sources" \
&& grep -q "$f/ck/skills" "$f/home/.agents/skills-sources"; then
ok 'configured sources are recorded for doctor'
else
no 'configured sources are recorded for doctor' "$out"
fi
# --- 8. surfaces: the library link set is not "every skill" -----------------
# claude.ai has no shell, no cluster and no checkout, so a chat-only skill
# linked into Crush is a skill that can never fire, competing for selection
# with ones that can (#29).
f=$(new_fixture surfaces)
mkdir -p "$f/ck/skills/chatonly"
printf -- '---\nname: chatonly\nsurfaces: [chat]\ndescription: c\n---\n' > "$f/ck/skills/chatonly/SKILL.md"
printf -- '---\nname: alpha\nsurfaces: [claude-code, library]\ndescription: a\n---\n' > "$f/ck/skills/alpha/SKILL.md"
out=$(run_install "$f")
if [ -L "$f/home/.agents/skills/alpha" ] && [ ! -e "$f/home/.agents/skills/chatonly" ]; then
ok 'a skill that does not ship to the library is not linked'
else
no 'a skill that does not ship to the library is not linked' "$out"
fi
# A foreign source has no reason to know this convention, so no declaration
# means "link it" — otherwise adding Anthropic's set would link nothing.
f=$(new_fixture foreign)
mkdir -p "$f/other/xlsx"
printf -- '---\nname: xlsx\ndescription: x\n---\n' > "$f/other/xlsx/SKILL.md"
out=$(run_install "$f" "$f/other")
if [ -L "$f/home/.agents/skills/xlsx" ]; then
ok 'a source with no surfaces convention is linked as-is'
else
no 'a source with no surfaces convention is linked as-is' "$out"
fi
printf '\ninstall: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
# Tests for bin/newer-version.
#
# Extracted from bin/doctor (#30) once `task check` needed the same comparison
# (#34): check asserted plugin.json == latest tag, which is red on any checkout
# that has not fetched the tag the CI tagger just pushed. The assertion it
# actually wants is "not behind", and ordering needs a real comparison.
set -u
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
SUT="$ROOT/bin/newer-version"
pass=0
fail=0
if [ ! -f "$SUT" ]; then
printf 'FAIL: %s does not exist\n' "$SUT"
exit 1
fi
# expect <winner> <a> <b> <label>
expect() {
_got=$(sh "$SUT" "$2" "$3")
if [ "$_got" = "$1" ]; then
pass=$((pass + 1)); printf ' ok %s\n' "$4"
else
fail=$((fail + 1)); printf ' FAIL %s (newer-version %s %s -> %s, want %s)\n' "$4" "$2" "$3" "$_got" "$1"
fi
}
expect 0.9.0 0.9.0 0.8.5 'higher minor wins, left'
expect 0.9.0 0.8.5 0.9.0 'higher minor wins, right'
expect 0.8.10 0.8.10 0.8.9 'numeric compare, not lexical — 10 beats 9'
expect 1.0.0 1.0.0 0.99.99 'major dominates'
expect 0.8.5 0.8.5 0.8.5 'equal returns the value'
expect 0.9.1 0.9.1 0.9 'a missing component counts as zero'
expect 0.9 0.9 0.9.0 'equal with a missing component returns the left, per contract'
printf '\nnewer_version: %d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]
+11
View File
@@ -61,6 +61,17 @@ expect deny 'op read "op://vault/item/field"' 'op read'
expect deny 'env | grep -i gitea' 'env piped to grep' expect deny 'env | grep -i gitea' 'env piped to grep'
expect deny 'env' 'bare env' expect deny 'env' 'bare env'
expect deny 'printenv' 'printenv' expect deny 'printenv' 'printenv'
expect deny 'env -i | grep -i token' 'env -i with no command is still a dump'
expect deny 'env FOO=bar' 'env with only assignments and no command is a dump'
# --- allow: `env VAR=x cmd` / `env -u VAR cmd` RUN a command (#25) ---
# These print nothing themselves; a pipe into grep filters the command's own
# output, not the environment. Blocking them cost a documented workaround
# ((unset VAR; cmd)) on the 1Password write path, which is the shape below.
expect allow 'env -u ANTHROPIC_API_KEY claude --debug -p "ok" 2>&1 | grep -i plugin' 'env -u VAR runs a command'
expect allow 'env FOO=bar ./script.sh | grep x' 'env VAR=x runs a command'
expect allow 'env -u OP_SERVICE_ACCOUNT_TOKEN op item create - < item.json' 'env -u on the 1P write path'
expect deny 'env -u FOO ./run.sh; env | grep -i secret' 'a real dump later in the same line still denies'
# --- deny: kubernetes secret material --- # --- deny: kubernetes secret material ---
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml' expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'