Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7775aac57 | ||
|
|
c6787e04d7 | ||
|
|
90a58f0b9e | ||
|
|
15737321ac | ||
|
|
025f01aa80 | ||
|
|
eee62e4fc0 | ||
|
|
a2afb35d1d | ||
|
|
ad17a7c6c0 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"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.",
|
||||
"version": "0.8.4",
|
||||
"version": "0.10.0",
|
||||
"author": {
|
||||
"name": "Mathias",
|
||||
"url": "https://git.d-ma.be/mathias"
|
||||
|
||||
+41
-16
@@ -94,26 +94,51 @@ jobs:
|
||||
git config user.name "${GITEA_ACTOR}"
|
||||
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
|
||||
|
||||
# Claude Code gates plugin updates on plugin.json's version field:
|
||||
# if it does not move, installed hosts never see the new tag. Bump
|
||||
# it in the same commit train so the tag and the manifest agree —
|
||||
# `task check` enforces that equality on every run.
|
||||
# The tag goes up FIRST and on its own. It names the commit this run
|
||||
# checked out and can never be rejected, however far main has moved.
|
||||
git tag -a "$TAG" -m "release $TAG"
|
||||
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
|
||||
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"
|
||||
git add "$manifest"
|
||||
# The trailer below stops this push from re-triggering the tagger.
|
||||
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`
|
||||
# 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"
|
||||
else
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
echo "pushed $TAG"
|
||||
if git push origin HEAD:main; then
|
||||
echo "bumped $manifest to $version"
|
||||
exit 0
|
||||
fi
|
||||
echo "main moved while bumping — retry $attempt"
|
||||
done
|
||||
|
||||
echo "could not land the manifest bump after 3 attempts" >&2
|
||||
exit 1
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Idempotent. It clones or fast-forwards the checkout, then creates **three**
|
||||
symlinks — one directory link per harness, never one per skill:
|
||||
Idempotent. It clones or fast-forwards the checkout, then populates one
|
||||
**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/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
|
||||
|
||||
@@ -74,6 +100,7 @@ default to point into `~/dev`.
|
||||
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
|
||||
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/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.
|
||||
|
||||
|
||||
+64
-53
@@ -2,50 +2,56 @@
|
||||
|
||||
This index lists all available engineering skills. Load the full SKILL.md on demand.
|
||||
|
||||
| Skill | Description | Use when |
|
||||
|-------|-------------|----------|
|
||||
| `tdd` | Test-Driven Development enforcement | Starting any new feature or fixing a bug |
|
||||
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code |
|
||||
| `solid` | SOLID principles with Go examples | Design review, architecture decisions |
|
||||
| `code-review` | Code smell detection + Go-specific checklist | Pre-merge review |
|
||||
| `refactoring` | Systematic refactoring methodology (Fowler) | When improving existing code |
|
||||
| `test-design` | Dave Farley test design scoring (Farley Index) | Reviewing or writing tests |
|
||||
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring |
|
||||
| `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task |
|
||||
| `user-stories` | Elephant Carpaccio story slicing | Breaking down features |
|
||||
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria |
|
||||
| `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements |
|
||||
| `planning` | Planning and task breakdown | Sprint planning, task decomposition |
|
||||
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
|
||||
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
|
||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
||||
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature |
|
||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
||||
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
||||
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
||||
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
|
||||
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
|
||||
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists |
|
||||
| `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 |
|
||||
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
|
||||
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
|
||||
| `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 |
|
||||
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents |
|
||||
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically |
|
||||
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete |
|
||||
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review |
|
||||
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments |
|
||||
| `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR |
|
||||
| `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree |
|
||||
| `superpowers-writing-skills` | Authoring new SKILL.md skills | Creating a new reusable skill |
|
||||
| `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills |
|
||||
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
|
||||
the SKILL.md, not the table.
|
||||
|
||||
<!-- BEGIN skills-table -->
|
||||
| Skill | Description | Use when | Surfaces |
|
||||
|-------|-------------|----------|----------|
|
||||
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria | 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 | claude-code, library |
|
||||
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code | claude-code, library |
|
||||
| `code-review` | Code smell detection + Go-specific checklist | Pre-merge review | claude-code, library |
|
||||
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring | claude-code, library |
|
||||
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious | claude-code, library |
|
||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | claude-code, library |
|
||||
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index | claude-code, library, chat |
|
||||
| `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 | claude-code, library |
|
||||
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | claude-code, library |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `planning` | Planning and task breakdown | Sprint planning, task decomposition | claude-code, library |
|
||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | claude-code, library |
|
||||
| `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task | claude-code, library |
|
||||
| `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 | 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 | claude-code, library |
|
||||
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost | claude-code, library |
|
||||
| `solid` | SOLID principles with Go examples | Design review, architecture decisions | claude-code, library |
|
||||
| `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements | 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 | claude-code, library |
|
||||
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas | claude-code, library |
|
||||
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents | claude-code, library |
|
||||
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion | claude-code, library |
|
||||
| `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR | claude-code, library |
|
||||
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments | claude-code, library |
|
||||
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review | claude-code, library |
|
||||
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents | claude-code, library |
|
||||
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically | claude-code, library |
|
||||
| `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree | claude-code, library |
|
||||
| `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills | claude-code, library |
|
||||
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete | claude-code, library |
|
||||
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding | claude-code, library |
|
||||
| `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
|
||||
|
||||
@@ -70,17 +76,21 @@ everything load twice.
|
||||
|
||||
### Crush, Pi, OpenCode — run `install.sh`
|
||||
|
||||
These have no plugin system, so they get symlinks — but **one directory link
|
||||
each**, never one per skill:
|
||||
These have no plugin system, so they get symlinks — but through one managed
|
||||
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/opencode/skills -> ~/.agents/skills
|
||||
```
|
||||
|
||||
Adding a skill therefore wires it everywhere with no re-run, and a broken link
|
||||
is one broken link instead of 42.
|
||||
Total links are N + one per harness. `SKILLS_EXTRA_SOURCES` (colon-separated
|
||||
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
|
||||
@@ -94,10 +104,11 @@ absolute link stored there is broken on one side by construction. See #23.
|
||||
|
||||
### Check a host
|
||||
|
||||
`task doctor` (or `bin/doctor`) reports dangling links, links resolving into a
|
||||
foreign checkout, count mismatches, stray entries, and leftover Claude
|
||||
symlinks. It also runs automatically at session start via the plugin, so drift
|
||||
announces itself instead of waiting to be found.
|
||||
`task doctor` (or `bin/doctor`) reports dangling links, links into a source
|
||||
nobody configured, skills in the checkout with no link, stray entries, leftover
|
||||
Claude symlinks, and a Claude Code plugin sitting on an older generation than
|
||||
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
|
||||
|
||||
|
||||
+25
-8
@@ -61,8 +61,13 @@ tasks:
|
||||
done
|
||||
exit $fail
|
||||
|
||||
index:
|
||||
desc: 'Regenerate SKILLS_INDEX.md''s table from skill frontmatter.'
|
||||
cmds:
|
||||
- "'{{.TASKFILE_DIR}}/bin/generate-index' '{{.TASKFILE_DIR}}'"
|
||||
|
||||
check:
|
||||
desc: 'Structure gate — layout, frontmatter, manifest/tag parity. Runs in CI.'
|
||||
desc: 'Structure gate — layout, frontmatter, surfaces, generated index, manifest/tag parity. Runs in CI.'
|
||||
cmds:
|
||||
- |
|
||||
fail=0
|
||||
@@ -94,18 +99,30 @@ tasks:
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
# 3. SKILLS_INDEX.md lists exactly the skills that exist. Delegated
|
||||
# to bin/check-index so the rule is covered by
|
||||
# tests/check_index_test.sh rather than living untested here.
|
||||
sh '{{.TASKFILE_DIR}}/bin/check-index' '{{.TASKFILE_DIR}}' || fail=1
|
||||
# 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. Plugin manifest version must match the latest tag, or installers
|
||||
# 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).
|
||||
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
||||
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
||||
tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
|
||||
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ]; then
|
||||
echo "ERROR: plugin.json version ($manifest_v) != latest tag ($tag_v)" >&2
|
||||
# Ordering, not equality. The fault this guards is the manifest
|
||||
# failing to MOVE — a release tag whose manifest still names the old
|
||||
# version, so no installed host ever sees the update. A manifest
|
||||
# ahead of the newest tag this checkout knows about is the normal
|
||||
# state between the tagger's bump commit and a tag fetch (#34).
|
||||
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ] \
|
||||
&& [ "$(sh '{{.TASKFILE_DIR}}/bin/newer-version' "$manifest_v" "$tag_v")" = "$tag_v" ]; then
|
||||
echo "ERROR: plugin.json version ($manifest_v) is behind the latest tag ($tag_v) — installed hosts never see the update" >&2
|
||||
fail=1
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/sh
|
||||
# check-index — asserts SKILLS_INDEX.md lists exactly the skills that exist.
|
||||
#
|
||||
# The index is hand-maintained and drifts silently in both directions: a skill
|
||||
# ships with no row (`dream` and `experiment-spec` did — their rows lived on
|
||||
# branches squash-merged after a directory reorg, so the SKILL.md landed and
|
||||
# the row did not, #15), or a row outlives the skill it names. Neither errors
|
||||
# anywhere; the library just misreports itself to every harness that reads it.
|
||||
#
|
||||
# Usage: check-index [checkout-root] (defaults to this script's repo root)
|
||||
|
||||
set -u
|
||||
|
||||
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
|
||||
INDEX="$ROOT/SKILLS_INDEX.md"
|
||||
fail=0
|
||||
|
||||
if [ ! -f "$INDEX" ]; then
|
||||
echo "ERROR: missing SKILLS_INDEX.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A row is a table line whose first cell is the skill name in backticks.
|
||||
listed=$(sed -n 's/^|[[:space:]]*`\([^`]*\)`.*/\1/p' "$INDEX" | sort -u)
|
||||
|
||||
for dir in "$ROOT"/skills/*/; do
|
||||
name=$(basename "$dir")
|
||||
[ -f "$dir/SKILL.md" ] || continue
|
||||
printf '%s\n' "$listed" | grep -qx "$name" || {
|
||||
echo "ERROR: skills/$name has no row in SKILLS_INDEX.md" >&2
|
||||
fail=1
|
||||
}
|
||||
done
|
||||
|
||||
for name in $listed; do
|
||||
[ -f "$ROOT/skills/$name/SKILL.md" ] || {
|
||||
echo "ERROR: SKILLS_INDEX.md lists '$name', which has no skills/$name/SKILL.md" >&2
|
||||
fail=1
|
||||
}
|
||||
done
|
||||
|
||||
exit $fail
|
||||
Executable
+49
@@ -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
@@ -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
|
||||
# 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".
|
||||
AGENTS_DIR="$HOME/.agents/skills"
|
||||
SOURCES_FILE="$(dirname -- "$AGENTS_DIR")/$(basename -- "$AGENTS_DIR")-sources"
|
||||
|
||||
if [ -n "${DOCTOR_CHECKOUT:-}" ]; then
|
||||
CHECKOUT="$DOCTOR_CHECKOUT"
|
||||
elif [ -d "$HOME/.agents/skills" ]; then
|
||||
# Resolve the link FIRST, then take the parent. "cd X/.." resolves ".."
|
||||
# lexically against the link path, which lands back in ~/.agents.
|
||||
_resolved=$(CDPATH= cd -- "$HOME/.agents/skills" 2>/dev/null && pwd -P)
|
||||
elif [ -f "$SOURCES_FILE" ]; then
|
||||
# Since #27 the canonical location is a real directory fed by several
|
||||
# sources, so it can no longer be resolved to a checkout. install.sh records
|
||||
# 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")
|
||||
else
|
||||
CHECKOUT="$SELF_CHECKOUT"
|
||||
@@ -66,9 +75,26 @@ add() {
|
||||
}
|
||||
|
||||
# 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() {
|
||||
[ -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 ----------------------------------------------------
|
||||
@@ -95,10 +121,54 @@ for entry in "$SKILLS_DIR"/*; do
|
||||
fi
|
||||
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 ---------------------------------------------------------
|
||||
# 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 \
|
||||
"$HOME/.agents/skills" \
|
||||
"$HOME/.config/crush/skills" \
|
||||
"$HOME/.config/opencode/skills"
|
||||
do
|
||||
@@ -110,32 +180,23 @@ do
|
||||
fi
|
||||
|
||||
# A real directory where a link belongs holds content nothing manages: it
|
||||
# cannot receive new skills and silently shadows the checkout. Distinguished
|
||||
# from the foreign-checkout case below because the fix differs — move it
|
||||
# aside and re-run install.sh, rather than re-point a link.
|
||||
# cannot receive new skills and silently shadows the canonical dir. Since
|
||||
# #27 the fix is not only "move it aside" — it can be added as an extra
|
||||
# source instead, which is how flamingo keeps the official Anthropic set.
|
||||
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
|
||||
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)
|
||||
canonical=$(CDPATH= cd -- "$SKILLS_DIR" 2>/dev/null && pwd -P)
|
||||
if [ -n "$resolved" ] && [ "$resolved" != "$canonical" ]; then
|
||||
add FAIL outside "$dir resolves to $resolved, outside the active checkout $canonical"
|
||||
continue
|
||||
fi
|
||||
|
||||
got=$(count_skills "$dir")
|
||||
if [ "$got" != "$EXPECTED" ]; then
|
||||
add FAIL count "$dir sees $got skills, checkout has $EXPECTED"
|
||||
canonical=$(CDPATH= cd -- "$AGENTS_DIR" 2>/dev/null && pwd -P)
|
||||
if [ -n "$resolved" ] && [ -n "$canonical" ] && [ "$resolved" != "$canonical" ]; then
|
||||
add FAIL outside "$dir resolves to $resolved, not the canonical $canonical"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 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"
|
||||
do
|
||||
[ -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"
|
||||
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 -----------------------------------------------------------------
|
||||
if [ "$JSON" -eq 1 ]; then
|
||||
printf '{"checkout":"%s","expected":%s,"findings":[%s]}\n' \
|
||||
"$CHECKOUT" "$EXPECTED" "${report%,}"
|
||||
printf '{"checkout":"%s","expected":%s,"plugin":{"installed":"%s","available":"%s"},"findings":[%s]}\n' \
|
||||
"$CHECKOUT" "$EXPECTED" "$installed_v" "$available_v" "${report%,}"
|
||||
elif [ "$findings" -gt 0 ]; then
|
||||
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
|
||||
"$findings" "$CHECKOUT" "$report"
|
||||
|
||||
Executable
+85
@@ -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"
|
||||
Executable
+26
@@ -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]
|
||||
}'
|
||||
+122
-6
@@ -20,14 +20,24 @@
|
||||
# cannot. This script covers the harnesses with no plugin system: Crush, Pi
|
||||
# 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/opencode/skills -> ~/.agents/skills
|
||||
#
|
||||
# Adding a skill to the repo therefore wires it everywhere with no re-run,
|
||||
# and a broken link is one broken link rather than 42.
|
||||
# ~/.agents/skills was itself a link to this checkout until #27. 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.
|
||||
# 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
|
||||
# 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}"
|
||||
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/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' "$*"; }
|
||||
|
||||
@@ -74,6 +88,106 @@ count_skills() {
|
||||
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>
|
||||
# Replaces whatever is there — including a directory left behind by the old
|
||||
# per-skill wiring — with a single symlink.
|
||||
@@ -104,12 +218,14 @@ main() {
|
||||
|
||||
# Canonical location. Pi reads ~/.agents/skills directly, so it needs no
|
||||
# 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 "$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"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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".
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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: >
|
||||
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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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".
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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").
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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".
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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".
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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?").
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Tests for bin/check-index.
|
||||
#
|
||||
# The regression these exist for: SKILLS_INDEX.md is maintained by hand, and
|
||||
# two skills (`dream`, `experiment-spec`) shipped without a row — the index
|
||||
# rows lived on feature branches that were squash-merged after a directory
|
||||
# reorg, so the SKILL.md landed and the row did not (#15, #22). Nothing
|
||||
# errored; the index just quietly under-reported the library.
|
||||
|
||||
set -u
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
SUT="$ROOT/bin/check-index"
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
WORK=${TMPDIR:-/tmp}/check_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
|
||||
|
||||
# new_fixture <name> — a checkout with skills/alpha and skills/beta.
|
||||
new_fixture() {
|
||||
_f="$WORK/$1"
|
||||
mkdir -p "$_f/skills/alpha" "$_f/skills/beta"
|
||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
|
||||
printf -- '---\nname: beta\ndescription: b\n---\n' > "$_f/skills/beta/SKILL.md"
|
||||
printf '%s' "$_f"
|
||||
}
|
||||
|
||||
index() {
|
||||
_dir=$1; shift
|
||||
printf '# Engineering Skills Index\n\n| Skill | Description | Use when |\n|---|---|---|\n' > "$_dir/SKILLS_INDEX.md"
|
||||
for _n in "$@"; do
|
||||
printf '| `%s` | desc | when |\n' "$_n" >> "$_dir/SKILLS_INDEX.md"
|
||||
done
|
||||
}
|
||||
|
||||
# expect <0|1> <label> <fixture> [grep-for]
|
||||
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, but report never names %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
||||
fi
|
||||
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
||||
}
|
||||
|
||||
fx=$(new_fixture complete); index "$fx" alpha beta
|
||||
expect 0 'every skill has a row' "$fx"
|
||||
|
||||
fx=$(new_fixture missing); index "$fx" alpha
|
||||
expect 1 'a skill with no row is reported' "$fx" beta
|
||||
|
||||
fx=$(new_fixture stale); index "$fx" alpha beta gamma
|
||||
expect 1 'a row for a skill that does not exist is reported' "$fx" gamma
|
||||
|
||||
fx=$(new_fixture noindex)
|
||||
expect 1 'a missing index is reported' "$fx" SKILLS_INDEX.md
|
||||
|
||||
printf '\ncheck_index: %d passed, %d failed\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ]
|
||||
@@ -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
@@ -37,11 +37,21 @@ new_fixture() {
|
||||
printf '%s' "$_f"
|
||||
}
|
||||
|
||||
# wire_clean <fixture> — the correct wiring: one canonical link, harnesses
|
||||
# pointing at it.
|
||||
# wire_clean <fixture> [extra-source-dir] — the correct wiring since #27: the
|
||||
# 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() {
|
||||
mkdir -p "$1/home/.agents" "$1/home/.config/crush" "$1/home/.config/opencode"
|
||||
ln -sfn "$1/ck/skills" "$1/home/.agents/skills"
|
||||
mkdir -p "$1/home/.agents/skills" "$1/home/.config/crush" "$1/home/.config/opencode"
|
||||
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/opencode/skills"
|
||||
}
|
||||
@@ -79,27 +89,50 @@ assert_clean "$f" 'clean fixture exits 0'
|
||||
|
||||
# --- 2. dangling link (the 243-link case, gitignored and invisible) ----------
|
||||
f=$(new_fixture dangling); wire_clean "$f"
|
||||
ln -sfn "$f/ck/skills-that-moved" "$f/home/.agents/skills"
|
||||
assert_fails "$f" 'dangling' 'dangling canonical link is reported'
|
||||
ln -sfn "$f/ck/skills/vanished" "$f/home/.agents/skills/alpha"
|
||||
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
|
||||
# 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)
|
||||
mkdir -p "$f/other/skills/alpha" "$f/other/skills/beta"
|
||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/skills/alpha/SKILL.md"
|
||||
printf -- '---\nname: beta\ndescription: b\n---\n' > "$f/other/skills/beta/SKILL.md"
|
||||
mkdir -p "$f/other/alpha"
|
||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/alpha/SKILL.md"
|
||||
wire_clean "$f"
|
||||
ln -sfn "$f/other/skills" "$f/home/.agents/skills"
|
||||
assert_fails "$f" 'outside' 'link into a foreign checkout is reported'
|
||||
ln -sfn "$f/other/alpha" "$f/home/.agents/skills/alpha"
|
||||
assert_fails "$f" 'unknown source' 'link into an unconfigured source is reported'
|
||||
|
||||
# --- 4. count mismatch (a harness seeing fewer skills than the checkout) -----
|
||||
f=$(new_fixture count)
|
||||
mkdir -p "$f/home/.config/crush/skills/alpha"
|
||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
|
||||
mkdir -p "$f/home/.agents"
|
||||
# --- 3b. a link into a CONFIGURED extra source is healthy -------------------
|
||||
# The whole point of #27: Anthropic's official set alongside this library.
|
||||
f=$(new_fixture multisource)
|
||||
mkdir -p "$f/other/xlsx"
|
||||
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"
|
||||
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) --------
|
||||
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
|
||||
# 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.
|
||||
f=$(new_fixture realdir)
|
||||
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha"
|
||||
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
|
||||
f=$(new_fixture realdir); wire_clean "$f"
|
||||
rm -f "$f/home/.config/crush/skills"
|
||||
mkdir -p "$f/home/.config/crush/skills/alpha"
|
||||
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"'
|
||||
|
||||
@@ -133,5 +166,63 @@ mkdir -p "$f/home/.claude/skills"
|
||||
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
|
||||
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"
|
||||
[ "$fail" -eq 0 ]
|
||||
|
||||
@@ -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 ]
|
||||
@@ -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 ]
|
||||
@@ -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 ]
|
||||
Reference in New Issue
Block a user