Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46cbfc5967 | ||
|
|
cf3abafc66 | ||
|
|
6838baa071 | ||
|
|
f031c2b77d | ||
|
|
1328dcd8f3 | ||
|
|
6cc224f151 | ||
|
|
8359d61e33 | ||
|
|
400d2b9b6b | ||
|
|
4edf42ce9f | ||
|
|
2f53346b8c | ||
|
|
dcc0677270 | ||
|
|
9fd5a054b3 | ||
|
|
da76a6b3d2 | ||
|
|
fc6d5628a7 | ||
|
|
1f7f53e6c0 | ||
|
|
bedb6d278d | ||
|
|
e9ad692418 | ||
|
|
86529d2ac5 | ||
|
|
d6e892ac4c | ||
|
|
4d6ebb626f | ||
|
|
88a4fa407b | ||
|
|
808af7b020 | ||
|
|
40405cd95a | ||
|
|
39ac61c411 | ||
|
|
2cb6b54a5e | ||
|
|
1448963823 | ||
|
|
58687c68a6 | ||
|
|
0c7fac3686 | ||
|
|
cba0fe4337 | ||
|
|
06592805b6 | ||
|
|
1d140ec525 | ||
|
|
2fcc15e9e4 | ||
|
|
b2af4e1129 | ||
|
|
3c8d2f5613 | ||
|
|
1d368d4b5d | ||
|
|
a565b04e91 | ||
|
|
b616c51a2c | ||
|
|
b8bacfa7b3 | ||
|
|
c377c23e21 | ||
|
|
8f9d5dd8bd | ||
|
|
7ba01abdbe | ||
|
|
9a81a707af | ||
|
|
5070a738e8 | ||
|
|
8da7c9c45f | ||
|
|
6e5024d498 | ||
|
|
62cfe46927 | ||
|
|
de84800892 | ||
|
|
51a2d396fd | ||
|
|
bb912e5594 | ||
|
|
08c935ba8a | ||
|
|
84a012cd0f | ||
|
|
e12ac99465 | ||
|
|
dc97fd4a36 | ||
|
|
d7775aac57 | ||
|
|
c6787e04d7 | ||
|
|
90a58f0b9e | ||
|
|
15737321ac | ||
|
|
025f01aa80 | ||
|
|
eee62e4fc0 | ||
|
|
a2afb35d1d | ||
|
|
ad17a7c6c0 | ||
|
|
21dbb35526 | ||
|
|
8ad2079f86 | ||
|
|
b188965161 | ||
|
|
ae66840a03 | ||
|
|
e7e514390b | ||
|
|
087685f2e3 | ||
|
|
83b069afba | ||
|
|
a699ad8cbd | ||
|
|
a42ec18f40 | ||
|
|
46ff1acdfe | ||
|
|
3112b4ce92 | ||
|
|
51a8c25b4d | ||
|
|
d40169f2b7 | ||
|
|
d4f0416dc5 | ||
|
|
abd81f939a | ||
|
|
e42d2ba9cb | ||
|
|
68cebc0bbb | ||
|
|
f50dd6be6a | ||
|
|
1906a774b3 | ||
|
|
38f905e705 | ||
|
|
7db7e0779b | ||
|
|
0be3506799 | ||
|
|
6151b973bc | ||
|
|
6c1b6ce9db | ||
|
|
5c8341c797 | ||
|
|
5ca44c9c6b | ||
|
|
e2a312d05e | ||
|
|
78b9b59eca | ||
|
|
88c54399fe | ||
|
|
747c6c7f4c | ||
|
|
56a947b2a0 | ||
|
|
21be3ea245 |
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "mathias",
|
||||||
|
"description": "Mathias's private plugin marketplace, hosted on gitea.d-ma.be. Self-hosted by design — nothing here transits GitHub.",
|
||||||
|
"owner": {
|
||||||
|
"name": "Mathias",
|
||||||
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "skills",
|
||||||
|
"source": ".",
|
||||||
|
"description": "Engineering skills, the pre-task-ritual and secret-guard hooks, and the brain/gitea/infra MCP servers as one versioned unit."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"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.19.6",
|
||||||
|
"author": {
|
||||||
|
"name": "Mathias",
|
||||||
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
},
|
||||||
|
"homepage": "https://git.d-ma.be/mathias/skills",
|
||||||
|
"repository": "https://git.d-ma.be/mathias/skills.git"
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# Two jobs, in order:
|
||||||
|
#
|
||||||
|
# gate — `task check` (structure, frontmatter, manifest/tag parity) plus
|
||||||
|
# `task test` (hooks, doctor, release-skip). Runs on every push and
|
||||||
|
# every PR. Before this existed the suite ran nowhere: CI reported
|
||||||
|
# green having tested nothing (#22).
|
||||||
|
#
|
||||||
|
# tag — auto-tags a push to main with a semver bump derived from the latest
|
||||||
|
# commit footer:
|
||||||
|
# Bump-Type: major → vX.0.0
|
||||||
|
# Bump-Type: minor → v0.Y.0
|
||||||
|
# (default) → v0.0.Z (patch)
|
||||||
|
# Skips when the commit carries the `Skip-Release: true` trailer (the
|
||||||
|
# tagger's own manifest bump) to prevent recursion. Gated on `gate`,
|
||||||
|
# so a red check cannot ship a tag.
|
||||||
|
|
||||||
|
"on":
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gate:
|
||||||
|
name: Check / Test
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# `task check` compares plugin.json's version to `git describe`;
|
||||||
|
# a shallow checkout has no tags and would skip that assertion.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Verify toolchain
|
||||||
|
run: task --version
|
||||||
|
|
||||||
|
- name: Structure gate
|
||||||
|
run: task check
|
||||||
|
|
||||||
|
- name: Shell test suite
|
||||||
|
run: task test
|
||||||
|
|
||||||
|
tag:
|
||||||
|
name: Tag release
|
||||||
|
needs: gate
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: derive next version
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
last_commit=$(git log -1 --pretty=%B)
|
||||||
|
# Delegated to bin/should-skip-release so the rule is covered by
|
||||||
|
# tests/release_skip_test.sh rather than living untested in YAML.
|
||||||
|
if printf '%s\n' "$last_commit" | sh bin/should-skip-release; then
|
||||||
|
echo "skipping — commit carries the Skip-Release trailer"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
|
major=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/')
|
||||||
|
minor=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\2/')
|
||||||
|
patch=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\3/')
|
||||||
|
bump=$(printf '%s' "$last_commit" | grep -iE '^Bump-Type:' | head -1 | awk '{print tolower($2)}' || true)
|
||||||
|
case "$bump" in
|
||||||
|
major) major=$((major + 1)); minor=0; patch=0 ;;
|
||||||
|
minor) minor=$((minor + 1)); patch=0 ;;
|
||||||
|
*) patch=$((patch + 1)) ;;
|
||||||
|
esac
|
||||||
|
next="v${major}.${minor}.${patch}"
|
||||||
|
echo "previous=$latest"
|
||||||
|
echo "bump=${bump:-patch}"
|
||||||
|
echo "next=$next"
|
||||||
|
echo "tag=$next" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: tag + push
|
||||||
|
if: steps.bump.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.bump.outputs.tag }}
|
||||||
|
GITEA_ACTOR: ${{ gitea.actor }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "${GITEA_ACTOR}"
|
||||||
|
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
|
||||||
|
|
||||||
|
# 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}"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
# skills#46. This commit is pushed with secrets.GITEA_TOKEN, and a
|
||||||
|
# push authenticated with the Actions token does NOT trigger
|
||||||
|
# workflows -- deliberate platform behaviour to stop CI recursing.
|
||||||
|
# `Skip-Release: true` stops the tagger recursing; nothing stopped
|
||||||
|
# the whole run, `gate` included, from never firing on this commit.
|
||||||
|
#
|
||||||
|
# So every release left main's HEAD with zero CI runs. Measured:
|
||||||
|
# 6cc224f (0.19.4) and f031c2b (0.19.5) each have none, and the
|
||||||
|
# koala cluster's CIHeadUncovered{repo=skills} had been firing on it
|
||||||
|
# for 85 hours -- an alert that can only be cleared by an unrelated
|
||||||
|
# human push.
|
||||||
|
#
|
||||||
|
# The commit is not cosmetic: plugin.json's version is what Claude
|
||||||
|
# Code gates plugin updates on, `task check` asserts it is not
|
||||||
|
# behind the latest tag, and the sed-and-retry logic above already
|
||||||
|
# shipped a mismatched manifest once (v0.10.0 reading 0.9.0).
|
||||||
|
#
|
||||||
|
# Gating it HERE rather than re-triggering CI afterwards: the commit
|
||||||
|
# is produced here, so it can be validated here, at the cost of one
|
||||||
|
# `task check` instead of a second full run -- and with no recursion
|
||||||
|
# risk to reintroduce.
|
||||||
|
if ! task check; then
|
||||||
|
echo "::error::the release-bump commit fails task check -- not pushing it" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
# Auto-tag every push to main with a semver bump derived from the latest
|
|
||||||
# commit footer:
|
|
||||||
# Bump-Type: major → vX.0.0
|
|
||||||
# Bump-Type: minor → v0.Y.0
|
|
||||||
# (default) → v0.0.Z (patch)
|
|
||||||
#
|
|
||||||
# Skips when the latest commit is itself the auto-tagger (commit message
|
|
||||||
# contains "[skip-release]") to prevent recursion. Tag is pushed back
|
|
||||||
# to the repo using the GITEA_TOKEN provided by act_runner.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tag:
|
|
||||||
runs-on: self-hosted
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: derive next version
|
|
||||||
id: bump
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
last_commit=$(git log -1 --pretty=%B)
|
|
||||||
if printf '%s' "$last_commit" | grep -qi '\[skip-release\]'; then
|
|
||||||
echo "skipping — commit marked [skip-release]"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
||||||
major=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/')
|
|
||||||
minor=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\2/')
|
|
||||||
patch=$(echo "$latest" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\3/')
|
|
||||||
bump=$(printf '%s' "$last_commit" | grep -iE '^Bump-Type:' | head -1 | awk '{print tolower($2)}' || true)
|
|
||||||
case "$bump" in
|
|
||||||
major) major=$((major + 1)); minor=0; patch=0 ;;
|
|
||||||
minor) minor=$((minor + 1)); patch=0 ;;
|
|
||||||
*) patch=$((patch + 1)) ;;
|
|
||||||
esac
|
|
||||||
next="v${major}.${minor}.${patch}"
|
|
||||||
echo "previous=$latest"
|
|
||||||
echo "bump=${bump:-patch}"
|
|
||||||
echo "next=$next"
|
|
||||||
echo "tag=$next" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: tag + push
|
|
||||||
if: steps.bump.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
TAG: ${{ steps.bump.outputs.tag }}
|
|
||||||
GITEA_ACTOR: ${{ gitea.actor }}
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
git config user.name "${GITEA_ACTOR}"
|
|
||||||
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
|
|
||||||
git tag -a "$TAG" -m "release $TAG"
|
|
||||||
# gitea-actions checkout uses an x-access-token URL; reuse it.
|
|
||||||
git push origin "$TAG"
|
|
||||||
echo "pushed $TAG"
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Tailscale Serve on koala, not the public d-ma.be hostnames. The public path exits to Cloudflare and NPM to reach a k3s service on the same machine most sessions run on, which imposes rate limits and message-size caps on tool results. Serve terminates TLS with a real Tailscale cert and proxies to the NodePort over loopback, so traffic reaches the service from koala itself and satisfies the default-deny-ingress NetworkPolicy with no cluster change. Clients need 'tailscale set --accept-dns=true' for the MagicDNS name to resolve; see mathias/skills#24. Every ${VAR} below must be one ~/.op-env defines -- bin/check-mcp-env enforces that, because an undefined one sends an empty Authorization header and reports as 'requires authentication', which is indistinguishable from a revoked credential.",
|
||||||
|
"mcpServers": {
|
||||||
|
"brain": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://koala.tail30472e.ts.net:8443/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer ${BRAIN_MCP_TOKEN}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"infra": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://koala.tail30472e.ts.net:8444/mcp"
|
||||||
|
},
|
||||||
|
"gitea": {
|
||||||
|
"_comment": "gitea-mcp authenticates by PASS-THROUGH: the bearer is a Gitea PAT, which the server validates against Gitea and then acts as. So no dedicated MCP token is needed, and the AgentSecrets blocker that parked this entry (skills#32, #33) never applied to it -- DMABE_GITEA_API_TOKEN is op://HomeLab and already in the session environment. Verified 2026-09-07: initialize 200 (serverInfo gitea-mcp) and tools/list returns 41 tools. Two DIFFERENT PATs both authenticate while GITEA_MCP_TOKEN 401s, which is what proves this is pass-through validation and not a static-token comparison.",
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://koala.tail30472e.ts.net:8446/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer ${DMABE_GITEA_API_TOKEN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_parked": {
|
||||||
|
"_reason": "cad is proxied, reachable and AUTHENTICATING: tailscale serve 8445 -> 127.0.0.1:31877, and an MCP initialize with the correct bearer returns 200 (serverInfo cad aed2aaf). It stays parked only because nothing puts CAD_MCP_TOKEN into the session environment. It lives in the 1Password AgentSecrets vault, and by deliberate design (infra#224, closed) no single identity on koala reads both vaults: koala-homelab is HomeLab-only and resolves ~/.op-env, koala-agentsecrets-write is AgentSecrets-only and lives in ~/.op-agent-write.env. Adding an AgentSecrets reference to ~/.op-env does NOT degrade gracefully -- `op run` fails wholesale and every other token, BRAIN_MCP_TOKEN included, goes unresolved with it. Enabling needs a read-scoped AgentSecrets identity in the session-env path; see mathias/skills#32, #33. gitea was parked here for the same stated reason and has been unparked: it needs no AgentSecrets token at all, because gitea-mcp is pass-through-authenticated.",
|
||||||
|
"cad": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://koala.tail30472e.ts.net:8445/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer ${CAD_MCP_TOKEN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
# skills
|
# skills
|
||||||
|
|
||||||
Engineering skills library — markdown `SKILL.md` files that plug into any
|
Engineering skills library — markdown `SKILL.md` files that plug into any
|
||||||
project or AI-coding harness. Installed via `task skills:install` in
|
AI-coding harness.
|
||||||
consumer projects, or directly by `install.sh` on hosts without Task.
|
|
||||||
|
For **Claude Code** this repo is a plugin, installed from the marketplace it
|
||||||
|
also hosts; that path carries the hooks and MCP servers too. For **Crush, Pi
|
||||||
|
and OpenCode** it is a plain library wired by `install.sh` as one directory
|
||||||
|
symlink per harness. See [`SKILLS_INDEX.md`](./SKILLS_INDEX.md#wiring-into-tools).
|
||||||
|
|
||||||
## What is a skill?
|
## What is a skill?
|
||||||
|
|
||||||
@@ -16,65 +20,117 @@ references.
|
|||||||
See [`SKILLS_INDEX.md`](./SKILLS_INDEX.md) for the full catalogue with
|
See [`SKILLS_INDEX.md`](./SKILLS_INDEX.md) for the full catalogue with
|
||||||
descriptions and "use when" triggers.
|
descriptions and "use when" triggers.
|
||||||
|
|
||||||
## Install (consumer project)
|
## Install
|
||||||
|
|
||||||
The expected pattern: each consumer project's `Taskfile.yml` has a
|
### Claude Code — the plugin
|
||||||
`skills:install` target that delegates here. From a project root:
|
|
||||||
|
|
||||||
```bash
|
```
|
||||||
task skills:install
|
/plugin marketplace add https://git.d-ma.be/mathias/skills.git
|
||||||
|
/plugin install skills@mathias
|
||||||
```
|
```
|
||||||
|
|
||||||
Bare-shell fallback for hosts without Task installed:
|
The plugin ships skills, the `secret-guard` + `pre-task-ritual` hooks, and the
|
||||||
|
brain/gitea/infra MCP servers as one versioned unit. Skills are namespaced
|
||||||
|
(`skills:tdd`). Updates land when `.claude-plugin/plugin.json`'s `version`
|
||||||
|
moves — CI bumps it on every release.
|
||||||
|
|
||||||
|
### Crush, Pi, OpenCode — `install.sh`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://gitea.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Both forms are idempotent and re-runnable. On every run they:
|
Idempotent. It clones or fast-forwards the checkout, then populates one
|
||||||
|
**managed directory** and gives each harness a single link to it:
|
||||||
|
|
||||||
1. `git clone` (or `git pull`) this repo into `~/.local/share/skills/` —
|
| Path | Target |
|
||||||
the **canonical local checkout** on every host.
|
|---|---|
|
||||||
2. Wire per-skill symlinks into the harnesses found on the host (see
|
| `~/.agents/skills/` | a real directory, one symlink per skill (Pi reads this path directly) |
|
||||||
[Wired harnesses](#wired-harnesses)).
|
| `~/.agents/skills/<skill>` | `$SKILLS_CHECKOUT_DIR/skills/<skill>`, or another configured source |
|
||||||
|
| `~/.config/crush/skills` | `~/.agents/skills` |
|
||||||
|
| `~/.config/opencode/skills` | `~/.agents/skills` |
|
||||||
|
|
||||||
Re-run after `git pull` upstream to pick up new skills or version
|
Total links are N + one per harness — not N x harnesses, which is what #23 was
|
||||||
changes. CI tags every change as a `vX.Y.Z` release (see
|
actually about.
|
||||||
[Versioning](#versioning)).
|
|
||||||
|
|
||||||
## Wired harnesses
|
### More than one source of skills
|
||||||
|
|
||||||
| Harness | Target path | Notes |
|
`~/.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
|
||||||
| **Claude Code (global)** | `~/.claude/skills/<name>` | Visible in every Claude Code session on the host. |
|
of skills on the host — and on flamingo the only way to install this library
|
||||||
| **Claude Code (per-repo)** | `<repo>/.claude/skills/<name>` | Created when `install` is invoked inside a git repo (not the skills repo itself). Gitignored globally via `**/.claude/skills/`. |
|
was to displace Anthropic's official set, which lived in Crush's directory.
|
||||||
| **Crush** | `~/.config/crush/skills/<name>` | Charmbracelet Crush — successor to `opencode-ai/opencode` (archived). |
|
Real capability loss dressed up as a tidy-up (#27).
|
||||||
| **Antigravity** | `~/.gemini/antigravity/skills/<name>` | Google Antigravity (VS Code extension). Our `SKILL.md` frontmatter (`name` + `description`) is already in the format Antigravity expects, so symlinks suffice — no manifest translation. |
|
|
||||||
|
|
||||||
### Not wired (and why)
|
Point `SKILLS_EXTRA_SOURCES` at any number of extra directories, colon
|
||||||
|
separated, each holding `<skill>/SKILL.md`:
|
||||||
|
|
||||||
- **opencode** (`opencode-ai/opencode`): archive notice; succeeded by Crush.
|
```
|
||||||
Its only "skill"-like surface — Custom Commands at `~/.config/opencode/commands/` —
|
SKILLS_EXTRA_SOURCES=~/.config/crush/skills.anthropic bash install.sh
|
||||||
is for user-facing prompt templates, not system-level instructions.
|
```
|
||||||
Misaligned with how we use skills. Skipped.
|
|
||||||
- **gitea-resident agents** (cobalt-dingo, agentsquad): they consume
|
- The checkout is always first and always wins a name collision — but the
|
||||||
skills via their containing project's `.claude/skills` directory
|
collision is **logged**, never resolved in silence. Quietly picking one is
|
||||||
(populated by the per-repo wirer when run from the host), or via the
|
how stale content wins.
|
||||||
brain MCP. No special target needed.
|
- 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
|
||||||
|
|
||||||
|
`SKILLS_CHECKOUT_DIR` defaults to `~/.local/share/skills`, and that default is
|
||||||
|
load-bearing: it is **host-local, outside any synced or shared directory**.
|
||||||
|
|
||||||
|
On this fleet `~/dev` is an iCloud folder shared by flamingo and iguana, and it
|
||||||
|
spans two path roots — `/Users/mathias` on the Macs, `/home/mathias` on koala.
|
||||||
|
A checkout there is a git working tree with two concurrent writers, and any
|
||||||
|
absolute symlink stored there is broken on one side by construction. That is
|
||||||
|
what produced 243 dangling links across 12 repos (#23). Do not "fix" the
|
||||||
|
default to point into `~/dev`.
|
||||||
|
|
||||||
### Per-host env-var overrides
|
### Per-host env-var overrides
|
||||||
|
|
||||||
Each target path can be overridden by setting the matching env var before
|
|
||||||
running `install.sh`:
|
|
||||||
|
|
||||||
| Env var | Default |
|
| Env var | Default |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SKILLS_REPO_URL` | `https://gitea.d-ma.be/mathias/skills.git` |
|
| `SKILLS_REPO_URL` | `https://git.d-ma.be/mathias/skills.git` |
|
||||||
| `SKILLS_REF` | `main` (set to e.g. `v0.1.0` to pin a release) |
|
| `SKILLS_REF` | `main` (set to e.g. `v0.4.0` to pin a release) |
|
||||||
| `SKILLS_CHECKOUT_DIR` | `$HOME/.local/share/skills` |
|
| `SKILLS_CHECKOUT_DIR` | `$HOME/.local/share/skills` |
|
||||||
| `CLAUDE_SKILLS_DIR` | `$HOME/.claude/skills` |
|
| `SKILLS_SKIP_FETCH` | unset (`1` wires an existing checkout without touching HEAD) |
|
||||||
|
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
|
||||||
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
|
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
|
||||||
| `ANTIGRAVITY_SKILLS_DIR` | `$HOME/.gemini/antigravity/skills` |
|
| `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` |
|
||||||
|
| `ANTIGRAVITY_SKILLS_JSON` | `$HOME/.gemini/config/skills.json` (registers `AGENTS_SKILLS_DIR` as a path entry; needs `jq`) |
|
||||||
|
| `SKILLS_EXTRA_SOURCES` | unset (colon-separated extra source directories) |
|
||||||
|
|
||||||
|
The Mistral Vibe target was removed — unused. Antigravity's was removed for
|
||||||
|
the same reason and re-added once it came back into use.
|
||||||
|
|
||||||
|
## Checking a host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task doctor # or bin/doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Reports dangling links, links into a source nobody configured, skills in the
|
||||||
|
checkout with no link, stray entries, leftover per-skill Claude symlinks, and a
|
||||||
|
Claude Code plugin sitting on an older generation than the host already has.
|
||||||
|
Non-zero exit on drift. It also runs at session start via the plugin, so a
|
||||||
|
drifting host says so rather than waiting to be discovered.
|
||||||
|
|
||||||
|
All of that compares copies already on the machine — deliberately, since it
|
||||||
|
runs in a session hook where latency shows. That leaves one case invisible: a
|
||||||
|
host where the checkout, the marketplace clone and the installed plugin all
|
||||||
|
agree, and all three are a release behind origin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task doctor -- --online # or bin/doctor --online
|
||||||
|
```
|
||||||
|
|
||||||
|
One `git ls-remote` against origin, opt-in. Belongs in a scheduled per-host job,
|
||||||
|
not the session hook. An unreachable remote is reported as its own fault rather
|
||||||
|
than as staleness — "I could not check" and "you are behind" are different
|
||||||
|
answers and only one of them is about the host.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
@@ -91,13 +147,48 @@ before `task skills:install`; default is `main` for always-current.
|
|||||||
|
|
||||||
## Contributing a skill
|
## Contributing a skill
|
||||||
|
|
||||||
1. Add a directory `<short-kebab-name>/`.
|
1. Add a directory `skills/<short-kebab-name>/`.
|
||||||
2. Create `<short-kebab-name>/SKILL.md` — start with one-paragraph
|
2. Create `skills/<short-kebab-name>/SKILL.md` with this frontmatter — all four
|
||||||
purpose, then triggers ("use when …"), then mechanics.
|
keys are gated by `task check`, and the index table is generated from the
|
||||||
3. Add a row to `SKILLS_INDEX.md` (description + use-when).
|
last three:
|
||||||
4. Commit directly to `main`. Conventional-commit prefix
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: <short-kebab-name>
|
||||||
|
surfaces: [claude-code, library] # any of claude-code | library | chat
|
||||||
|
summary: "<one line for the index table>"
|
||||||
|
use_when: "<the trigger, for the index table>"
|
||||||
|
description: <the paragraph the model selects on>
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Then: one-paragraph purpose, triggers ("use when …"), mechanics.
|
||||||
|
3. Run `task index` and commit the regenerated `SKILLS_INDEX.md`. Do **not**
|
||||||
|
hand-edit the table — `task check` regenerates it and fails on a diff.
|
||||||
|
4. Pressure-test it before shipping: run the scenario against a subagent that
|
||||||
|
does *not* have the skill, write the skill against what that run actually
|
||||||
|
did, then re-run with it. See `superpowers-writing-skills` and #19.
|
||||||
|
5. Commit directly to `main`. Conventional-commit prefix
|
||||||
(`feat:` / `fix:` / `docs:`). Add `Bump-Type: minor` footer when
|
(`feat:` / `fix:` / `docs:`). Add `Bump-Type: minor` footer when
|
||||||
adding a new skill or expanding scope; default patch covers edits.
|
adding a new skill or expanding scope; default patch covers edits.
|
||||||
|
|
||||||
This repo is markdown-only and public — never include secrets, client
|
This repo is markdown-only and public — never include secrets, client
|
||||||
names, or anything under NDA in a skill.
|
names, or anything under NDA in a skill.
|
||||||
|
|
||||||
|
## Mode 2 routing candidates
|
||||||
|
|
||||||
|
These notes used to sit in each SKILL.md as a `## Mode 2 Routing Note` section.
|
||||||
|
They changed nothing about how the skill is invoked — several said so outright —
|
||||||
|
and cost context on every load, so they live here instead. Nothing in the repo
|
||||||
|
depends on this table; it is a record of which skills looked mechanical enough
|
||||||
|
to route to a local model, should a routing layer ever exist.
|
||||||
|
|
||||||
|
| Skill | Note |
|
||||||
|
|---|---|
|
||||||
|
| `cad-babysit` | Phases 2-4 (reading comments/logs, classifying fault vs outcome) are judgment-heavy and should stay Mode 1. Phase 1 (running `cad stuck` and grouping its output) is mechanical and a Mode 2 routing candidate once Plan 6 ships the routing pod. |
|
||||||
|
| `code-review` | This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes. |
|
||||||
|
| `debug` | This skill produces high-volume mechanical output (hypothesis enumeration) and is a candidate for Mode 2 routing to a local model in the future. Until Plan 6 ships the routing pod, treat as Mode 1 only. The hypothesis format and discipline are identical regardless of which model generates them. |
|
||||||
|
| `feature-spec` | Spec writing requires interpretation of intent and tradeoff reasoning — work where Claude generally outperforms a 9-13B local model. This skill is intentionally **not** a Mode 2 routing candidate. In Mode 2, the routing layer should leave `feature-spec` calls on Claude. |
|
||||||
|
| `session-retrospective` | Reading long session logs and pulling themes is high-volume, mechanical pattern-recognition work — a strong Mode 2 routing candidate. Until Plan 6 ships the routing pod, treat as Mode 1 only. |
|
||||||
|
| `tdd` | This skill is invoked identically whether the agent is running in Mode 1 (cloud Claude, no routing) or Mode 2 (client-local, supervisor routing layer). The routing pod (Plan 6) does not exist yet; until it does, treat this skill as Mode 1 only. The discipline does not change between modes — only the model behind the call. |
|
||||||
|
| `trainer` | Trainer is the same shape as `session-retrospective` — long-context reading, mechanical filtering — and is a Mode 2 routing candidate. Until Plan 6, treat as Mode 1 only. |
|
||||||
|
|||||||
+115
-64
@@ -2,67 +2,116 @@
|
|||||||
|
|
||||||
This index lists all available engineering skills. Load the full SKILL.md on demand.
|
This index lists all available engineering skills. Load the full SKILL.md on demand.
|
||||||
|
|
||||||
| Skill | Description | Use when |
|
The table is **generated** from each skill's frontmatter (`summary`, `use_when`,
|
||||||
|-------|-------------|----------|
|
`surfaces`) by `task index`, and `task check` fails if it is out of date. Edit
|
||||||
| `tdd` | Test-Driven Development enforcement | Starting any new feature or fixing a bug |
|
the SKILL.md, not the table.
|
||||||
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code |
|
|
||||||
| `solid` | SOLID principles with Go examples | Design review, architecture decisions |
|
<!-- BEGIN skills-table -->
|
||||||
| `code-review` | Code smell detection + Go-specific checklist | Pre-merge review |
|
| Skill | Description | Use when | Surfaces |
|
||||||
| `refactoring` | Systematic refactoring methodology (Fowler) | When improving existing code |
|
|-------|-------------|----------|----------|
|
||||||
| `test-design` | Dave Farley test design scoring (Farley Index) | Reviewing or writing tests |
|
| `adr` | Architecture Decision Records — house format, and how to record a decision you cannot make yet | Recording an architectural decision; revisiting one; or deciding between options where the right answer genuinely is not knowable yet | claude-code, library |
|
||||||
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring |
|
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria | claude-code, library |
|
||||||
| `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task |
|
| `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 |
|
||||||
| `user-stories` | Elephant Carpaccio story slicing | Breaking down features |
|
| `clean-code` | Clean code principles with Go adaptation | Code review, refactoring, new code | claude-code, library |
|
||||||
| `atdd` | Acceptance Test-Driven Development workflow | Feature development with acceptance criteria |
|
| `code-review` | Code smell detection + Go-specific checklist | Pre-merge review | claude-code, library |
|
||||||
| `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements |
|
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring | claude-code, library |
|
||||||
| `planning` | Planning and task breakdown | Sprint planning, task decomposition |
|
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious — or a bug report with no failure text, which this skill refuses rather than guesses at | claude-code, library |
|
||||||
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
|
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | claude-code, library |
|
||||||
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
|
| `document-to-markdown` | Read .docx/.xlsx/.pptx/.epub/.msg and other formats the Read tool cannot open | You need the contents of a binary document — Word, Excel, PowerPoint, EPUB, Outlook .msg, a zip of documents — and Read returns nothing useful | claude-code, library |
|
||||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index | claude-code, library, chat |
|
||||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature | claude-code, library |
|
||||||
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
|
||||||
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
| `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 |
|
| `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 |
|
| `lineage` | Backward decision archaeology — was this tried before, and does the reason it failed still hold | Before proposing something that feels new; when an idea keeps resurfacing; when you inherit code whose reason is gone | claude-code, library |
|
||||||
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
| `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 |
|
||||||
| `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 |
|
| `planning` | Planning and task breakdown | Sprint planning, task decomposition | claude-code, library |
|
||||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
|
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | 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 |
|
| `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task | 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 |
|
| `refactoring` | Systematic refactoring methodology (Fowler) | When improving existing code | claude-code, library |
|
||||||
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
|
| `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 |
|
||||||
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
|
| `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 |
|
||||||
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion |
|
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost | claude-code, library |
|
||||||
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents |
|
| `solid` | SOLID principles with Go examples | Design review, architecture decisions | claude-code, library |
|
||||||
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents |
|
| `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements | claude-code, library |
|
||||||
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically |
|
| `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-test-driven-development` | TDD discipline (superpowers variant) | Red → green → refactor on a change |
|
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas | claude-code, library |
|
||||||
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete |
|
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents | claude-code, library |
|
||||||
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review |
|
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion | claude-code, library |
|
||||||
| `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 | claude-code, library |
|
||||||
| `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR |
|
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments | claude-code, library |
|
||||||
| `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree |
|
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review | claude-code, library |
|
||||||
| `superpowers-writing-skills` | Authoring new SKILL.md skills | Creating a new reusable skill |
|
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents | claude-code, library |
|
||||||
| `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills |
|
| `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 |
|
||||||
|
| `traceability` | Forward trace — requirement to decision to commit to verification to release, and what cannot be checked | Proving why a change exists; auditing a claim; before an attestation or a compliance ask | claude-code, library |
|
||||||
|
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation | claude-code, library |
|
||||||
|
| `user-stories` | Elephant Carpaccio story slicing | Breaking down features | claude-code, library |
|
||||||
|
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists | claude-code, library |
|
||||||
|
<!-- END skills-table -->
|
||||||
|
|
||||||
## Wiring into tools
|
## Wiring into tools
|
||||||
|
|
||||||
This repo **is** the canonical home. Wire every skill into each harness with the
|
This repo is the canonical home, and it is delivered two different ways.
|
||||||
**`install.sh`** script (idempotent, per-skill symlinks) — either the one-shot
|
|
||||||
`curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash`,
|
|
||||||
or, from a clone, `SKILLS_CHECKOUT_DIR="$PWD" bash install.sh`. (The `task
|
|
||||||
install` targets are currently broken — see issue — use `install.sh`.)
|
|
||||||
|
|
||||||
- **Claude Code (global)** — `~/.claude/skills/<name>` → `<repo>/<name>`.
|
### Claude Code — install the plugin
|
||||||
- **Claude Code (per-repo)** — `<repo>/.claude/skills/<name>` when `task install`
|
|
||||||
is run inside another git repo (excluded via `~/.config/git/ignore`).
|
|
||||||
- **Crush** — `~/.config/crush/skills/<name>`.
|
|
||||||
- **Antigravity** — `~/.gemini/antigravity/skills/<name>`.
|
|
||||||
- **Mistral Vibe** — `~/.vibe/skills/<name>.md` → `<repo>/<name>/SKILL.md`
|
|
||||||
(flat `.md` layout).
|
|
||||||
|
|
||||||
Individual targets are also runnable: `task install:claude:global`,
|
This repo hosts a marketplace containing exactly one plugin: itself. The plugin
|
||||||
`install:crush`, `install:antigravity`, `install:vibe`. `task update` pulls the
|
carries the skills **plus** the `secret-guard` and `pre-task-ritual` hooks and
|
||||||
canonical checkout then re-wires.
|
the brain/gitea/infra MCP servers — none of which a symlink can deliver.
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin marketplace add https://git.d-ma.be/mathias/skills.git
|
||||||
|
/plugin install skills@mathias
|
||||||
|
```
|
||||||
|
|
||||||
|
Skills are namespaced: `tdd` becomes `skills:tdd`. Updates arrive when
|
||||||
|
`.claude-plugin/plugin.json`'s `version` moves, which CI does on every release.
|
||||||
|
Remove any leftover `~/.claude/skills/<name>` symlinks from the pre-plugin era —
|
||||||
|
plugin skills do **not** override same-named user skills, so leftovers make
|
||||||
|
everything load twice.
|
||||||
|
|
||||||
|
### Crush, Pi, OpenCode — run `install.sh`
|
||||||
|
|
||||||
|
These have no plugin system, so they get symlinks — but through one managed
|
||||||
|
directory, never one link per skill per harness:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
`SKILLS_CHECKOUT_DIR` defaults to `~/.local/share/skills` and **must stay
|
||||||
|
outside `~/dev`**. On this fleet `~/dev` is an iCloud folder shared by both
|
||||||
|
Macs and spans two path roots (`/Users/mathias` vs `/home/mathias` on koala);
|
||||||
|
a checkout there means a git working tree with two concurrent writers, and any
|
||||||
|
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 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
|
## Skill Relationships
|
||||||
|
|
||||||
@@ -73,14 +122,16 @@ problem-analysis
|
|||||||
│
|
│
|
||||||
├── spec-driven-dev (PM-kickoff lifecycle)
|
├── spec-driven-dev (PM-kickoff lifecycle)
|
||||||
│ │
|
│ │
|
||||||
│ └── feature-spec (per-feature, mid-implementation)
|
│ ├── feature-spec (per-feature, mid-implementation)
|
||||||
│ │
|
│ │ │
|
||||||
│ └── planning
|
│ │ └── planning
|
||||||
│ │
|
│ │ │
|
||||||
│ └── atdd ──→ tdd (foundation)
|
│ │ └── atdd ──→ tdd (foundation)
|
||||||
│ ├──→ debug (when tests fail)
|
│ │ ├──→ debug (when tests fail)
|
||||||
│ ├──→ clean-code (REFACTOR phase)
|
│ │ ├──→ clean-code (REFACTOR phase)
|
||||||
│ └──→ test-design (review)
|
│ │ └──→ test-design (review)
|
||||||
|
│ │
|
||||||
|
│ └── experiment-spec (research/ML phase, instead of feature-spec)
|
||||||
│
|
│
|
||||||
└── planning
|
└── planning
|
||||||
|
|
||||||
|
|||||||
+140
-154
@@ -2,22 +2,19 @@ version: '3'
|
|||||||
|
|
||||||
# Skills library Taskfile.
|
# Skills library Taskfile.
|
||||||
#
|
#
|
||||||
# Primary entry point for project + host installation. Consumer projects
|
# This repo is two things at once:
|
||||||
# do NOT depend on this file directly — they ship their own
|
# 1. A Claude Code PLUGIN (skills + hooks + MCP servers), distributed through
|
||||||
# `skills:install` target that delegates here via `install.sh`. This
|
# the marketplace this same repo hosts at .claude-plugin/marketplace.json.
|
||||||
# Taskfile is invoked by maintainers of the skills repo itself, plus by
|
# 2. A plain skills library for harnesses with no plugin system — Crush, Pi,
|
||||||
# install.sh during the post-clone wire step.
|
# OpenCode — wired by install.sh as ONE directory symlink per harness.
|
||||||
|
#
|
||||||
|
# The wiring loops that used to live here were five near-identical copies that
|
||||||
|
# drifted; install.sh owns wiring now and this file delegates to it.
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
REPO_URL: 'https://git.d-ma.be/mathias/skills.git'
|
REPO_URL: 'https://git.d-ma.be/mathias/skills.git'
|
||||||
CHECKOUT_DIR: '{{.HOME}}/.local/share/skills'
|
SKILLS_DIR: '{{.TASKFILE_DIR}}/skills'
|
||||||
CLAUDE_GLOBAL_DIR: '{{.HOME}}/.claude/skills'
|
PLUGIN_MANIFEST: '{{.TASKFILE_DIR}}/.claude-plugin/plugin.json'
|
||||||
CRUSH_DIR: '{{.HOME}}/.config/crush/skills'
|
|
||||||
ANTIGRAVITY_DIR: '{{.HOME}}/.gemini/antigravity/skills'
|
|
||||||
VIBE_DIR: '{{.HOME}}/.vibe/skills'
|
|
||||||
# Anything at the repo root that is NOT a skill directory. Both
|
|
||||||
# Taskfile + install.sh share this exclusion regex.
|
|
||||||
NON_SKILL_ENTRIES: '^(Taskfile.yml|install.sh|README.md|SKILLS_INDEX.md|.gitea|.git$)$'
|
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
@@ -28,134 +25,146 @@ tasks:
|
|||||||
list:
|
list:
|
||||||
desc: 'Print every skill in this repo'
|
desc: 'Print every skill in this repo'
|
||||||
cmds:
|
cmds:
|
||||||
- 'ls -1 {{.TASKFILE_DIR}} | grep -Ev "^(Taskfile.yml|install.sh|README.md|SKILLS_INDEX.md|.gitea|.git$)$" || true'
|
- 'ls -1 {{.SKILLS_DIR}}'
|
||||||
silent: true
|
silent: true
|
||||||
|
|
||||||
install:
|
install:
|
||||||
desc: 'Wire skills into every supported harness (Claude Code, Crush, Antigravity, Mistral Vibe).'
|
desc: 'Wire THIS checkout into Crush / Pi / OpenCode. Claude Code uses the plugin instead.'
|
||||||
cmds:
|
cmds:
|
||||||
- task: install:claude:global
|
- SKILLS_SKIP_FETCH=1 SKILLS_CHECKOUT_DIR='{{.TASKFILE_DIR}}' bash '{{.TASKFILE_DIR}}/install.sh'
|
||||||
- task: install:claude:repo
|
|
||||||
- task: install:crush
|
|
||||||
- task: install:antigravity
|
|
||||||
- task: install:vibe
|
|
||||||
|
|
||||||
install:claude:global:
|
|
||||||
desc: 'Per-skill symlinks under ~/.claude/skills/<name> — visible in every Claude Code project on this host.'
|
|
||||||
cmds:
|
|
||||||
- mkdir -p {{.CLAUDE_GLOBAL_DIR}}
|
|
||||||
- |
|
|
||||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
|
||||||
target="{{.TASKFILE_DIR}}/${skill}"
|
|
||||||
link="{{.CLAUDE_GLOBAL_DIR}}/${skill}"
|
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
|
||||||
if [ "$current" = "$target" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -rf "$link"
|
|
||||||
fi
|
|
||||||
ln -s "$target" "$link"
|
|
||||||
echo "linked $link → $target"
|
|
||||||
done
|
|
||||||
|
|
||||||
install:claude:repo:
|
|
||||||
desc: 'Per-skill symlinks under $(pwd)/.claude/skills/<name> when invoked inside a git repo.'
|
|
||||||
cmds:
|
|
||||||
- |
|
|
||||||
if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
||||||
echo "not in a git repo — skipping per-repo wire"; exit 0
|
|
||||||
fi
|
|
||||||
repo=$(git rev-parse --show-toplevel)
|
|
||||||
if [ "$repo" = "{{.TASKFILE_DIR}}" ]; then
|
|
||||||
echo "running from skills repo itself — skipping per-repo wire"; exit 0
|
|
||||||
fi
|
|
||||||
target_dir="$repo/.claude/skills"
|
|
||||||
mkdir -p "$target_dir"
|
|
||||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
|
||||||
target="{{.TASKFILE_DIR}}/${skill}"
|
|
||||||
link="${target_dir}/${skill}"
|
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
|
||||||
if [ "$current" = "$target" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -rf "$link"
|
|
||||||
fi
|
|
||||||
ln -s "$target" "$link"
|
|
||||||
echo "linked $link → $target"
|
|
||||||
done
|
|
||||||
|
|
||||||
install:crush:
|
|
||||||
desc: 'Per-skill symlinks under ~/.config/crush/skills/<name> — visible in every Crush session on this host.'
|
|
||||||
cmds:
|
|
||||||
- mkdir -p {{.CRUSH_DIR}}
|
|
||||||
- |
|
|
||||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
|
||||||
target="{{.TASKFILE_DIR}}/${skill}"
|
|
||||||
link="{{.CRUSH_DIR}}/${skill}"
|
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
|
||||||
if [ "$current" = "$target" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -rf "$link"
|
|
||||||
fi
|
|
||||||
ln -s "$target" "$link"
|
|
||||||
echo "linked $link → $target"
|
|
||||||
done
|
|
||||||
|
|
||||||
install:antigravity:
|
|
||||||
desc: 'Per-skill symlinks under ~/.gemini/antigravity/skills/<name>. SKILL.md frontmatter (name + description) is already antigravity-compatible.'
|
|
||||||
cmds:
|
|
||||||
- mkdir -p {{.ANTIGRAVITY_DIR}}
|
|
||||||
- |
|
|
||||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
|
||||||
target="{{.TASKFILE_DIR}}/${skill}"
|
|
||||||
link="{{.ANTIGRAVITY_DIR}}/${skill}"
|
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
|
||||||
if [ "$current" = "$target" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -rf "$link"
|
|
||||||
fi
|
|
||||||
ln -s "$target" "$link"
|
|
||||||
echo "linked $link → $target"
|
|
||||||
done
|
|
||||||
|
|
||||||
install:vibe:
|
|
||||||
desc: 'Per-skill file symlinks under ~/.vibe/skills/<name>.md → <skill>/SKILL.md for Mistral Vibe (flat .md layout, not a directory).'
|
|
||||||
cmds:
|
|
||||||
- mkdir -p {{.VIBE_DIR}}
|
|
||||||
- |
|
|
||||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "{{.NON_SKILL_ENTRIES}}"); do
|
|
||||||
target="{{.TASKFILE_DIR}}/${skill}/SKILL.md"
|
|
||||||
[ -f "$target" ] || continue
|
|
||||||
link="{{.VIBE_DIR}}/${skill}.md"
|
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
|
||||||
if [ "$current" = "$target" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -rf "$link"
|
|
||||||
fi
|
|
||||||
ln -s "$target" "$link"
|
|
||||||
echo "linked $link → $target"
|
|
||||||
done
|
|
||||||
|
|
||||||
update:
|
update:
|
||||||
desc: 'git pull the canonical checkout then re-run install.'
|
desc: 'Fast-forward this checkout and re-wire.'
|
||||||
cmds:
|
cmds:
|
||||||
- |
|
- git -C '{{.TASKFILE_DIR}}' pull --ff-only
|
||||||
if [ ! -d "{{.CHECKOUT_DIR}}/.git" ]; then
|
|
||||||
echo "no canonical checkout at {{.CHECKOUT_DIR}} — run install.sh first"; exit 1
|
|
||||||
fi
|
|
||||||
git -C "{{.CHECKOUT_DIR}}" pull --ff-only
|
|
||||||
- task: install
|
- task: install
|
||||||
|
|
||||||
|
doctor:
|
||||||
|
desc: 'Report drift across every harness on this host. Non-zero exit means something is wrong.'
|
||||||
|
cmds:
|
||||||
|
- '{{.TASKFILE_DIR}}/bin/doctor'
|
||||||
|
|
||||||
|
validate:
|
||||||
|
desc: 'Validate the plugin + marketplace manifests with the Claude Code CLI.'
|
||||||
|
cmds:
|
||||||
|
- claude plugin validate '{{.TASKFILE_DIR}}'
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: 'Run the shell test suite (hooks + doctor).'
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
fail=0
|
||||||
|
for t in '{{.TASKFILE_DIR}}'/tests/*_test.sh; do
|
||||||
|
[ -f "$t" ] || continue
|
||||||
|
echo "--- $(basename "$t")"
|
||||||
|
sh "$t" || fail=1
|
||||||
|
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, surfaces, generated index, manifest/tag parity. Runs in CI.'
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
# 1. No stray entries at the repo root. A loose foo.md where
|
||||||
|
# skills/foo/SKILL.md was meant loads in NO harness and errors in
|
||||||
|
# none either — it just silently does nothing. (See #23.)
|
||||||
|
for entry in "{{.TASKFILE_DIR}}"/*; do
|
||||||
|
base=$(basename "$entry")
|
||||||
|
case "$base" in
|
||||||
|
install.sh|Taskfile.yml|README.md|SKILLS_INDEX.md|skills|hooks|bin|tests) continue ;;
|
||||||
|
esac
|
||||||
|
echo "ERROR: unexpected entry at repo root: $base" >&2
|
||||||
|
fail=1
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Every skill is a directory with a SKILL.md carrying name + description.
|
||||||
|
count=0
|
||||||
|
for dir in "{{.SKILLS_DIR}}"/*; do
|
||||||
|
name=$(basename "$dir")
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
echo "ERROR: skills/$name is not a directory" >&2; fail=1; continue
|
||||||
|
fi
|
||||||
|
if [ ! -f "$dir/SKILL.md" ]; then
|
||||||
|
echo "ERROR: skills/$name missing SKILL.md" >&2; fail=1; continue
|
||||||
|
fi
|
||||||
|
head -n 20 "$dir/SKILL.md" | grep -q '^name:' || { echo "ERROR: skills/$name SKILL.md has no 'name:' frontmatter" >&2; fail=1; }
|
||||||
|
head -n 20 "$dir/SKILL.md" | grep -q '^description:' || { echo "ERROR: skills/$name SKILL.md has no 'description:' frontmatter" >&2; fail=1; }
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. SKILLS_INDEX.md's table is generated from frontmatter, so the
|
||||||
|
# assertion is that regenerating it changes nothing. This replaces
|
||||||
|
# bin/check-index: a generated table cannot drift in either
|
||||||
|
# direction, so there is no separate rule left to check.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/generate-index' --check '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 4. Every skill says which surfaces it ships to, and names only
|
||||||
|
# surfaces that exist — a wrong value silently drops a skill from a
|
||||||
|
# harness instead of erroring.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/check-surfaces' '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 5. A documented session_log call must be one the tool would accept:
|
||||||
|
# session_id is its only required field, and nine skills omitted it.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/check-session-log' '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 6. Every skill says when it stops and hands back (#5). Skills whose
|
||||||
|
# rule is not written yet are listed in bin/escalation-pending.txt,
|
||||||
|
# which only shrinks.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/check-escalation' '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 7. 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 '')
|
||||||
|
# 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
|
||||||
|
echo "ERROR: missing .claude-plugin/plugin.json" >&2; fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Every ${VAR} a live .mcp.json server interpolates must be one the
|
||||||
|
# session env-file defines. An undefined one is silent: the header
|
||||||
|
# goes out empty and the server reports "requires authentication",
|
||||||
|
# which reads as a revoked credential. Exit 2 means the check could
|
||||||
|
# not run (no jq, or no env-file — CI has neither); that is reported
|
||||||
|
# and not fatal, but it is never treated as a pass.
|
||||||
|
if [ -x "{{.TASKFILE_DIR}}/bin/check-mcp-env" ]; then
|
||||||
|
# `|| rc=$?` is load-bearing, not style. Task runs this block under
|
||||||
|
# `sh -e`, so a bare invocation exiting non-zero kills the shell
|
||||||
|
# before `case` is reached -- the tolerance below never ran, and the
|
||||||
|
# gate died with `exit status 2` on the commit that added it.
|
||||||
|
rc=0
|
||||||
|
"{{.TASKFILE_DIR}}/bin/check-mcp-env" "{{.TASKFILE_DIR}}/.mcp.json" "$HOME/.op-env" || rc=$?
|
||||||
|
case $rc in
|
||||||
|
0) ;;
|
||||||
|
2) echo "NOTE: MCP env wiring unverified on this host" >&2 ;;
|
||||||
|
*) fail=1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $fail -ne 0 ]; then
|
||||||
|
echo "check FAILED" >&2; exit 1
|
||||||
|
fi
|
||||||
|
echo "check OK — $count skills, manifest matches tag"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
desc: 'Tag HEAD per Bump-Type in the latest commit footer. Normally CI handles this — run locally only when CI is unavailable.'
|
desc: 'Tag HEAD per Bump-Type in the latest commit footer. Normally CI handles this.'
|
||||||
cmds:
|
cmds:
|
||||||
- |
|
- |
|
||||||
latest=$(git -C "{{.TASKFILE_DIR}}" describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
latest=$(git -C "{{.TASKFILE_DIR}}" describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
@@ -172,26 +181,3 @@ tasks:
|
|||||||
echo "tagging $next (previous $latest, bump=${bump:-patch})"
|
echo "tagging $next (previous $latest, bump=${bump:-patch})"
|
||||||
git -C "{{.TASKFILE_DIR}}" tag -a "$next" -m "release $next"
|
git -C "{{.TASKFILE_DIR}}" tag -a "$next" -m "release $next"
|
||||||
echo "now: git push origin $next"
|
echo "now: git push origin $next"
|
||||||
|
|
||||||
check:
|
|
||||||
desc: 'Sanity check — every skill dir has a SKILL.md, no stray files at root.'
|
|
||||||
cmds:
|
|
||||||
- |
|
|
||||||
missing=0
|
|
||||||
for skill in $(ls -1 "{{.TASKFILE_DIR}}" | grep -Ev "^(Taskfile.yml|install.sh|README.md|SKILLS_INDEX.md|.gitea|.git$)$"); do
|
|
||||||
dir="{{.TASKFILE_DIR}}/${skill}"
|
|
||||||
if [ ! -d "$dir" ]; then
|
|
||||||
echo "ERROR: $skill is not a directory" >&2
|
|
||||||
missing=$((missing + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [ ! -f "$dir/SKILL.md" ]; then
|
|
||||||
echo "ERROR: $dir missing SKILL.md" >&2
|
|
||||||
missing=$((missing + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ $missing -gt 0 ]; then
|
|
||||||
echo "$missing skill(s) failed structure check" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "all skills have SKILL.md"
|
|
||||||
|
|||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# check-escalation — every skill states when it stops and hands back.
|
||||||
|
#
|
||||||
|
# #5 decided this belongs in the body, not the frontmatter: no harness reads
|
||||||
|
# frontmatter beyond name/description, so a `failure_criteria:` key would be a
|
||||||
|
# guarantee nothing honours. The model reads the body.
|
||||||
|
#
|
||||||
|
# #37 then had to define which skills need one, and every obvious answer was
|
||||||
|
# bad: a hardcoded list rots, sniffing the text for "does it write anything" is
|
||||||
|
# unfalsifiable, and a frontmatter flag is the rejected option wearing a hat.
|
||||||
|
#
|
||||||
|
# So there is no acting set. EVERY skill answers the question. A read-only skill
|
||||||
|
# answers it in one line — "read-only; findings are the output, nothing is
|
||||||
|
# handed back" — which turns an absence into a claim someone can disagree with.
|
||||||
|
#
|
||||||
|
# bin/escalation-pending.txt is a ratchet for skills whose real rule has not
|
||||||
|
# been written yet (a stop condition nobody thought about is not worth
|
||||||
|
# enforcing). It is self-cleaning: a listed skill that HAS a section fails, so
|
||||||
|
# the list cannot outlive its reason.
|
||||||
|
#
|
||||||
|
# Usage: check-escalation [checkout-root]
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
|
||||||
|
PENDING="$ROOT/bin/escalation-pending.txt"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
listed() {
|
||||||
|
[ -f "$PENDING" ] || return 1
|
||||||
|
sed 's/#.*//' "$PENDING" | grep -qx "[[:space:]]*$1[[:space:]]*"
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in "$ROOT"/skills/*/; do
|
||||||
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
|
name=$(basename "$dir")
|
||||||
|
|
||||||
|
if grep -q '^## Escalation' "$dir/SKILL.md"; then
|
||||||
|
if listed "$name"; then
|
||||||
|
echo "ERROR: skills/$name has an Escalation section but is still in bin/escalation-pending.txt — remove it from the list" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
listed "$name" && continue
|
||||||
|
echo "ERROR: skills/$name has no '## Escalation' section — say when it stops and hands back, or say it never does" >&2
|
||||||
|
fail=1
|
||||||
|
done
|
||||||
|
|
||||||
|
# A pending entry for a skill that no longer exists is a stale exemption, and a
|
||||||
|
# stale exemption is how a gate quietly stops covering something. Looped without
|
||||||
|
# a pipe on purpose: a `while read` on the right of a pipe runs in a subshell,
|
||||||
|
# so `fail=1` would be set in a process that exits before it is read.
|
||||||
|
if [ -f "$PENDING" ]; then
|
||||||
|
entries=$(sed 's/#.*//' "$PENDING" | tr -d '[:blank:]' | grep -v '^$' || true)
|
||||||
|
for entry in $entries; do
|
||||||
|
[ -f "$ROOT/skills/$entry/SKILL.md" ] && continue
|
||||||
|
echo "ERROR: bin/escalation-pending.txt lists '$entry', which has no skills/$entry/SKILL.md" >&2
|
||||||
|
fail=1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- the ratchet must actually ratchet ---------------------------------------
|
||||||
|
# "This list only shrinks" was asserted in five places and enforced in none. A
|
||||||
|
# traceability probe falsified it in one run: add an unsectioned skill, append it
|
||||||
|
# to the list, gate returns 0. So compare against the list as committed at HEAD —
|
||||||
|
# a convention nothing checks is a convention, and this repo has spent a day on
|
||||||
|
# what those cost.
|
||||||
|
if [ -f "$PENDING" ]; then
|
||||||
|
if ! git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
|
echo "WARN: $ROOT is not a git checkout — cannot verify the pending list only shrank" >&2
|
||||||
|
elif ! git -C "$ROOT" cat-file -e HEAD:bin/escalation-pending.txt 2>/dev/null; then
|
||||||
|
: # first commit introducing the file — nothing to compare against yet
|
||||||
|
else
|
||||||
|
before=$(git -C "$ROOT" show HEAD:bin/escalation-pending.txt 2>/dev/null \
|
||||||
|
| sed 's/#.*//' | tr -d '[:blank:]' | grep -v '^$' | sort -u || true)
|
||||||
|
now=$(sed 's/#.*//' "$PENDING" | tr -d '[:blank:]' | grep -v '^$' | sort -u || true)
|
||||||
|
added=$(printf '%s\n' "$now" | grep -vxF "$before" 2>/dev/null || true)
|
||||||
|
for entry in $added; do
|
||||||
|
[ -n "$entry" ] || continue
|
||||||
|
echo "ERROR: '$entry' was added to bin/escalation-pending.txt — the list only shrinks. Write its '## Escalation' section instead of exempting it." >&2
|
||||||
|
fail=1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $fail
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# check-mcp-env — assert every variable a live MCP server interpolates is one
|
||||||
|
# the session env-file actually defines.
|
||||||
|
#
|
||||||
|
# Why this is a gate and not a comment: the failure is silent. A header of
|
||||||
|
# "Bearer ${FOO}" where FOO is undefined does not error -- it goes out empty,
|
||||||
|
# the server answers 401, and Claude Code reports "requires authentication",
|
||||||
|
# which reads exactly like a revoked credential. On 2026-09-07 that cost a
|
||||||
|
# full investigation on koala before the credential turned out to be fine.
|
||||||
|
#
|
||||||
|
# Servers under `_parked` are exempt by design. Parked means "reachable, but
|
||||||
|
# nothing puts its token in the environment yet" -- flagging those would
|
||||||
|
# report the intended state as a fault.
|
||||||
|
#
|
||||||
|
# Usage: check-mcp-env <mcp.json> <env-file>
|
||||||
|
#
|
||||||
|
# Exit 0 every live server's variables are defined
|
||||||
|
# 1 at least one is not, named on stderr
|
||||||
|
# 2 could not verify (bad usage, unreadable input, jq absent).
|
||||||
|
# Deliberately distinct from 0: an unverified config is not a clean
|
||||||
|
# one, and a gate that cannot run should say so rather than pass.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
CONFIG=${1:-}
|
||||||
|
ENVFILE=${2:-}
|
||||||
|
|
||||||
|
if [ -z "$CONFIG" ] || [ -z "$ENVFILE" ]; then
|
||||||
|
echo "usage: check-mcp-env <mcp.json> <env-file>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "SKIP: jq not installed -- MCP env wiring unverified" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIG" ]; then
|
||||||
|
echo "ERROR: no such config: $CONFIG" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$ENVFILE" ]; then
|
||||||
|
# EXPECTED IN CI, and permanently so. The env-file holds live credentials, so
|
||||||
|
# it is not on the runner and must not be -- which means this check can only
|
||||||
|
# ever verify on a host that has one. infra#368 added jq to the runner image
|
||||||
|
# believing that would let CI verify; it removed the jq branch below and
|
||||||
|
# changed nothing else, because this branch is the binding one.
|
||||||
|
#
|
||||||
|
# Said plainly so nobody concludes CI covers this. It does not, it cannot,
|
||||||
|
# and putting ~/.op-env on the runner to make it would be trading a real
|
||||||
|
# secret for a green tick.
|
||||||
|
echo "ERROR: no such env-file: $ENVFILE -- cannot verify MCP variables" >&2
|
||||||
|
echo " (expected in CI: this check only verifies on a host with ~/.op-env)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Every "<server>\t<VAR>" pair a live server interpolates. `..` walks the whole
|
||||||
|
# value, so a variable is found wherever it is placed, not only under headers.
|
||||||
|
refs=$(jq -r '
|
||||||
|
.mcpServers // {}
|
||||||
|
| to_entries[]
|
||||||
|
| .key as $server
|
||||||
|
| (.value | .. | strings)
|
||||||
|
| select(test("\\$\\{"))
|
||||||
|
| [ scan("\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}")[] ]
|
||||||
|
| .[]
|
||||||
|
| "\($server)\t\(.)"
|
||||||
|
' "$CONFIG" 2>/dev/null) || {
|
||||||
|
echo "ERROR: $CONFIG is not valid JSON -- MCP env wiring unverified" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "$refs" ]; then
|
||||||
|
echo "check-mcp-env: no interpolated variables in live servers"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
checked=0
|
||||||
|
for pair in $(printf '%s\n' "$refs" | tr '\t' ':'); do
|
||||||
|
server=${pair%%:*}
|
||||||
|
var=${pair#*:}
|
||||||
|
checked=$((checked + 1))
|
||||||
|
if grep -qE "^[[:space:]]*(export[[:space:]]+)?$var=" "$ENVFILE"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
printf 'ERROR: MCP server %s interpolates %s, which %s does not define.\n' \
|
||||||
|
"$server" "$var" "$ENVFILE" >&2
|
||||||
|
printf ' It will send an empty Authorization header and report as "requires authentication".\n' >&2
|
||||||
|
fail=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'check-mcp-env: %d interpolated variable(s) across live MCP servers, all defined in %s\n' \
|
||||||
|
"$checked" "$ENVFILE"
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# check-session-log — a skill that documents a session_log call must document a
|
||||||
|
# call the tool would actually accept.
|
||||||
|
#
|
||||||
|
# Nine skills documented the payload and none named `session_id`, which is the
|
||||||
|
# tool's only REQUIRED field. An agent following any of them either failed the
|
||||||
|
# call or invented an id — and invented ids do not group, so the pass-rate the
|
||||||
|
# endpoint computes was being assembled from entries that could not be joined
|
||||||
|
# (#36, found by a pressure test against `debug`).
|
||||||
|
#
|
||||||
|
# Also rejected: `duration_ms: <wall-clock>`. An agent does not observe a clock.
|
||||||
|
# A documented field nobody can honestly populate gets fabricated or silently
|
||||||
|
# dropped, and fabricated numbers in a metrics stream are worse than no numbers.
|
||||||
|
#
|
||||||
|
# Usage: check-session-log [checkout-root]
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
for dir in "$ROOT"/skills/*/; do
|
||||||
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
|
name=$(basename "$dir")
|
||||||
|
|
||||||
|
grep -q 'session_log' "$dir/SKILL.md" || continue
|
||||||
|
|
||||||
|
# Read the PAYLOAD, not the file. The first version of this gate grepped the
|
||||||
|
# whole SKILL.md for "session_id", so a payload missing the field passed as
|
||||||
|
# long as the word appeared anywhere — including in prose explaining that the
|
||||||
|
# field is required. A traceability probe demonstrated it in one sed.
|
||||||
|
payloads=$(grep -n 'session_log' "$dir/SKILL.md" | grep '{' || true)
|
||||||
|
|
||||||
|
if [ -z "$payloads" ]; then
|
||||||
|
# Mentions the tool but documents no call. Nothing to check, and nothing to
|
||||||
|
# get wrong.
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$payloads" | while IFS= read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
body=$(printf '%s' "$line" | sed 's/^[0-9]*://')
|
||||||
|
lineno=$(printf '%s' "$line" | cut -d: -f1)
|
||||||
|
|
||||||
|
printf '%s' "$body" | grep -q 'session_id' \
|
||||||
|
|| echo "ERROR: skills/$name:$lineno documents a session_log payload with no session_id — the tool's only required field" >&2
|
||||||
|
|
||||||
|
# Any spelling of the key, quoted or not, followed by anything that is not a
|
||||||
|
# bare number. A placeholder nobody can fill gets fabricated or dropped; a
|
||||||
|
# real measurement is fine.
|
||||||
|
if printf '%s' "$body" | grep -Eq '"?duration_ms"?[[:space:]]*:[[:space:]]*[^0-9,}[:space:]]'; then
|
||||||
|
echo "ERROR: skills/$name:$lineno documents duration_ms as something other than a real measurement — an agent does not observe a clock; omit it" >&2
|
||||||
|
fi
|
||||||
|
done > "$ROOT/.session_log_errors.$$" 2>&1
|
||||||
|
|
||||||
|
if [ -s "$ROOT/.session_log_errors.$$" ]; then
|
||||||
|
cat "$ROOT/.session_log_errors.$$" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
rm -f "$ROOT/.session_log_errors.$$"
|
||||||
|
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
|
||||||
Executable
+379
@@ -0,0 +1,379 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# doctor — report skills-library drift on this host.
|
||||||
|
#
|
||||||
|
# Written because every failure this fleet actually hit was SILENT: 243
|
||||||
|
# dangling symlinks hidden from git by the **/.claude/skills/ ignore rule, a
|
||||||
|
# checkout that still resolved but was 20 skills behind, a loose .md that
|
||||||
|
# loaded in no harness, and two machines where the library was simply absent.
|
||||||
|
# None of it errored. Exit status here is the signal that was missing.
|
||||||
|
#
|
||||||
|
# doctor human report, non-zero exit on drift
|
||||||
|
# doctor --quiet print only when something is wrong (SessionStart hook)
|
||||||
|
# doctor --json machine-readable findings
|
||||||
|
# doctor --online ALSO ask origin for the latest tag (one network call)
|
||||||
|
#
|
||||||
|
# POSIX sh on purpose: it runs on macOS and Linux, before any toolchain is
|
||||||
|
# guaranteed present, and inside a session-start hook where latency shows.
|
||||||
|
# It does no network I/O and never shells out to git for a fetch.
|
||||||
|
#
|
||||||
|
# Test seams: $HOME drives harness paths, $DOCTOR_CHECKOUT names the checkout.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
QUIET=0
|
||||||
|
JSON=0
|
||||||
|
ONLINE=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--quiet) QUIET=1 ;;
|
||||||
|
--json) JSON=1 ;;
|
||||||
|
--online) ONLINE=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0 ;;
|
||||||
|
*) printf 'doctor: unknown option %s\n' "$arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
SELF_CHECKOUT=$(CDPATH= cd -- "$SELF_DIR/.." && pwd)
|
||||||
|
|
||||||
|
# The canonical checkout is whatever ~/.agents/skills resolves to — NOT the
|
||||||
|
# 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 [ -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"
|
||||||
|
fi
|
||||||
|
SKILLS_DIR="$CHECKOUT/skills"
|
||||||
|
|
||||||
|
findings=0
|
||||||
|
report=""
|
||||||
|
|
||||||
|
# add <severity> <check> <detail>
|
||||||
|
add() {
|
||||||
|
findings=$((findings + 1))
|
||||||
|
if [ "$JSON" -eq 1 ]; then
|
||||||
|
report="${report}{\"severity\":\"$1\",\"check\":\"$2\",\"detail\":\"$(printf '%s' "$3" | sed 's/"/\\"/g')\"},"
|
||||||
|
else
|
||||||
|
report="${report}$1 $2: $3
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 -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 ----------------------------------------------------
|
||||||
|
if [ ! -d "$SKILLS_DIR" ]; then
|
||||||
|
add FAIL checkout "no skills/ directory at $CHECKOUT"
|
||||||
|
# Nothing downstream can be judged without it.
|
||||||
|
printf 'FAIL checkout: no skills/ directory at %s\n' "$CHECKOUT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED=$(count_skills "$SKILLS_DIR")
|
||||||
|
[ "$EXPECTED" -gt 0 ] || add FAIL checkout "skills/ holds no SKILL.md at all"
|
||||||
|
|
||||||
|
# --- stray entries in the checkout -----------------------------------------
|
||||||
|
# A loose foo.md where foo/SKILL.md was meant loads in NO harness and errors
|
||||||
|
# in none either. That is exactly how secret-hygiene.md sat dead for months.
|
||||||
|
for entry in "$SKILLS_DIR"/*; do
|
||||||
|
[ -e "$entry" ] || continue
|
||||||
|
name=$(basename "$entry")
|
||||||
|
if [ ! -d "$entry" ]; then
|
||||||
|
add FAIL stray "$SKILLS_DIR/$name is not a directory — it loads nowhere"
|
||||||
|
elif [ ! -f "$entry/SKILL.md" ]; then
|
||||||
|
add FAIL stray "$SKILLS_DIR/$name has no SKILL.md"
|
||||||
|
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 ---------------------------------------------------------
|
||||||
|
# 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/.config/crush/skills" \
|
||||||
|
"$HOME/.config/opencode/skills"
|
||||||
|
do
|
||||||
|
[ -L "$dir" ] || [ -d "$dir" ] || continue
|
||||||
|
|
||||||
|
if [ -L "$dir" ] && [ ! -e "$dir" ]; then
|
||||||
|
add FAIL dangling "$dir -> $(readlink "$dir") (target does not exist)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A real directory where a link belongs holds content nothing manages: it
|
||||||
|
# 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 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
|
||||||
|
|
||||||
|
resolved=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd -P)
|
||||||
|
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/.config/crush/skills" \
|
||||||
|
"$HOME/.config/opencode/skills" "$HOME/.claude/skills"
|
||||||
|
do
|
||||||
|
[ -d "$dir" ] || continue
|
||||||
|
# Plain glob, not find|while: a while loop on the right of a pipe runs in a
|
||||||
|
# subshell, so add() would increment a counter that dies with it and doctor
|
||||||
|
# would exit 0 having found faults.
|
||||||
|
for link in "$dir"/*; do
|
||||||
|
[ -L "$link" ] || continue
|
||||||
|
[ -e "$link" ] && continue
|
||||||
|
add FAIL dangling "$link -> $(readlink "$link") (target does not exist)"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Claude Code: plugin vs leftover per-skill symlinks ---------------------
|
||||||
|
# Plugin skills do NOT override same-named user skills — both stay visible. So
|
||||||
|
# leftover links from the pre-plugin era mean every skill appears twice.
|
||||||
|
CLAUDE_SKILLS="$HOME/.claude/skills"
|
||||||
|
if [ -d "$CLAUDE_SKILLS" ]; then
|
||||||
|
legacy=$(find "$CLAUDE_SKILLS"/ -maxdepth 1 -type l 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
if [ "$legacy" -gt 0 ]; then
|
||||||
|
add FAIL legacy "$CLAUDE_SKILLS holds $legacy per-skill symlink(s) from the pre-plugin wiring — remove them or every skill loads twice"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Two delivery mechanisms means two copies that can drift apart: the plugin
|
||||||
|
# (Claude Code) and the checkout the other harnesses link to. Same host,
|
||||||
|
# different skill sets, no error — compare them.
|
||||||
|
# Skipped when DOCTOR_CHECKOUT is set: that scopes doctor to an explicit
|
||||||
|
# target, so comparing it against whatever copy the script lives in is
|
||||||
|
# meaningless rather than informative.
|
||||||
|
if [ -z "${DOCTOR_CHECKOUT:-}" ] && [ -f "$SELF_DIR/../.claude-plugin/plugin.json" ] \
|
||||||
|
&& [ "$SELF_CHECKOUT" != "$CHECKOUT" ]; then
|
||||||
|
plugin_count=$(count_skills "$SELF_CHECKOUT/skills")
|
||||||
|
if [ "$plugin_count" != "$EXPECTED" ]; then
|
||||||
|
add FAIL drift "Claude Code plugin serves $plugin_count skills, the other harnesses see $EXPECTED — the two copies have diverged"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTALLED="$HOME/.claude/plugins/installed_plugins.json"
|
||||||
|
if [ -f "$INSTALLED" ]; then
|
||||||
|
grep -q '"skills@mathias"' "$INSTALLED" 2>/dev/null \
|
||||||
|
|| 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
|
||||||
|
|
||||||
|
# --- optional: the release this host has never fetched ----------------------
|
||||||
|
# Every check above compares copies already present, so a host whose checkout,
|
||||||
|
# marketplace clone and installed plugin all agree reads as clean while sitting
|
||||||
|
# a release behind origin (#38). Closing that needs a network call, which the
|
||||||
|
# default run must not make — hence opt-in, for a scheduled per-host job rather
|
||||||
|
# than the SessionStart hook.
|
||||||
|
#
|
||||||
|
# $DOCTOR_LATEST_TAG_CMD is the test seam; it prints the newest remote tag.
|
||||||
|
if [ "$ONLINE" -eq 1 ]; then
|
||||||
|
if [ -n "${DOCTOR_LATEST_TAG_CMD:-}" ]; then
|
||||||
|
remote_tag=$(eval "$DOCTOR_LATEST_TAG_CMD" 2>/dev/null | tail -1 | tr -d '[:space:]')
|
||||||
|
else
|
||||||
|
# The URL is never printed: a remote rewritten by url.insteadOf can expand
|
||||||
|
# into a live token.
|
||||||
|
origin=$(git -C "$CHECKOUT" config --get remote.origin.url 2>/dev/null || true)
|
||||||
|
if [ -n "$origin" ]; then
|
||||||
|
remote_tag=$(git ls-remote --tags --refs "$origin" 2>/dev/null \
|
||||||
|
| sed 's#.*refs/tags/##' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1 | tr -d '[:space:]')
|
||||||
|
else
|
||||||
|
remote_tag=''
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$remote_tag" ]; then
|
||||||
|
# Not drift. A host that cannot reach origin has an unanswered question,
|
||||||
|
# not a stale library, and saying otherwise would make the two look alike.
|
||||||
|
add WARN offline "could not reach origin for the latest tag — this run proves nothing about staleness"
|
||||||
|
else
|
||||||
|
remote_v=${remote_tag#v}
|
||||||
|
if [ -n "$available_v" ] && [ "$remote_v" != "$available_v" ] \
|
||||||
|
&& [ "$(newer_version "$available_v" "$remote_v")" = "$remote_v" ]; then
|
||||||
|
add FAIL behind "this host has $available_v; origin has released $remote_v — run 'task update' and '/plugin update skills@mathias'"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- toolchain: golangci-lint must be built with Go >= GOROOT ---------------
|
||||||
|
# infra#375, second recurrence. golangci-lint embeds the go/types export-data
|
||||||
|
# reader from the Go it was BUILT with. When GOROOT is newer, that reader
|
||||||
|
# cannot decode the stdlib and the linter dies:
|
||||||
|
#
|
||||||
|
# panic: file requires newer Go version go1.27 (application built with go1.26)
|
||||||
|
#
|
||||||
|
# An earlier form printed a typecheck error naming the user's own file, which
|
||||||
|
# sends people to debug code that is fine. It cost time in two repos before
|
||||||
|
# anyone recognised it as a host fault rather than a repo fault.
|
||||||
|
#
|
||||||
|
# It is invisible to CI -- the runner image has matching versions -- so it
|
||||||
|
# breaks exactly the local `task check` that AGENT.md requires before a push,
|
||||||
|
# and nothing else. That is why it belongs in doctor: the signal was missing,
|
||||||
|
# not the fix.
|
||||||
|
#
|
||||||
|
# Fails open on anything unparseable. A host with no Go toolchain is a normal
|
||||||
|
# host, not a broken one, and doctor runs in a SessionStart hook everywhere.
|
||||||
|
ver_lt() {
|
||||||
|
# 0 when $1 < $2 on major.minor. Deliberately not sort -V: this is POSIX sh
|
||||||
|
# that also runs on macOS, and the export-data format is minor-versioned, so
|
||||||
|
# patch level does not enter into it.
|
||||||
|
_a=${1%%.*}; _ar=${1#*.}; _am=${_ar%%.*}
|
||||||
|
_b=${2%%.*}; _br=${2#*.}; _bm=${_br%%.*}
|
||||||
|
case "$_a$_am$_b$_bm" in *[!0-9]*) return 1 ;; esac
|
||||||
|
[ "$_a" -lt "$_b" ] && return 0
|
||||||
|
[ "$_a" -gt "$_b" ] && return 1
|
||||||
|
[ "$_am" -lt "$_bm" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
if command -v golangci-lint >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
|
||||||
|
lint_go=$(golangci-lint --version 2>/dev/null | sed -n 's/.*built with go\([0-9][0-9.]*\).*/\1/p' | head -1)
|
||||||
|
root_go=$(go version 2>/dev/null | sed -n 's/.*go version go\([0-9][0-9.]*\).*/\1/p' | head -1)
|
||||||
|
if [ -n "$lint_go" ] && [ -n "$root_go" ] && ver_lt "$lint_go" "$root_go"; then
|
||||||
|
add FAIL toolchain "golangci-lint built with go$lint_go but GOROOT is go$root_go — lint panics on every Go repo; reinstall it under the current toolchain (infra#375)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- output -----------------------------------------------------------------
|
||||||
|
if [ "$JSON" -eq 1 ]; then
|
||||||
|
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"
|
||||||
|
elif [ "$QUIET" -eq 0 ]; then
|
||||||
|
printf 'skills doctor: clean — %s skills, checkout %s\n' "$EXPECTED" "$CHECKOUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$findings" -eq 0 ]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Skills whose escalation rule has not been written yet (#37).
|
||||||
|
#
|
||||||
|
# This list only shrinks. A skill here is exempt from the '## Escalation'
|
||||||
|
# requirement until someone writes its real stop conditions; a skill that HAS a
|
||||||
|
# section and is still listed fails the gate, so the list cannot outlive its
|
||||||
|
# reason.
|
||||||
|
#
|
||||||
|
# The ones already done were the advisory and read-only skills, where the honest
|
||||||
|
# answer is a line. What is left is the acting set — skills that write files,
|
||||||
|
# dispatch agents, touch credentials or move releases — and each of those needs
|
||||||
|
# a rule someone thought about, not a rote paragraph. Per #19, one at a time.
|
||||||
|
|
||||||
|
atdd
|
||||||
|
dream
|
||||||
|
experiment-spec
|
||||||
|
feature-spec
|
||||||
|
gitea-ci
|
||||||
|
own-the-task
|
||||||
|
planning
|
||||||
|
playwright
|
||||||
|
problem-analysis
|
||||||
|
refactoring
|
||||||
|
regulatory-risk-assessment
|
||||||
|
secret-hygiene
|
||||||
|
session-retrospective
|
||||||
|
spec-driven-dev
|
||||||
|
superpowers-brainstorming
|
||||||
|
superpowers-dispatching-parallel-agents
|
||||||
|
superpowers-executing-plans
|
||||||
|
superpowers-finishing-a-development-branch
|
||||||
|
superpowers-receiving-code-review
|
||||||
|
superpowers-requesting-code-review
|
||||||
|
superpowers-subagent-driven-development
|
||||||
|
superpowers-using-git-worktrees
|
||||||
|
superpowers-using-superpowers
|
||||||
|
superpowers-verification-before-completion
|
||||||
|
superpowers-writing-plans
|
||||||
|
superpowers-writing-skills
|
||||||
|
tdd
|
||||||
|
trainer
|
||||||
|
user-stories
|
||||||
|
web-shot
|
||||||
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]
|
||||||
|
}'
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# should-skip-release — reads a commit message on stdin. Exits 0 when the
|
||||||
|
# release tagger should skip this commit, 1 when it should tag.
|
||||||
|
#
|
||||||
|
# Skipping is opt-in via a git trailer:
|
||||||
|
#
|
||||||
|
# Skip-Release: true
|
||||||
|
#
|
||||||
|
# It is a TRAILER, matched anchored to its own line, deliberately. The first
|
||||||
|
# version searched the whole message for a bracketed marker as a substring,
|
||||||
|
# and the very commit that introduced the manifest-bump step described that
|
||||||
|
# marker in its body — so the tagger read its own documentation as an
|
||||||
|
# instruction and skipped the release. Success, no tag, no bump, no error
|
||||||
|
# (run 15, 2026-08-23). A marker you cannot mention without triggering is a
|
||||||
|
# footgun, not a mechanism.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
msg=$(cat)
|
||||||
|
|
||||||
|
if printf '%s\n' "$msg" | grep -qiE '^[[:space:]]*Skip-Release:[[:space:]]*true[[:space:]]*$'; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
---
|
|
||||||
name: debug
|
|
||||||
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.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Debug
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Debugging is hypothesis generation, not fix generation. The first instinct — "try this and see if it works" — wastes time and teaches nothing. A disciplined debug session produces a small set of ranked, falsifiable hypotheses, each with a concrete verification command. The fix comes after one hypothesis is confirmed, not before.
|
|
||||||
|
|
||||||
**Core principle:** A hypothesis you cannot verify in one command is not a hypothesis — it is a guess.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Any test failure whose cause is not immediately obvious
|
|
||||||
- Any production error or unexpected behavior
|
|
||||||
- Any bug report from a user or stakeholder
|
|
||||||
- Before reaching for a debugger or adding print statements: form hypotheses first
|
|
||||||
|
|
||||||
**Do not use for:**
|
|
||||||
- Compile errors with clear messages (just fix the typo)
|
|
||||||
- Already-diagnosed bugs where you know the cause (go straight to TDD with a regression test)
|
|
||||||
|
|
||||||
## Iron Laws
|
|
||||||
|
|
||||||
1. **Never suggest "try X and see what happens."** Every hypothesis must have a specific expected outcome if correct.
|
|
||||||
2. **Generate 3-5 hypotheses, ordered by likelihood (most likely first).** Fewer than 3 means you stopped thinking; more than 5 means you are not prioritizing.
|
|
||||||
3. **Diagnose only — do not fix in this skill.** The fix happens in a separate TDD cycle (load `tdd` skill) once a hypothesis is confirmed.
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
### Step 1: Read the failure
|
|
||||||
|
|
||||||
Before forming hypotheses:
|
|
||||||
- Read the full error message and stack trace, not just the headline
|
|
||||||
- Read the file where the failure originated, around the failing line
|
|
||||||
- If the failure is from a test, read the test and the code under test
|
|
||||||
- Identify the **failure mode** — what actually went wrong (e.g. "nil pointer dereference in goroutine spawned by handler") not just what the error says ("runtime error")
|
|
||||||
|
|
||||||
### Step 2: Generate hypotheses
|
|
||||||
|
|
||||||
For each hypothesis, capture three things:
|
|
||||||
|
|
||||||
- **Mechanism:** what specific code path or state would produce this exact failure
|
|
||||||
- **Verification:** the single command or file inspection that confirms or denies it
|
|
||||||
- **Expected outcome if correct:** the specific output you would see
|
|
||||||
|
|
||||||
Order by likelihood. The most likely cause is hypothesis 1.
|
|
||||||
|
|
||||||
### Step 3: Output
|
|
||||||
|
|
||||||
Use this format:
|
|
||||||
|
|
||||||
```
|
|
||||||
HYPOTHESIS 1 (likelihood: high): <mechanism in one sentence>
|
|
||||||
VERIFY: <exact command or file:line to inspect>
|
|
||||||
EXPECTED IF CORRECT: <specific output, value, or condition>
|
|
||||||
|
|
||||||
HYPOTHESIS 2 (likelihood: medium): <mechanism>
|
|
||||||
VERIFY: <exact command>
|
|
||||||
EXPECTED IF CORRECT: <specific output>
|
|
||||||
|
|
||||||
[... up to 5 ...]
|
|
||||||
|
|
||||||
RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
|
||||||
```
|
|
||||||
|
|
||||||
End with the recommendation, not a fix.
|
|
||||||
|
|
||||||
## Worked Example
|
|
||||||
|
|
||||||
**Failure:**
|
|
||||||
```
|
|
||||||
--- FAIL: TestInvoiceParser_HandlesEmptyPDF (0.00s)
|
|
||||||
parser_test.go:47: panic: runtime error: index out of range [0] with length 0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output:**
|
|
||||||
|
|
||||||
```
|
|
||||||
HYPOTHESIS 1 (likelihood: high): parser indexes into pages[0] before checking len(pages) > 0; empty PDFs produce a zero-page document
|
|
||||||
VERIFY: rg -n 'pages\[0\]' internal/parser/
|
|
||||||
EXPECTED IF CORRECT: at least one site reads pages[0] without a preceding length check
|
|
||||||
|
|
||||||
HYPOTHESIS 2 (likelihood: medium): the test fixture is a zero-byte file rather than a valid empty PDF; pdf library returns nil pages slice instead of empty
|
|
||||||
VERIFY: ls -la testdata/empty.pdf && file testdata/empty.pdf
|
|
||||||
EXPECTED IF CORRECT: file size 0 bytes or "data" rather than "PDF document"
|
|
||||||
|
|
||||||
HYPOTHESIS 3 (likelihood: low): a recently changed dependency reordered the page-extraction API; pages[0] now refers to metadata, not content
|
|
||||||
VERIFY: git log --oneline -10 -- go.sum | grep -i pdf
|
|
||||||
EXPECTED IF CORRECT: a pdf-library bump in the last few commits
|
|
||||||
|
|
||||||
RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
|
|
||||||
| Anti-Pattern | Why It Fails |
|
|
||||||
|---|---|
|
|
||||||
| "Maybe try restarting the service" | Not a mechanism. Not falsifiable. Teaches nothing if it works. |
|
|
||||||
| "Could be a race condition" | Mechanism without specifics. Which two operations race? On what state? |
|
|
||||||
| "Let me add some print statements and see" | Skips hypothesis generation. Generates noise, not understanding. |
|
|
||||||
| Single hypothesis presented as fact | If you are sure, write the regression test, do not run a debug skill. |
|
|
||||||
| Mixing hypotheses with fix suggestions | The skill is diagnose-only. The fix is a TDD task on its hypothesis. |
|
|
||||||
|
|
||||||
## Brain MCP Integration
|
|
||||||
|
|
||||||
The brain holds prior debug sessions across the project. Use it to skip rediscovering known failure modes.
|
|
||||||
|
|
||||||
**At debug start:**
|
|
||||||
- Run `brain_query` with the error message snippet + the package name. Past sessions may have logged identical or similar failures with their resolved hypotheses.
|
|
||||||
|
|
||||||
**After a hypothesis is confirmed:**
|
|
||||||
- Run `brain_write` with the failure signature → confirmed mechanism. Future debug sessions on the same area get the answer immediately.
|
|
||||||
|
|
||||||
**Never:**
|
|
||||||
- Run `brain_write` for an unconfirmed hypothesis. Speculation in the brain pollutes future queries.
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
|
|
||||||
Call `session_log` once at the end of every phase to record the outcome.
|
|
||||||
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
|
||||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
|
||||||
|
|
||||||
**At end of each phase:**
|
|
||||||
- `session_log` with `{skill: "debug", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
|
||||||
|
|
||||||
**Phases for this skill:** read-failure, generate-hypotheses, output
|
|
||||||
|
|
||||||
**Status semantics:**
|
|
||||||
- `pass` — the phase's intended outcome was reached.
|
|
||||||
- `fail` — the phase's intended outcome was NOT reached.
|
|
||||||
- `skip` — phase was skipped intentionally.
|
|
||||||
|
|
||||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
|
||||||
|
|
||||||
## Mode 2 Routing Note
|
|
||||||
|
|
||||||
This skill produces high-volume mechanical output (hypothesis enumeration) and is a candidate for Mode 2 routing to a local model in the future. Until Plan 6 ships the routing pod, treat as Mode 1 only. The hypothesis format and discipline are identical regardless of which model generates them.
|
|
||||||
|
|
||||||
## Cross-References
|
|
||||||
|
|
||||||
- After a hypothesis is confirmed, load `tdd` skill — write a failing regression test that proves the bug exists, then fix it.
|
|
||||||
- For test failures specifically caused by mock-vs-real divergence, also load `tdd/references/testing-anti-patterns.md`.
|
|
||||||
- Load `code-review` skill if the diagnosis surfaces a structural issue (god object, shotgun surgery) rather than a pointwise bug.
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "sh \"${CLAUDE_PLUGIN_ROOT}/bin/doctor\" --quiet",
|
||||||
|
"timeout": 5,
|
||||||
|
"statusMessage": "Checking skills wiring..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-task-ritual.sh\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/secret-guard.sh\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Surfaces rule 0 on every prompt so the pre-task ritual is visible in context.
|
||||||
|
echo "Rule 0 (non-negotiable): brain_query → load skill → failing test → state success criterion"
|
||||||
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# PreToolUse hook (Bash matcher) — hard-blocks command patterns that reveal
|
||||||
|
# secret values into the transcript, rather than relying on remembering an
|
||||||
|
# existence-only check every time.
|
||||||
|
#
|
||||||
|
# Triggered by two real leaks in one session (2026-07-30):
|
||||||
|
# 1. `op item get <name> --format json` — JSON mode has NO masking, unlike
|
||||||
|
# the human-readable table view (`op item get <name>` alone masks
|
||||||
|
# concealed fields with bullets by default). Printed a live Gitea MCP
|
||||||
|
# token into the transcript.
|
||||||
|
# 2. `env | grep -i gitea` (infra#228, 2026-07-27) — same shape: an
|
||||||
|
# inspection command that dumps values instead of checking existence.
|
||||||
|
#
|
||||||
|
# Policy: block ALL direct reads of secret material via these command
|
||||||
|
# shapes, unconditionally, regardless of flags. Existence/rotation must go
|
||||||
|
# through a wrapper script (e.g. infra's scripts/op-safe.sh, scripts/rotate-
|
||||||
|
# *.sh) that strips values before anything reaches stdout — those scripts
|
||||||
|
# call the same underlying commands internally, but as a subprocess of the
|
||||||
|
# script, never as a literal Bash tool call the model composes and can typo.
|
||||||
|
#
|
||||||
|
# Reads PreToolUse hook JSON on stdin, decides allow/deny for the Bash tool.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT="$(cat)"
|
||||||
|
TOOL_NAME="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')"
|
||||||
|
[ "$TOOL_NAME" = "Bash" ] || { printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'; exit 0; }
|
||||||
|
|
||||||
|
CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')"
|
||||||
|
|
||||||
|
deny() {
|
||||||
|
local reason="$1"
|
||||||
|
jq -n --arg reason "$reason" \
|
||||||
|
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $reason}, systemMessage: $reason}'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1Password CLI raw reads — block unconditionally, any flags.
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+item\s+get\b'; then
|
||||||
|
deny "Blocked: 'op item get' prints secret values to stdout (JSON mode has no masking at all; even table mode can leak via --fields/--reveal). Use a wrapper that strips .fields[].value before output (e.g. scripts/op-safe.sh exists|meta), or op item edit to WRITE without ever printing the value."
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+item\s+list\b'; then
|
||||||
|
deny "Blocked: 'op item list' can include concealed field previews. Use a wrapper that filters to titles only (e.g. scripts/op-safe.sh list)."
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+read\b'; then
|
||||||
|
deny "Blocked: 'op read' prints the raw secret value directly. Consume it inside a subprocess only (op run -- <cmd>, or a script that never echoes/captures it to stdout)."
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+document\s+get\b'; then
|
||||||
|
deny "Blocked: 'op document get' prints document contents (may contain secrets) to stdout. Use a wrapper script instead."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Raw env/secret-file dumps (infra#228/#229 root cause).
|
||||||
|
#
|
||||||
|
# `env` is a dump only when nothing but options and VAR=value assignments
|
||||||
|
# follow it. `env -u VAR cmd` and `env FOO=bar cmd` EXEC a command with a
|
||||||
|
# modified environment and print nothing themselves — piping those into grep
|
||||||
|
# filters the command's own output, not the environment (#25). The earlier
|
||||||
|
# pattern matched any `env` token followed anywhere later by a pipe into
|
||||||
|
# grep, which blocked the documented 1Password write path
|
||||||
|
# (`env -u OP_SERVICE_ACCOUNT_TOKEN op item create`) and cost a workaround.
|
||||||
|
# Denying only the "no command word follows" shape also catches `env FOO=bar`
|
||||||
|
# with no command, which the two previous rules let through.
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|[[:space:]])env([[:space:]]+-[iu][[:space:]]*[A-Za-z_][A-Za-z0-9_]*|[[:space:]]+-[iu]|[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*)*[[:space:]]*($|[|;&])'; then
|
||||||
|
deny "Blocked: this 'env' prints every variable's value, including secrets (no command word follows it, so it dumps rather than execs). Use an existence-only check: [ -n \"\$VAR\" ] && echo set. Note 'env -u VAR cmd' and 'env FOO=bar cmd' are allowed — they run a command."
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
|
||||||
|
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)cat\b[^;&|]*\.env'; then
|
||||||
|
deny "Blocked: cat of a .env file prints secret values. Use: grep -c '=' file.env (count only) or [ -s file.env ] && echo present"
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)cat\b[^;&|]*(\.llmkeys|\.op-env|\.op-agent-write\.env)\b'; then
|
||||||
|
deny "Blocked: cat of a known secrets file prints raw values. Use: grep -o '^[A-Z_]*=' file (key names only, never the value)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# kubectl secrets — -o yaml/json prints base64 (trivially reversible).
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)kubectl\b[^;&|]*\bget\s+secrets?\b[^;&|]*(-o|--output)[[:space:]=]*(yaml|json)'; then
|
||||||
|
deny "Blocked: 'kubectl get secret -o yaml/json' prints base64-encoded values (trivially reversible, same as plaintext). Use 'kubectl describe secret <name>' (shows keys + byte lengths only, never values) for existence checks."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gitea app.ini — mixes plain config ([queue]/[indexer]/[repository]) with
|
||||||
|
# live secrets ([security].INTERNAL_TOKEN/SECRET_KEY, [oauth2].JWT_SECRET,
|
||||||
|
# [server].LFS_JWT_SECRET) in the SAME file, auto-generated on install and
|
||||||
|
# not git/SOPS-tracked. A context-range grep (-A/-B/-C) doesn't respect
|
||||||
|
# section boundaries and silently pulls a secret line into stdout even when
|
||||||
|
# targeting an unrelated section. Root cause of a real leak, infra sprint
|
||||||
|
# 2026-08-04 (`kubectl exec ... cat app.ini | grep -A15 "^\[queue\]"` pulled
|
||||||
|
# through into [oauth2].JWT_SECRET).
|
||||||
|
# Tight on purpose: requires "cat" immediately followed by a bare path-shaped
|
||||||
|
# token ending in app.ini (no intervening whitespace) so this only fires on
|
||||||
|
# an actual invocation ('cat /data/gitea/conf/app.ini', 'cat app.ini') and
|
||||||
|
# never on prose that merely mentions both words, e.g. a commit message
|
||||||
|
# describing this exact guard (that false-positived once while writing the
|
||||||
|
# fix — commands are matched as raw strings, heredoc bodies included).
|
||||||
|
if printf '%s' "$CMD" | grep -Eq '\bcat[[:space:]]+[A-Za-z0-9_./-]*app\.ini\b'; then
|
||||||
|
deny "Blocked: cat of Gitea's app.ini prints secrets mixed into the same file ([security]/[oauth2]/[server].LFS_JWT_SECRET). Use exact-key checks: grep -c '^KEY = ' app.ini (existence only) — never cat the whole file."
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eq '\bgrep\b[^;&|]*-[A-Za-z]*[ABC][A-Za-z]*[[:space:]]*[0-9][^;&|]*app\.ini\b'; then
|
||||||
|
deny "Blocked: grep -A/-B/-C context flags against app.ini can bleed past section boundaries into [security]/[oauth2]/[server] secrets, even when targeting an unrelated section (this is exactly how the 2026-08-04 leak happened). Use an exact anchor instead: grep '^KEY = ' app.ini, or bound to one section with awk '/^\\[section\\]/,/^\\[/' and confirm the section has no secret keys first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sops decrypt output inspected directly — infra sweep-sprint incident,
|
||||||
|
# 2026-08-05: `sops -d ntfy-tokens.enc.yaml > /tmp/plain.yaml` (safe,
|
||||||
|
# redirected to a file) immediately followed by `cat /tmp/plain.yaml | head`
|
||||||
|
# in the SAME command block printed a live ntfy token into the transcript.
|
||||||
|
# Re-encrypting a secret (e.g. switching a file to data-only encryption per
|
||||||
|
# infra#177's pattern) requires decrypting it locally at some point — that
|
||||||
|
# redirect-to-file step alone is fine, the leak is any print command touching
|
||||||
|
# the plaintext afterward. Block the combination unconditionally rather than
|
||||||
|
# trying to track which file a decrypt wrote to across separate tool calls.
|
||||||
|
if printf '%s' "$CMD" | grep -Eq '\bsops\b[^;&|]*(-d\b|--decrypt\b|[[:space:]]decrypt\b)' \
|
||||||
|
&& printf '%s' "$CMD" | grep -Eq '\b(cat|head|tail|less|more|bat|od|xxd)\b'; then
|
||||||
|
deny "Blocked: this command decrypts a sops-encrypted file AND runs a print command (cat/head/tail/etc) in the same invocation — that's exactly the 2026-08-05 ntfy-token leak shape. Decrypt to a temp file (sops -d file > /tmp/x, fine on its own), do whatever transform is needed WITHOUT printing it (e.g. sops -e --encrypted-regex '^(data|stringData)\$' /tmp/x > newfile), then shred -u the temp file. Never cat/head/tail a sops-decrypted file, even to 'just check a few lines'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# base64 transform of a secret-shaped env var — the documented 2026-06-11
|
||||||
|
# 'defeats op run masking' bypass.
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '\$\{?[A-Za-z0-9_]*(TOKEN|SECRET|PASSWORD|API_KEY|_KEY|DSN|CREDENTIAL|PAT)[A-Za-z0-9_]*\}?[^;&|]*\|[^;&|]*base64'; then
|
||||||
|
deny "Blocked: piping a secret-shaped variable through base64 to inspect it. base64 is an encoding, not masking — op run's own masking only recognizes the raw value, so this is a known bypass (2026-06-11 incident). Never transform a secret to look at it."
|
||||||
|
fi
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi 'base64[^;&|]*<<<\s*"?\$\{?[A-Za-z0-9_]*(TOKEN|SECRET|PASSWORD|API_KEY|_KEY|DSN|CREDENTIAL|PAT)'; then
|
||||||
|
deny "Blocked: base64 <<< of a secret-shaped variable. Same bypass as above — never transform a secret to inspect it."
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "allow"}}'
|
||||||
+223
-90
@@ -5,25 +5,81 @@
|
|||||||
# curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
# curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
||||||
#
|
#
|
||||||
# Pinned to a specific tag:
|
# Pinned to a specific tag:
|
||||||
# SKILLS_REF=v0.1.0 bash install.sh
|
# SKILLS_REF=v0.4.0 bash install.sh
|
||||||
#
|
#
|
||||||
# Idempotent: re-running pulls the latest changes for the configured ref
|
# Idempotent: re-running pulls the latest changes for the configured ref
|
||||||
# (default: main) and re-wires symlinks. Existing correct links are
|
# (default: main) and re-wires the links.
|
||||||
# left alone; mismatched ones are replaced.
|
#
|
||||||
|
# Claude Code is NOT wired here. It installs this repo as a plugin from the
|
||||||
|
# marketplace this repo also hosts:
|
||||||
|
#
|
||||||
|
# /plugin marketplace add https://git.d-ma.be/mathias/skills.git
|
||||||
|
# /plugin install skills@mathias
|
||||||
|
#
|
||||||
|
# That path carries the hooks and MCP server definitions too, which symlinks
|
||||||
|
# cannot. This script covers the harnesses with no plugin system: Crush, Pi,
|
||||||
|
# OpenCode, and Antigravity.
|
||||||
|
#
|
||||||
|
# Wiring model — one managed directory, one link per harness:
|
||||||
|
#
|
||||||
|
# ~/.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
|
||||||
|
#
|
||||||
|
# Antigravity has no symlink-directory discovery — it reads a path-entries
|
||||||
|
# JSON instead (~/.gemini/config/skills.json:
|
||||||
|
# {"entries":[{"path":"..."}]}). One entry points at ~/.agents/skills, so it
|
||||||
|
# sees exactly the same surfaces-filtered, collision-resolved set every other
|
||||||
|
# harness does. Requires jq; skipped with a warning if jq is unavailable —
|
||||||
|
# every other target in this script stays dependency-free on purpose.
|
||||||
|
#
|
||||||
|
# ~/.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
|
||||||
|
# fleet whose workspace is shared between macOS (/Users/...) and Linux
|
||||||
|
# (/home/...), an absolute link stored there is broken on one side by
|
||||||
|
# construction. See mathias/skills#23.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_URL="${SKILLS_REPO_URL:-https://git.d-ma.be/mathias/skills.git}"
|
REPO_URL="${SKILLS_REPO_URL:-https://git.d-ma.be/mathias/skills.git}"
|
||||||
REF="${SKILLS_REF:-main}"
|
REF="${SKILLS_REF:-main}"
|
||||||
CHECKOUT_DIR="${SKILLS_CHECKOUT_DIR:-$HOME/.local/share/skills}"
|
CHECKOUT_DIR="${SKILLS_CHECKOUT_DIR:-$HOME/.local/share/skills}"
|
||||||
CLAUDE_GLOBAL_DIR="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}"
|
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
||||||
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
||||||
ANTIGRAVITY_DIR="${ANTIGRAVITY_SKILLS_DIR:-$HOME/.gemini/antigravity/skills}"
|
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
|
||||||
VIBE_SKILLS_DIR="${VIBE_SKILLS_DIR:-$HOME/.vibe/skills}"
|
ANTIGRAVITY_SKILLS_JSON="${ANTIGRAVITY_SKILLS_JSON:-$HOME/.gemini/config/skills.json}"
|
||||||
|
EXTRA_SOURCES="${SKILLS_EXTRA_SOURCES:-}"
|
||||||
|
# Overridable so the jq-absent branch is testable on a host that has jq. That
|
||||||
|
# branch is a contract, not a fallback: jq is optional here while every other
|
||||||
|
# target in this script is dependency-free.
|
||||||
|
JQ="${SKILLS_JQ:-jq}"
|
||||||
|
# Recorded beside the managed directory rather than inside it: a stray file in
|
||||||
|
# there would look like a skill to every harness reading the dir.
|
||||||
|
SOURCES_MANIFEST="$(dirname "$AGENTS_DIR")/$(basename "$AGENTS_DIR")-sources"
|
||||||
|
|
||||||
log() { printf '[skills] %s\n' "$*"; }
|
log() { printf '[skills] %s\n' "$*"; }
|
||||||
|
|
||||||
ensure_checkout() {
|
ensure_checkout() {
|
||||||
|
# Wiring an existing working checkout (task install) must not fetch or
|
||||||
|
# move HEAD — that would discard in-progress work.
|
||||||
|
if [ "${SKILLS_SKIP_FETCH:-}" = "1" ]; then
|
||||||
|
[ -d "$CHECKOUT_DIR/skills" ] || { log "no skills/ under $CHECKOUT_DIR"; exit 1; }
|
||||||
|
log "using $CHECKOUT_DIR as-is (SKILLS_SKIP_FETCH=1)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
if [ -d "$CHECKOUT_DIR/.git" ]; then
|
if [ -d "$CHECKOUT_DIR/.git" ]; then
|
||||||
log "updating $CHECKOUT_DIR"
|
log "updating $CHECKOUT_DIR"
|
||||||
git -C "$CHECKOUT_DIR" fetch --tags --quiet
|
git -C "$CHECKOUT_DIR" fetch --tags --quiet
|
||||||
@@ -40,109 +96,186 @@ ensure_checkout() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
list_skills() {
|
count_skills() {
|
||||||
# Every top-level entry that isn't a known meta file.
|
find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
|
||||||
(cd "$CHECKOUT_DIR" && ls -1) | grep -Ev '^(Taskfile\.yml|install\.sh|README\.md|SKILLS_INDEX\.md|\.gitea|\.git)$' || true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
link_skill() {
|
# sources — every configured source directory, primary first, one per line.
|
||||||
# link_skill <target_dir> <skill_name>
|
sources() {
|
||||||
local target_dir="$1"
|
printf '%s\n' "$CHECKOUT_DIR/skills"
|
||||||
local skill="$2"
|
[ -n "$EXTRA_SOURCES" ] || return 0
|
||||||
local target="$CHECKOUT_DIR/$skill"
|
# printf '%s\n' deliberately: without the trailing newline `read` hits EOF
|
||||||
local link="$target_dir/$skill"
|
# 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
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
[ -n "$src" ] || continue
|
||||||
local current
|
if [ -d "$src" ]; then
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
printf '%s\n' "$src"
|
||||||
if [ "$current" = "$target" ]; then
|
else
|
||||||
return
|
log "extra source $src does not exist — skipping"
|
||||||
fi
|
fi
|
||||||
rm -rf "$link"
|
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
|
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.
|
||||||
|
link_dir() {
|
||||||
|
local link="$1" target="$2"
|
||||||
|
if [ -L "$link" ]; then
|
||||||
|
[ "$(readlink "$link")" = "$target" ] && return
|
||||||
|
rm -f "$link"
|
||||||
|
elif [ -d "$link" ]; then
|
||||||
|
# Refuse to discard a directory holding anything but symlinks: that
|
||||||
|
# would be someone's real skills, not our old wiring.
|
||||||
|
if find "$link" -mindepth 1 -maxdepth 1 ! -type l | grep -q .; then
|
||||||
|
log "REFUSING to replace $link — it holds non-symlink entries; move them and re-run"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
find "$link" -mindepth 1 -maxdepth 1 -type l -delete
|
||||||
|
rmdir "$link"
|
||||||
|
elif [ -e "$link" ]; then
|
||||||
|
rm -f "$link"
|
||||||
|
fi
|
||||||
|
mkdir -p "$(dirname "$link")"
|
||||||
ln -s "$target" "$link"
|
ln -s "$target" "$link"
|
||||||
log "linked $link → $target"
|
log "linked $link → $target"
|
||||||
}
|
}
|
||||||
|
|
||||||
wire_claude_global() {
|
# wire_antigravity — register $AGENTS_DIR in Antigravity's skills.json.
|
||||||
mkdir -p "$CLAUDE_GLOBAL_DIR"
|
# Merges rather than overwrites: skills.json is a documented user-editable
|
||||||
while IFS= read -r skill; do
|
# config (json_configs.md), so a hand-added entry (e.g. a personal skills
|
||||||
[ -n "$skill" ] || continue
|
# dir) must survive a re-run untouched.
|
||||||
link_skill "$CLAUDE_GLOBAL_DIR" "$skill"
|
|
||||||
done < <(list_skills)
|
|
||||||
}
|
|
||||||
|
|
||||||
wire_crush() {
|
|
||||||
# Crush reads skills from ~/.config/crush/skills/<name>/SKILL.md.
|
|
||||||
# Pre-creating the dir is cheap even when Crush isn't installed.
|
|
||||||
mkdir -p "$CRUSH_DIR"
|
|
||||||
while IFS= read -r skill; do
|
|
||||||
[ -n "$skill" ] || continue
|
|
||||||
link_skill "$CRUSH_DIR" "$skill"
|
|
||||||
done < <(list_skills)
|
|
||||||
}
|
|
||||||
|
|
||||||
wire_antigravity() {
|
wire_antigravity() {
|
||||||
# Antigravity (Google's VS Code extension) reads global skills from
|
if ! command -v "$JQ" >/dev/null 2>&1; then
|
||||||
# ~/.gemini/antigravity/skills/<name>/SKILL.md. Our SKILL.md files
|
log "Antigravity: jq not found — skipping (needed to merge $ANTIGRAVITY_SKILLS_JSON safely)"
|
||||||
# already carry the required `name` + `description` YAML frontmatter,
|
|
||||||
# so symlinks are sufficient — no manifest translation step.
|
|
||||||
mkdir -p "$ANTIGRAVITY_DIR"
|
|
||||||
while IFS= read -r skill; do
|
|
||||||
[ -n "$skill" ] || continue
|
|
||||||
link_skill "$ANTIGRAVITY_DIR" "$skill"
|
|
||||||
done < <(list_skills)
|
|
||||||
}
|
|
||||||
|
|
||||||
wire_claude_repo() {
|
|
||||||
# Only wire per-repo when invoked from inside a git repo and it isn't
|
|
||||||
# the skills repo itself.
|
|
||||||
if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local repo
|
|
||||||
repo=$(git rev-parse --show-toplevel)
|
local existing already updated
|
||||||
if [ "$repo" = "$CHECKOUT_DIR" ]; then
|
mkdir -p "$(dirname "$ANTIGRAVITY_SKILLS_JSON")"
|
||||||
|
|
||||||
|
if [ -s "$ANTIGRAVITY_SKILLS_JSON" ]; then
|
||||||
|
existing="$(cat "$ANTIGRAVITY_SKILLS_JSON")"
|
||||||
|
else
|
||||||
|
existing='{}'
|
||||||
|
fi
|
||||||
|
|
||||||
|
already="$(printf '%s' "$existing" | "$JQ" --arg p "$AGENTS_DIR" \
|
||||||
|
'[.entries // [] | .[] | select(.path == $p)] | length' 2>/dev/null)" || {
|
||||||
|
log "Antigravity: $ANTIGRAVITY_SKILLS_JSON is not valid JSON — leaving it untouched"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$already" -gt 0 ]; then
|
||||||
|
log "Antigravity: $AGENTS_DIR already registered in $ANTIGRAVITY_SKILLS_JSON"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local target_dir="$repo/.claude/skills"
|
|
||||||
mkdir -p "$target_dir"
|
|
||||||
while IFS= read -r skill; do
|
|
||||||
[ -n "$skill" ] || continue
|
|
||||||
link_skill "$target_dir" "$skill"
|
|
||||||
done < <(list_skills)
|
|
||||||
}
|
|
||||||
|
|
||||||
wire_vibe() {
|
updated="$(printf '%s' "$existing" | "$JQ" --arg p "$AGENTS_DIR" \
|
||||||
# Mistral Vibe reads a flat ~/.vibe/skills/<name>.md — link the SKILL.md
|
'.entries = ((.entries // []) + [{path: $p}])')"
|
||||||
# file directly (not the skill directory, as the other harnesses do).
|
printf '%s\n' "$updated" > "$ANTIGRAVITY_SKILLS_JSON"
|
||||||
mkdir -p "$VIBE_SKILLS_DIR"
|
log "Antigravity: registered $AGENTS_DIR in $ANTIGRAVITY_SKILLS_JSON"
|
||||||
while IFS= read -r skill; do
|
|
||||||
[ -n "$skill" ] || continue
|
|
||||||
local target="$CHECKOUT_DIR/$skill/SKILL.md"
|
|
||||||
[ -f "$target" ] || continue
|
|
||||||
local link="$VIBE_SKILLS_DIR/$skill.md"
|
|
||||||
if [ -L "$link" ] || [ -e "$link" ]; then
|
|
||||||
local current
|
|
||||||
current=$(readlink "$link" 2>/dev/null || true)
|
|
||||||
if [ "$current" = "$target" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
rm -rf "$link"
|
|
||||||
fi
|
|
||||||
ln -s "$target" "$link"
|
|
||||||
log "linked $link → $target"
|
|
||||||
done < <(list_skills)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
ensure_checkout
|
ensure_checkout
|
||||||
wire_claude_global
|
|
||||||
wire_claude_repo
|
# Canonical location. Pi reads ~/.agents/skills directly, so it needs no
|
||||||
wire_crush
|
# harness-specific link of its own.
|
||||||
|
prepare_agents_dir
|
||||||
|
LINKED_COUNT=0
|
||||||
|
wire_skills
|
||||||
|
|
||||||
|
link_dir "$CRUSH_DIR" "$AGENTS_DIR"
|
||||||
|
link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
|
||||||
wire_antigravity
|
wire_antigravity
|
||||||
wire_vibe
|
|
||||||
log "done — $(list_skills | wc -l | tr -d ' ') skill(s) wired at ref=$REF"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
---
|
||||||
|
name: adr
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Architecture Decision Records — house format, and how to record a decision you cannot make yet"
|
||||||
|
use_when: "Recording an architectural decision; revisiting one; or deciding between options where the right answer genuinely is not knowable yet"
|
||||||
|
description: Write an Architecture Decision Record in this estate's format — YAML frontmatter, Status/Context/Decision/Consequences/Validation — including the case where the tradeoff is genuinely context-dependent and forcing a resolution would be worse than preserving the tension. Use when recording or superseding an architectural decision, or when a decision keeps getting re-litigated because nobody wrote down why.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An ADR is the *record* of a decision — the artifact someone reads in eight months when
|
||||||
|
they are about to undo it without knowing why it exists. It does not confer the decision;
|
||||||
|
a person with authority does that, and this skill never is that person.
|
||||||
|
|
||||||
|
Two things make one useful, and neither is the prose quality: a **format consistent
|
||||||
|
enough to grep across repos**, and an honest account of **what was not decided**.
|
||||||
|
|
||||||
|
## Before you write
|
||||||
|
|
||||||
|
Both of these change what goes in the ADR, so they are not cross-references, they are
|
||||||
|
steps:
|
||||||
|
|
||||||
|
1. **`lineage`** — was this decided before and reversed? The reason it was dropped is
|
||||||
|
usually the most valuable sentence in your Context, and if an earlier ADR exists,
|
||||||
|
yours supersedes it rather than ignoring it.
|
||||||
|
2. **`grill-me`** (Full Grill) — find the hole now. It is cheaper here than in the
|
||||||
|
Consequences section of the ADR that supersedes yours.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Any choice where a reasonable alternative existed and was not taken
|
||||||
|
- Superseding or amending an earlier ADR (never edit the old one — see Status)
|
||||||
|
- A decision that keeps getting re-litigated, which means the reasoning was never recorded
|
||||||
|
- A tradeoff that is genuinely context-dependent — see *Preserving a productive tension*
|
||||||
|
|
||||||
|
**Do not use for:** implementation choices with no alternative worth naming, or anything
|
||||||
|
a code comment answers. An ADR per function is an ADR nobody reads.
|
||||||
|
|
||||||
|
## The format
|
||||||
|
|
||||||
|
**Where it goes:** `docs/adr/NNNN-kebab-slug.md`, zero-padded to four digits, numbered
|
||||||
|
sequentially within the repo. Frontmatter you cannot locate is not greppable, and half
|
||||||
|
the value of the format is that `docs/adr/*` is a known path in every repo here.
|
||||||
|
|
||||||
|
**The number is repo-local.** `cad` ADR-0002 and `infra` ADR-0002 are different
|
||||||
|
documents. When referring across repos — including in `supersedes` — qualify it:
|
||||||
|
`cad#0002`. Within a repo the bare number is fine.
|
||||||
|
|
||||||
|
Frontmatter first. It exists so the corpus is machine-readable — "which ADRs does 0004
|
||||||
|
supersede", "what is still `proposed`" — and a bespoke header block, however well
|
||||||
|
written, cannot answer those.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
adr: 0004
|
||||||
|
title: <the decision, as a sentence — not "database choice">
|
||||||
|
status: proposed | accepted | superseded | amended
|
||||||
|
date: 2026-08-29
|
||||||
|
deciders: <who accepted it, by name — or "pending: <name>" (QUOTED) while proposed>
|
||||||
|
supersedes: <adr number, or null>
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR-0004 — <same title>
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**The frontmatter is authoritative for status and date.** This section exists for the one
|
||||||
|
thing the frontmatter cannot hold: *why* it moved — what superseded it and on what
|
||||||
|
evidence, or what accepting it was contingent on. If it would only restate the YAML,
|
||||||
|
delete it.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The forces. What is true that makes this a decision rather than a default — constraints,
|
||||||
|
the failure that prompted it, what was tried. Name the unknowns explicitly; a decision
|
||||||
|
made under uncertainty is not weaker for saying so, it is auditable.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
One sentence of what was decided, then why *this* option rather than the named
|
||||||
|
alternatives. Address the strongest case against, not the weakest.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:** what this buys.
|
||||||
|
|
||||||
|
**Negative / watch:** what it costs, and the specific thing to watch that would tell you
|
||||||
|
it was wrong. "Some added complexity" is not a consequence; "the event schema is now the
|
||||||
|
coupling surface between cad and every executor — version it deliberately" is.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
How you will know. A link to the experiment or the metric that proves or kills it, and
|
||||||
|
the condition that fires a revisit. An ADR with no validation section is a preference
|
||||||
|
with a template around it.
|
||||||
|
```
|
||||||
|
|
||||||
|
### The frontmatter must actually parse
|
||||||
|
|
||||||
|
It exists to be machine-read, so a block that does not load is worse than no block at
|
||||||
|
all: it drops that ADR silently out of every "what is still open?" query, while looking
|
||||||
|
correct. Two shapes this template used to suggest are invalid YAML, and both were found
|
||||||
|
live in the estate on 2026-09-05 — `openbanking-api` ADR-0001 and `infra` ADR-0019, each
|
||||||
|
copied verbatim from here:
|
||||||
|
|
||||||
|
| Wrong | Why | Right |
|
||||||
|
|---|---|---|
|
||||||
|
| `supersedes: -` | a bare dash is a YAML *sequence entry*, not a string | `supersedes: null` |
|
||||||
|
| `deciders: pending: Mathias` | the second, unquoted colon reads as a nested mapping | `deciders: "pending: Mathias"` |
|
||||||
|
|
||||||
|
Check before committing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -c "import yaml,sys;yaml.safe_load(open(sys.argv[1]).read().split('---')[1]);print('ok')" docs/adr/0004-*.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preserving a productive tension
|
||||||
|
|
||||||
|
Some tradeoffs are not unresolved because nobody thought hard enough. They are
|
||||||
|
context-dependent, the context is not settled, and picking now means picking blind.
|
||||||
|
Forcing resolution there produces a confident record that is wrong half the time.
|
||||||
|
|
||||||
|
Record the tension instead, in the Decision section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Provisional: **<option A>**, on this reasoning: <what makes it safe to pick now>.
|
||||||
|
|
||||||
|
<The strongest case for the option you did NOT take, and why it does not win today. The
|
||||||
|
plain format demands this and a preserved tension does not get an exemption — the whole
|
||||||
|
point is that the loser is still live.>
|
||||||
|
|
||||||
|
### The tension, preserved
|
||||||
|
|
||||||
|
- **Option A** — <what it assumes, what it costs>
|
||||||
|
- **Option B** — <what it assumes, what it costs>
|
||||||
|
|
||||||
|
**What we do not know:** <the variable that decides it>
|
||||||
|
|
||||||
|
**Preservation strategy:** <what we build now so BOTH stay reachable. This is the
|
||||||
|
technical content of the ADR, not a one-liner — enumerate it. It is usually the part both
|
||||||
|
options need anyway, which is often most of the work: the schema, the interface seam, the
|
||||||
|
idempotency key. Name each piece and say which option it keeps alive.>
|
||||||
|
|
||||||
|
**Resolution triggers:** <plural, and one per unknown. Two independent unknowns do not
|
||||||
|
collapse into one condition, and a trigger may exist that neither unknown implies.>
|
||||||
|
```
|
||||||
|
|
||||||
|
Two rules keep this from becoming an excuse:
|
||||||
|
|
||||||
|
1. **A preserved tension still ships a decision.** "We will decide later" is not one.
|
||||||
|
Prefer the option whose correctness does not depend on the unknown, say so, and mark
|
||||||
|
it provisional.
|
||||||
|
|
||||||
|
**When no such option exists** — and often none does, because a tension is productive
|
||||||
|
precisely when both options are hostage to the same unknown — do not manufacture a
|
||||||
|
reason that one of them is. That reads as reasoned and will not be re-examined, which
|
||||||
|
makes it worse than an honest arbitrary pick. Fall back, in order: the option that is
|
||||||
|
cheaper to reverse; the one with the lower switching cost given your preservation
|
||||||
|
strategy; the one already built. Then **say that is why you picked it.** "Chosen
|
||||||
|
because it is the cheaper mistake" is a real justification and an auditable one.
|
||||||
|
2. **The resolution trigger must be observable by someone who is not you.** "When it
|
||||||
|
becomes a problem" is not a trigger. "When executor count passes five and they are all
|
||||||
|
long-lived services in one cluster" is. A trigger nobody can evaluate is a decision
|
||||||
|
that quietly becomes permanent.
|
||||||
|
|
||||||
|
`cad`'s ADR-0001 does this informally under *"Known refinements (recorded, deliberately
|
||||||
|
not built yet)"* — sovereignty deferred to a 3-class model *"until a client actually opts
|
||||||
|
into an EU provider"*. That clause is the pattern: option, cost, and the event that
|
||||||
|
forces the choice.
|
||||||
|
|
||||||
|
## Status discipline
|
||||||
|
|
||||||
|
- **Never edit an accepted ADR to change its decision.** Write a new one and set
|
||||||
|
`supersedes`. The record of what was believed, and when, is the whole value.
|
||||||
|
- **Amend** for corrections that do not change the decision (a broken link, a clarified
|
||||||
|
consequence). Note the amendment inline with its date.
|
||||||
|
- **`proposed` is not a parking space.** An ADR that sits proposed for a month *with no
|
||||||
|
named decider* is a decision nobody is making; either accept it provisionally or close
|
||||||
|
it as declined. With a named decider it is a decision waiting on a person, which is a
|
||||||
|
different thing and is chased rather than closed.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
| Anti-pattern | Why it fails |
|
||||||
|
|---|---|
|
||||||
|
| Consequences that are all positive | Then it was not a decision, it was an announcement |
|
||||||
|
| "We chose X because it is better" | Names no alternative and no force; unfalsifiable |
|
||||||
|
| Written after the code shipped | Reverse-engineers a justification. Record the reasoning while it is still contested |
|
||||||
|
| One ADR amended repeatedly | Destroys the history the format exists to keep |
|
||||||
|
| No Validation section | Nothing distinguishes a good decision from a lucky one |
|
||||||
|
| A tension "resolved" by picking arbitrarily | The next person re-litigates it, having lost the reasoning |
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Writes a file, so it acts — but it decides nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the deciders are not you — which, for an agent, is always. An
|
||||||
|
ADR records a decision someone with authority made; writing one to *create* that
|
||||||
|
authority is how a preference acquires a document number.
|
||||||
|
|
||||||
|
So the deliverable is a draft marked `proposed` with **the person who must accept it named
|
||||||
|
in `deciders` as `pending: <name>`**, and that hand-back stated in the response, not left
|
||||||
|
implied. Naming the decider is the escalation, not a formality — an unnamed one is how a
|
||||||
|
proposal sits for a month.
|
||||||
|
|
||||||
|
That is the intended state for an agent-authored ADR, and it is not the parking-space
|
||||||
|
failure below: a `proposed` ADR with a named decider is waiting on someone; one without is
|
||||||
|
waiting on nobody.
|
||||||
|
|
||||||
|
Also stop when the Context cannot be written without inventing a constraint. A missing
|
||||||
|
force means the decision has not been thought through yet, and an ADR built on an
|
||||||
|
invented one is worse than none: it will be cited.
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- `lineage` and `grill-me` — see *Before you write*; they change the content, so they are
|
||||||
|
steps rather than references.
|
||||||
|
- `spec-driven-dev` / `feature-spec` — an ADR is the *why*; a spec is the *what*. If you
|
||||||
|
are describing behaviour, you are writing a spec.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: atdd
|
name: atdd
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Acceptance Test-Driven Development workflow"
|
||||||
|
use_when: "Feature development with acceptance criteria"
|
||||||
description: Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time.
|
description: Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
---
|
||||||
|
|
||||||
|
# CAD Babysit
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A deliberately narrow session: pick up whatever CAD has run into trouble on, work it with a
|
||||||
|
human present, and stop. This is not an autonomous supervisor — it is a babysitter with a
|
||||||
|
person in the room, and that is the point, not a limitation to route around later.
|
||||||
|
|
||||||
|
**Core principle:** an unattended supervisor reproduces the exact failure it exists to catch.
|
||||||
|
`dispatch` ran dead for eight weeks because it exited 0 and nobody noticed (`dispatch#33`); a
|
||||||
|
long-running watcher process that dies quietly has the same property one level up, and
|
||||||
|
manufactures false confidence while doing it. With a human present, **the human is the
|
||||||
|
liveness check** — that removes the objection entirely and needs no new machinery.
|
||||||
|
|
||||||
|
This skill is the ritual around `cad stuck` (`mathias/cad#14`), not a replacement for it.
|
||||||
|
Without that query it would be a checklist with no input; the query without this skill is
|
||||||
|
data nobody reads carefully. Neither closes the loop alone.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Picking up CAD's stuck queue with a human present and time set aside to work it
|
||||||
|
- After `dispatch` (or any executor) reports a failure and it is not immediately obvious
|
||||||
|
whether it is an infrastructure fault or a real task outcome
|
||||||
|
- Reviewing the "awaiting human" queue (issues CI has verified but nobody has closed)
|
||||||
|
|
||||||
|
**Do not use for:**
|
||||||
|
- Unattended/scheduled runs. This skill assumes a human is present for its entire duration —
|
||||||
|
if nobody is watching, its central safety property (human-as-liveness-check) does not hold.
|
||||||
|
- Dispatching, arming a redispatch label, or closing an issue. This skill only ever diagnoses;
|
||||||
|
see Iron Laws.
|
||||||
|
|
||||||
|
## Iron Laws
|
||||||
|
|
||||||
|
1. **Read-only, always.** No dispatching, no arming labels (including `status:redispatch`), no
|
||||||
|
closing or reopening an issue. A reviewer that can act on its own findings has no
|
||||||
|
independent check — control stays where it already is.
|
||||||
|
2. **Every finding cites a locator.** A run id, commit sha, comment permalink, or a specific
|
||||||
|
log line. A finding without one is an unfalsifiable opinion about a machine — do not emit
|
||||||
|
it, and do not soften this into "probably" language instead of dropping it.
|
||||||
|
3. **Re-derive, never recall.** State lives in Gitea and Actions, not in this session. Start
|
||||||
|
cold every time: run `cad stuck` fresh, do not carry forward a prior session's worklist.
|
||||||
|
Sessions get compacted and lose exactly the cross-run history that makes a stale worklist
|
||||||
|
dangerous to trust.
|
||||||
|
4. **Distinguish "blocked" from "awaiting human".** `cad stuck` already makes this split
|
||||||
|
(`stuck` vs `awaiting_human`); preserve it in every summary. The `awaiting_human` class is
|
||||||
|
the human gate working, not a bottleneck — do not report it as one.
|
||||||
|
5. **Anything that is the operator's call surfaces as `DECISION NEEDED`, never gets decided
|
||||||
|
for them.** See Output Format.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### Phase 1 — Query (re-derive the worklist)
|
||||||
|
|
||||||
|
Run `cad stuck` against the estate (the `dispatch-repos.txt` scope, or whatever repo set the
|
||||||
|
operator names). This is the only input. Do not substitute a memory of a prior run, a brain
|
||||||
|
note, or this issue's own worked examples — all three can be stale within the same day (a
|
||||||
|
taxonomy row's cited live example can stop reproducing between when it was written and when
|
||||||
|
you run the query, because the underlying state genuinely changed; that is the design working,
|
||||||
|
not a bug to paper over).
|
||||||
|
|
||||||
|
Group the output by class exactly as `cad stuck` returns it: `stuck` first, `awaiting_human`
|
||||||
|
second. Pick ONE row to work per pass — this skill is deliberately narrow, not a full-estate
|
||||||
|
sweep.
|
||||||
|
|
||||||
|
### Phase 2 — Read the issue's own comments FIRST
|
||||||
|
|
||||||
|
Before anything else, before touching Actions or pod logs: read every comment on the issue,
|
||||||
|
oldest first. This step exists because it was skipped once and it cost a session — a confident
|
||||||
|
wrong root cause was produced from pod logs while the correct diagnosis sat in the issue's own
|
||||||
|
comment thread from nine days earlier (`cad-atlas#10`). The rule "read issue comments before
|
||||||
|
working" was already written down elsewhere and was not followed; it is written down again
|
||||||
|
here because that is exactly the kind of already-known thing a rushed session skips.
|
||||||
|
|
||||||
|
What to look for: the bot's own report comments (routing decision, ground-truth verdict,
|
||||||
|
attempt count), and any human comment that already contains the diagnosis.
|
||||||
|
|
||||||
|
### Phase 3 — THEN the Actions job logs
|
||||||
|
|
||||||
|
Only after Phase 2 turns up nothing conclusive, read the CI job logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/actions/runs # find the run, match on .run_number
|
||||||
|
GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs # returns 200, full text, secrets masked
|
||||||
|
```
|
||||||
|
|
||||||
|
This endpoint works. A prior note claiming it 404s was wrong and cost a diagnosis the day this
|
||||||
|
skill was written — do not rediscover that. Get the run id from `/actions/runs`, not from the
|
||||||
|
`run_number` shown in a comment (they are different fields; match on `.run_number` to find the
|
||||||
|
internal `id` the jobs endpoint needs).
|
||||||
|
|
||||||
|
This is where the two most valuable findings of the day this skill is drawn from lived: an
|
||||||
|
agent issuing the identical refused command five times and stopping (never trying the tools it
|
||||||
|
was actually permitted to use), and a different model, same denial, writing a Go program to
|
||||||
|
route around it. Neither is visible from the issue comments alone — no metric produces either
|
||||||
|
of them. They need a reader of the raw transcript.
|
||||||
|
|
||||||
|
### Phase 4 — Separate infrastructure fault from task outcome
|
||||||
|
|
||||||
|
A run that died before turn 1 says nothing about the issue itself. Before concluding anything
|
||||||
|
about the WORK, check whether the run reached the model at all:
|
||||||
|
|
||||||
|
- Zero turns, a 2-3 record transcript, a "could not start" comment, or a claim label with no
|
||||||
|
attempt comment behind it → infrastructure fault. This is not evidence the issue is hard, ill
|
||||||
|
specified, or that the assigned model is unsuitable — it is evidence something upstream
|
||||||
|
(credit, a key, MCP connectivity, the executor binary) is broken.
|
||||||
|
- A run that reached the model, drove tools, and stopped on a ceiling, a denial loop, or a
|
||||||
|
genuine task failure → a real task outcome, worth diagnosing on its own terms.
|
||||||
|
|
||||||
|
Four claim labels were burned this exact way in one estate sweep and the tracker read them as
|
||||||
|
four handled issues (`dispatch#33`). Get this distinction wrong and everything downstream is
|
||||||
|
about the wrong thing.
|
||||||
|
|
||||||
|
### Phase 5 — Decide, or say `DECISION NEEDED`
|
||||||
|
|
||||||
|
For each row worked, produce one of:
|
||||||
|
|
||||||
|
- **A closed diagnosis** — cause identified, cited to a locator, no operator judgment call
|
||||||
|
required (e.g. "infrastructure fault, credit exhausted, see comment #N — retry is automatic
|
||||||
|
on the next cycle, no action needed").
|
||||||
|
- **A `DECISION NEEDED`** — anything that is genuinely the operator's call: whether to close a
|
||||||
|
verified-but-open issue, whether to raise a ceiling, whether to abandon an issue that keeps
|
||||||
|
hitting the same denial. Never make this call on the operator's behalf.
|
||||||
|
|
||||||
|
This skill's OWN last act, every time, is to stop — not to dispatch, not to redispatch, not to
|
||||||
|
close. Ending a pass is itself the enforcement of Iron Law 1.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
For a closed diagnosis:
|
||||||
|
|
||||||
|
```
|
||||||
|
DIAGNOSIS: <repo>#<issue> — <category from cad stuck, e.g. ceiling_hit>
|
||||||
|
CAUSE: <one sentence, cites the mechanism>
|
||||||
|
LOCATOR: <url or run id / log line>
|
||||||
|
ACTION: <what happens automatically, or "none — informational">
|
||||||
|
```
|
||||||
|
|
||||||
|
For anything requiring a human call:
|
||||||
|
|
||||||
|
```
|
||||||
|
DECISION NEEDED: <repo>#<issue>
|
||||||
|
QUESTION: <the exact thing only the operator can decide>
|
||||||
|
OPTIONS:
|
||||||
|
1. <option> — <consequence>
|
||||||
|
2. <option> — <consequence>
|
||||||
|
RECOMMENDATION: <your pick, with reasoning — but the operator decides>
|
||||||
|
BLOCKS: <what stays stuck until this is answered, if anything>
|
||||||
|
```
|
||||||
|
|
||||||
|
End every pass with a one-line summary: `N diagnosed, M decisions needed, stopping.`
|
||||||
|
|
||||||
|
## Worked Example
|
||||||
|
|
||||||
|
`cad stuck` returns `mathias/cad-atlas#10 [near_attempt_cap] 4 dispatch attempts recorded, at
|
||||||
|
or above the near-cap threshold (4 of MaxAttempts=5).`
|
||||||
|
|
||||||
|
Phase 2 (issue comments): the issue's own thread already explains the pattern — repeated
|
||||||
|
`status:redispatch` re-arming without the label being cleared after a completed attempt.
|
||||||
|
|
||||||
|
Phase 3: not needed — Phase 2 was conclusive.
|
||||||
|
|
||||||
|
Phase 4: each of the 4 attempts reached the model and ran (not an infrastructure fault) — a
|
||||||
|
real re-fire pattern, not a claim-label artifact.
|
||||||
|
|
||||||
|
Phase 5 output:
|
||||||
|
|
||||||
|
```
|
||||||
|
DECISION NEEDED: mathias/cad-atlas#10
|
||||||
|
QUESTION: one more automatic attempt remains before MaxAttempts=5 gives up permanently. Let
|
||||||
|
it fire, or intervene now (fix the underlying cause, or manually close/redirect the issue)?
|
||||||
|
OPTIONS:
|
||||||
|
1. Let it fire — costs one more headless run; if it fails the same way, dispatch gives up
|
||||||
|
automatically and posts a terminal comment. No action needed until then.
|
||||||
|
2. Intervene now — diagnose why redispatch keeps re-arming (likely: the watcher isn't
|
||||||
|
clearing RedispatchLabel after a serviced run) and fix that before the cap is spent on
|
||||||
|
a symptom rather than the cause.
|
||||||
|
RECOMMENDATION: option 2. The pattern (redispatch not self-clearing after a completed run) is
|
||||||
|
already documented as a known operational hazard (dispatch#54) — spending the last attempt
|
||||||
|
on it again teaches nothing new.
|
||||||
|
BLOCKS: nothing else; this issue's own progress.
|
||||||
|
```
|
||||||
|
|
||||||
|
`1 diagnosed, 1 decision needed, stopping.`
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
| Anti-Pattern | Why It Fails |
|
||||||
|
|---|---|
|
||||||
|
| Diagnosing from pod/Actions logs before reading issue comments | The exact mistake that cost a session — the answer was already written down nine days earlier. |
|
||||||
|
| Reporting `awaiting_human` rows as a backlog or bottleneck | It is the human gate working. Misreporting it manufactures urgency that isn't there and trains the operator to distrust the queue. |
|
||||||
|
| Treating a zero-turn, could-not-start run as evidence about the issue | It is an infrastructure fault. Four claim labels were burned exactly this way and read as four handled issues. |
|
||||||
|
| Deciding a `DECISION NEEDED` case on the operator's behalf "to save time" | Removes the one property that makes this skill safe to run unattended-adjacent: the human decides, this skill only prepares the decision. |
|
||||||
|
| Applying `status:redispatch`, closing an issue, or adding any label from this skill | Control stays where it is. This skill has no write path and should never be given one. |
|
||||||
|
| Carrying forward a worklist from an earlier session instead of re-running `cad stuck` | State changes between sessions (see the worked note on `brain-gardener#190` in `cad#14`'s own acceptance run — its trigger label was gone hours later). A stale worklist gives false confidence, which is the specific failure mode this whole skill exists to avoid. |
|
||||||
|
|
||||||
|
## Honest Limitation
|
||||||
|
|
||||||
|
This skill encodes the *repeatable* diagnostic sequence: read comments, then logs, separate
|
||||||
|
fault from outcome, verify by exit code, re-derive rather than recall. It reliably catches the
|
||||||
|
taxonomy `cad stuck` already knows about. It will not, by itself, produce a genuinely novel
|
||||||
|
finding the way an unhurried human-plus-agent reading session did the day this skill was
|
||||||
|
written — that came from noticing things (an agent editing the file that authorizes it, an
|
||||||
|
issue body recommending a command the policy forbids) that no checklist anticipates. Expect
|
||||||
|
this skill to be reliable on known failure modes and to miss the next new one. That is an
|
||||||
|
acceptable trade as long as nobody mistakes a clean pass through this skill for "nothing is
|
||||||
|
wrong" — it means "nothing on the known list is wrong."
|
||||||
|
|
||||||
|
## Brain MCP Integration
|
||||||
|
|
||||||
|
**At session start:**
|
||||||
|
- Run `brain_query` for the domain (`cad`, `dispatch`, the specific repo) to check whether a
|
||||||
|
finding this session is about to produce has already been diagnosed and written up. Do not
|
||||||
|
re-diagnose a known pattern from scratch every session.
|
||||||
|
|
||||||
|
**After a genuinely novel finding (not already in the taxonomy or the brain):**
|
||||||
|
- `brain_write` the pattern, generalized beyond the specific issue — the failure mode, not the
|
||||||
|
issue number. Do not write routine, already-known-taxonomy findings; that is what `cad
|
||||||
|
stuck`'s own output already records, durably, in Gitea.
|
||||||
|
|
||||||
|
**Never:**
|
||||||
|
- Write a `DECISION NEEDED` outcome to the brain before the operator has actually decided —
|
||||||
|
that would record a guess as a resolution.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Call `session_log` once at the end of every phase to record the outcome.
|
||||||
|
|
||||||
|
**At end of each phase:**
|
||||||
|
- `session_log` with `{session_id: "<this session's id>", skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
|
||||||
|
"fail" | "skip", message: "<one-line summary>", project_root:
|
||||||
|
"<absolute path>"}`
|
||||||
|
|
||||||
|
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
|
||||||
|
decide
|
||||||
|
|
||||||
|
**Status semantics:**
|
||||||
|
- `pass` — the phase's intended outcome was reached (worklist re-derived; comments read;
|
||||||
|
fault/outcome separated; a diagnosis or `DECISION NEEDED` was produced).
|
||||||
|
- `fail` — the phase could not be completed (e.g. `cad stuck` errored, Gitea was unreachable).
|
||||||
|
- `skip` — phase intentionally skipped (e.g. Phase 3 skipped because Phase 2 was conclusive).
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
|
||||||
|
replacement.
|
||||||
|
- Load `debug` for the hypothesis-generation discipline if Phase 4's fault/outcome split is
|
||||||
|
itself ambiguous.
|
||||||
|
- Load `secret-hygiene` before reading raw Actions job logs if the repo is client-tagged —
|
||||||
|
logs are secret-masked by Gitea but a pasted excerpt into a summary is not.
|
||||||
|
- `mathias/cad-atlas#11` renders the same `cad stuck` output as a pipeline overlay — read that
|
||||||
|
instead of this skill's text output when a visual "where is everything stuck" view is wanted
|
||||||
|
over working one row in depth.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only by construction, and the Iron Laws above are the escalation rule: never close
|
||||||
|
an issue, never dispatch, never write a label. Everything that is the operator's call
|
||||||
|
leaves as a `DECISION NEEDED` block (question / options / recommendation / blocks).
|
||||||
|
|
||||||
|
Stop and hand back when: the stuck queue is empty; a diagnosis needs a write you are not
|
||||||
|
allowed to make; or two runs in a row produce the same finding, which means the finding
|
||||||
|
is not being acted on and repeating it is noise.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: clean-code
|
name: clean-code
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Clean code principles with Go adaptation"
|
||||||
|
use_when: "Code review, refactoring, new code"
|
||||||
description: Write code that humans can read, understand, and safely change. Apply SOLID, GRASP, and Clean Code principles during the REFACTOR phase of TDD.
|
description: Write code that humans can read, understand, and safely change. Apply SOLID, GRASP, and Clean Code principles during the REFACTOR phase of TDD.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -278,5 +281,14 @@ During refactoring:
|
|||||||
|
|
||||||
- `references/clean-code.md` — detailed naming rules, object calisthenics
|
- `references/clean-code.md` — detailed naming rules, object calisthenics
|
||||||
- `references/code-smells.md` — comprehensive smell catalog with examples
|
- `references/code-smells.md` — comprehensive smell catalog with examples
|
||||||
|
- `references/python-adaptation.md` — Python conventions: error chaining, dataclasses, the truthiness trap
|
||||||
- Load `solid` skill for SOLID principles with Go examples
|
- Load `solid` skill for SOLID principles with Go examples
|
||||||
- Load `refactoring` skill for systematic refactoring techniques
|
- Load `refactoring` skill for systematic refactoring techniques
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Advisory during the REFACTOR phase. Suggests; does not restructure behaviour.
|
||||||
|
|
||||||
|
Stop and hand back when: the cleanup you are proposing changes behaviour. That is not
|
||||||
|
refactoring, it is a change, and it needs its own failing test first — hand back to
|
||||||
|
`tdd`.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Clean Code: Python Adaptation
|
||||||
|
|
||||||
|
The principles in `SKILL.md` are language-agnostic. This file covers the places where
|
||||||
|
Python's idioms differ from the Go examples, and where following the Go shape produces
|
||||||
|
bad Python.
|
||||||
|
|
||||||
|
The canonical stack (ruff, mypy --strict, pytest, uv, pydantic-settings, src-layout)
|
||||||
|
lives in `~/dev/.context/AGENT.md` and is not restated here — one source of truth, and
|
||||||
|
it is not this file.
|
||||||
|
|
||||||
|
## Reveal intent with types, not comments
|
||||||
|
|
||||||
|
`mypy --strict` is the Python analogue of Go's compiler, and it is non-negotiable for
|
||||||
|
the same reason: a type is a claim the machine checks, and a comment is a claim nobody
|
||||||
|
checks. Two consequences for clean code specifically:
|
||||||
|
|
||||||
|
- **`Any` is a hole in the argument.** Each one says "the reader is on their own here".
|
||||||
|
If a value is genuinely polymorphic, say so precisely — `Protocol`, a union, a
|
||||||
|
`TypeVar` — rather than surrendering.
|
||||||
|
- **A `# type: ignore` needs the error code and a reason**: `# type: ignore[arg-type] #
|
||||||
|
boto3 stubs lag the API`. A bare ignore is an unexplained exception to a rule the rest
|
||||||
|
of the file follows.
|
||||||
|
|
||||||
|
## Errors: chain them, never swallow them
|
||||||
|
|
||||||
|
Go's `fmt.Errorf("operation: %w", err)` is `raise` with `from`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Bad — the original cause is gone, and so is the stack that produced it
|
||||||
|
try:
|
||||||
|
row = fetch(invoice_id)
|
||||||
|
except KeyError:
|
||||||
|
raise InvoiceNotFound(invoice_id)
|
||||||
|
|
||||||
|
# Good — the chain survives; the traceback shows both
|
||||||
|
try:
|
||||||
|
row = fetch(invoice_id)
|
||||||
|
except KeyError as e:
|
||||||
|
raise InvoiceNotFound(invoice_id) from e
|
||||||
|
```
|
||||||
|
|
||||||
|
Two rules with no exceptions:
|
||||||
|
|
||||||
|
- **Never bare `except:`.** It catches `KeyboardInterrupt` and `SystemExit` too, so it
|
||||||
|
turns Ctrl-C into a swallowed error. `except Exception` if you truly mean everything.
|
||||||
|
- **Never a silent `pass`.** An exception handler that does nothing is a decision to
|
||||||
|
ignore a failure; if that is genuinely right, the handler needs a comment saying why,
|
||||||
|
and usually a log line. Most of the time it is a bug being stored for later.
|
||||||
|
|
||||||
|
## Data classes over dicts
|
||||||
|
|
||||||
|
A `dict[str, Any]` passed between functions is Go's `map[string]interface{}` — every
|
||||||
|
reader has to reconstruct the shape from the code that writes it.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Bad: shape known only by reading every producer
|
||||||
|
def total(invoice: dict[str, Any]) -> int: ...
|
||||||
|
|
||||||
|
# Good: shape declared once, checked everywhere
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Invoice:
|
||||||
|
id: InvoiceId
|
||||||
|
lines: tuple[Line, ...]
|
||||||
|
currency: str # ISO 4217
|
||||||
|
|
||||||
|
def total(invoice: Invoice) -> Money: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`frozen=True` where the value is conceptually immutable — it makes accidental mutation
|
||||||
|
an error rather than a surprise at a distance. Use `pydantic` models at the *boundary*
|
||||||
|
(env config, HTTP payloads) where validation is the point, and dataclasses inside,
|
||||||
|
where validation has already happened.
|
||||||
|
|
||||||
|
## Two Python-specific traps that read as clean code
|
||||||
|
|
||||||
|
**Mutable default arguments** are evaluated once, at definition:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def add_line(line: Line, lines: list[Line] = []) -> list[Line]: # shared across calls
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `None` and build inside. Ruff catches this (`B006`); the point of naming it here is
|
||||||
|
that the broken version looks tidier than the correct one.
|
||||||
|
|
||||||
|
**Truthiness is not presence.** `if not amount:` is true for `0`, `0.0`, `""`, `[]` and
|
||||||
|
`None` alike. For anything where zero is a legitimate value — money, counts, offsets —
|
||||||
|
write `if amount is None:`. This is the single most common way a Python function
|
||||||
|
silently mishandles a valid input.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `src/` layout, `pyproject.toml` only. A flat layout lets tests import the working
|
||||||
|
directory instead of the installed package, so the suite passes on a package that
|
||||||
|
cannot actually be installed.
|
||||||
|
- Module names are the unit of cohesion, as Go packages are. `utils.py` is the Python
|
||||||
|
spelling of a god package: it has no reason to change because it has no subject.
|
||||||
|
- Prefer functions to classes with one method. A class whose only job is to hold
|
||||||
|
constructor arguments and expose `run()` is a closure with ceremony.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `references/clean-code.md` — the language-agnostic practices
|
||||||
|
- `references/code-smells.md` — the smell catalogue, all of which apply here
|
||||||
|
- `tdd/references/python.md` — the test-side conventions
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: code-review
|
name: code-review
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Code smell detection + Go-specific checklist"
|
||||||
|
use_when: "Pre-merge review"
|
||||||
description: Systematic code smell detection and Go-specific review checklist. Load before any pre-merge review.
|
description: Systematic code smell detection and Go-specific review checklist. Load before any pre-merge review.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,6 +14,8 @@ Code review is smell detection and principle verification, not style policing. T
|
|||||||
|
|
||||||
**This skill is read-only analysis.** When you find issues, document them with specific file:line references and prioritize by architectural impact. For fixes, load the `refactoring` or `clean-code` skill.
|
**This skill is read-only analysis.** When you find issues, document them with specific file:line references and prioritize by architectural impact. For fixes, load the `refactoring` or `clean-code` skill.
|
||||||
|
|
||||||
|
Three similarly-named skills cover orthogonal concerns, and landing on one is no reason to miss the others: this one is **what to check**, `superpowers-requesting-code-review` is **how to commission a reviewer**, and `superpowers-receiving-code-review` is **how to behave when the feedback arrives**.
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
|
|
||||||
- Pre-merge review of any pull request
|
- Pre-merge review of any pull request
|
||||||
@@ -248,7 +253,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
|||||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||||
|
|
||||||
**At end of each phase:**
|
**At end of each phase:**
|
||||||
- `session_log` with `{skill: "code-review", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "code-review", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** phase-0-iron-laws, phase-1-context, phase-2-architectural-analysis, phase-3-go-checklist, phase-4-code-smell-detection, phase-5-test-quality
|
**Phases for this skill:** phase-0-iron-laws, phase-1-context, phase-2-architectural-analysis, phase-3-go-checklist, phase-4-code-smell-detection, phase-5-test-quality
|
||||||
|
|
||||||
@@ -257,7 +262,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
|
|||||||
- `fail` — the phase's intended outcome was NOT reached.
|
- `fail` — the phase's intended outcome was NOT reached.
|
||||||
- `skip` — phase was skipped intentionally.
|
- `skip` — phase was skipped intentionally.
|
||||||
|
|
||||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
|
||||||
|
|
||||||
## Common Go Anti-Patterns
|
## Common Go Anti-Patterns
|
||||||
|
|
||||||
@@ -274,11 +279,18 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
- `references/python-adaptation.md` — the Python anti-pattern table, and what ruff/mypy
|
||||||
|
should have caught before review
|
||||||
- `clean-code/references/code-smells.md` — comprehensive smell catalog
|
- `clean-code/references/code-smells.md` — comprehensive smell catalog
|
||||||
- Load `refactoring` skill when recommending fixes
|
- Load `refactoring` skill when recommending fixes
|
||||||
- Load `test-design` skill for reviewing test quality
|
- Load `test-design` skill for reviewing test quality
|
||||||
- Load `solid` skill for SOLID principle violations
|
- Load `solid` skill for SOLID principle violations
|
||||||
|
|
||||||
## Mode 2 Routing Note
|
## Escalation
|
||||||
|
|
||||||
This skill is invoked identically in Mode 1 (cloud Claude) and Mode 2 (client-local with supervisor routing). The routing layer (Plan 6) does not exist yet; treat this skill as Mode 1 only for now. Findings format and severity classification are unchanged across modes.
|
Read-only analysis. Findings are the output; nothing is handed back mid-review.
|
||||||
|
|
||||||
|
Stop and hand back when: the diff is large enough that a review would be a skim — say so
|
||||||
|
and ask for it in parts, rather than producing confident coverage of a fraction of it; or
|
||||||
|
the problem is structural rather than pointwise, in which case name it and hand off to
|
||||||
|
`solid` or `refactoring` instead of filing twenty line-level comments about one cause.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Code Review: Python Anti-Patterns
|
||||||
|
|
||||||
|
`SKILL.md` carries the Go anti-pattern table. This is the Python one. The review
|
||||||
|
process, severity classification and output format above it are language-neutral and
|
||||||
|
apply unchanged.
|
||||||
|
|
||||||
|
Canonical stack in `~/dev/.context/AGENT.md`. Two gates decide most of this file:
|
||||||
|
`ruff` and `mypy --strict`. **If a finding is something ruff or mypy already catches,
|
||||||
|
the review comment is not "fix this line" — it is "why is the gate not running?"**
|
||||||
|
|
||||||
|
## The table
|
||||||
|
|
||||||
|
| Anti-pattern | What to look for | Why it matters |
|
||||||
|
|---|---|---|
|
||||||
|
| Bare `except:` | `except:` with no exception class | Swallows `KeyboardInterrupt` and `SystemExit`; Ctrl-C stops working |
|
||||||
|
| Silent `pass` | `except X: pass` with no comment | A decision to ignore a failure, recorded nowhere |
|
||||||
|
| Lost cause | `raise Y(...)` inside `except X as e` with no `from e` | Traceback loses the original failure — the Python analogue of a naked `fmt.Errorf` |
|
||||||
|
| Mutable default | `def f(x, acc=[])` or `={}` | Evaluated once at definition; state leaks between calls |
|
||||||
|
| Truthiness for presence | `if not amount:` where `0` is valid | Silently mishandles zero, empty string, empty list |
|
||||||
|
| `Any` as an escape | `Any` in a signature that could be `Protocol`/union/`TypeVar` | Removes exactly the checking `--strict` was adopted for |
|
||||||
|
| Unexplained ignore | bare `# type: ignore` | No error code, no reason; permanent by default |
|
||||||
|
| `assert` in production code | `assert user.is_admin` outside tests | `python -O` strips asserts — the check vanishes in a tuned deployment |
|
||||||
|
| Import-time side effects | connections, env reads, or I/O at module scope | Cannot be injected, cannot be tested, ordering-dependent |
|
||||||
|
| Sync I/O in `async def` | `requests.get`, `time.sleep`, blocking file reads | Blocks the whole event loop; looks concurrent, is not |
|
||||||
|
| Un-awaited coroutine | a call to an `async def` with no `await` | Silently does nothing; "coroutine was never awaited" is a warning, not an error |
|
||||||
|
| `float` for money | `float` amounts, `round()` on currency | `0.1 + 0.2 != 0.3`; use `Decimal` or int minor units |
|
||||||
|
| Broad `Exception` re-raise | `except Exception` around a whole function body | Turns a specific failure into an unclassifiable one |
|
||||||
|
| Mutating a shared default/global | module-level `list`/`dict` mutated by functions | The Python spelling of shared mutable state |
|
||||||
|
| `dict[str, Any]` as a domain type | dicts passed between layers | Shape known only by reading every producer; use a dataclass |
|
||||||
|
| Star imports | `from x import *` | Breaks `--strict`, hides shadowing, defeats grep |
|
||||||
|
| `os.path` string surgery | manual path joining, `+ "/"` | `pathlib` exists and is correct on every platform |
|
||||||
|
| `subprocess(..., shell=True)` with interpolation | f-strings into a shell command | Injection; pass a list, no shell |
|
||||||
|
|
||||||
|
## What to check that a linter cannot
|
||||||
|
|
||||||
|
- **Does the test suite assert the failure mode, or only the happy path?** `pytest.raises`
|
||||||
|
with no `match=` accepts any message, including one from an unrelated bug.
|
||||||
|
- **Are fixtures doing real work?** A `conftest.py` fixture with a `session` scope and
|
||||||
|
mutable state couples every test that touches it; failures then depend on ordering.
|
||||||
|
- **Is the mock mocking a boundary or a collaborator you own?** The latter tests the mock.
|
||||||
|
- **Does the type say what the code means?** `-> dict` and `-> tuple[int, int]` both pass
|
||||||
|
`--strict` and both tell the reader nothing. A named type is a review comment worth
|
||||||
|
making.
|
||||||
|
- **Would `--strict` actually run on this file?** Check for a per-module override in
|
||||||
|
`pyproject.toml` silently exempting it. An exemption nobody revisits is a hole with a
|
||||||
|
changelog entry.
|
||||||
|
|
||||||
|
## Severity in this context
|
||||||
|
|
||||||
|
- **Blocking:** anything in the table above under security or correctness — `shell=True`
|
||||||
|
interpolation, `assert` as an authorization check, `float` money, sync I/O in async.
|
||||||
|
- **Should fix:** typing escapes (`Any`, unexplained ignores), mutable defaults,
|
||||||
|
truthiness-for-presence.
|
||||||
|
- **Consider:** structural comments — dataclass instead of dict, narrower Protocol.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `clean-code/references/python-adaptation.md` — the style-level conventions
|
||||||
|
- `solid/references/python-adaptation.md` — Protocol over ABC, DIP in Python
|
||||||
|
- `tdd/references/python.md` — the test-side conventions this review checks against
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: cognitive-load
|
name: cognitive-load
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Cognitive complexity analysis (CLI score)"
|
||||||
|
use_when: "Identifying hotspots, before refactoring"
|
||||||
description: Measure how much mental effort a codebase demands from developers. Use when identifying hotspots, before architectural decisions, or when code feels hard to understand.
|
description: Measure how much mental effort a codebase demands from developers. Use when identifying hotspots, before architectural decisions, or when code feels hard to understand.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -344,3 +347,12 @@ type UserWriter interface {
|
|||||||
- Load `refactoring` skill for systematic techniques to reduce identified hotspots
|
- Load `refactoring` skill for systematic techniques to reduce identified hotspots
|
||||||
- Load `solid` skill for structural fixes (coupling, cohesion)
|
- Load `solid` skill for structural fixes (coupling, cohesion)
|
||||||
- Load `clean-code` skill for naming improvements
|
- Load `clean-code` skill for naming improvements
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only measurement. The score is the output.
|
||||||
|
|
||||||
|
Stop and hand back when: the measurement disagrees with the reader's lived experience of
|
||||||
|
the code. A metric that says "fine" about code everyone finds hard is evidence about the
|
||||||
|
metric, not about the code, and continuing to defend the number is how a measure becomes
|
||||||
|
a target.
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
name: debug
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Hypothesis-first debugging discipline"
|
||||||
|
use_when: "Any test failure or bug whose cause is not immediately obvious — or a bug report with no failure text, which this skill refuses rather than guesses at"
|
||||||
|
description: Systematic hypothesis-first debugging. Requires the failure itself — error text, stack, run id or log — and names what it needs and stops if the report has none, rather than hypothesising into a vacuum. Given evidence, generates 3-5 ranked, falsifiable hypotheses with verification steps before suggesting any fix. Use when encountering a bug, test failure, or unexpected behavior.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Debugging is hypothesis generation, not fix generation. The first instinct — "try this and see if it works" — wastes time and teaches nothing. A disciplined debug session produces a small set of ranked, falsifiable hypotheses, each with a concrete verification command. The fix comes after one hypothesis is confirmed, not before.
|
||||||
|
|
||||||
|
**Core principle:** A hypothesis you cannot verify with a single bounded, decisive observation is not a hypothesis — it is a guess. In a repo that observation is usually one command. Above one it may be an instrumented run, or partitioning the last twenty CI runs by trigger type; what makes it a hypothesis is that the observation *discriminates*, not that it fits on one line.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Any test failure whose cause is not immediately obvious
|
||||||
|
- Any production error or unexpected behavior
|
||||||
|
- Any bug report from a user or stakeholder
|
||||||
|
- Before reaching for a debugger or adding print statements: form hypotheses first
|
||||||
|
|
||||||
|
**Do not use for:**
|
||||||
|
- Compile errors with clear messages (just fix the typo)
|
||||||
|
- Already-diagnosed bugs where you know the cause (go straight to TDD with a regression test)
|
||||||
|
|
||||||
|
### Is it actually diagnosed?
|
||||||
|
|
||||||
|
That last exclusion is the one people get wrong, because a confident wrong diagnosis feels
|
||||||
|
exactly like a right one. It is diagnosed if you can state the mechanism *and* name the
|
||||||
|
observation that would have proved it wrong. If the evidence is "we changed X and it got
|
||||||
|
better", that is correlation with a deployment, and this skill applies.
|
||||||
|
|
||||||
|
The strongest tell is a run of failed fixes — see below. Three plausible fixes that did
|
||||||
|
not eliminate the symptom is not bad luck, it is the diagnosis being wrong.
|
||||||
|
|
||||||
|
### Intake: what you need before hypothesising
|
||||||
|
|
||||||
|
Before generating anything, confirm you have the failure itself — the error text, the
|
||||||
|
stack, the failing run's id, the log. If the report is "it's flaky again" with no
|
||||||
|
artifact, **name the specific thing you need and stop**. Iron Law 1 forbids "try X and
|
||||||
|
see"; hypothesising into a vacuum is the same failure with better grammar, and it is
|
||||||
|
worse because the output looks like analysis.
|
||||||
|
|
||||||
|
This is the rule `cad-babysit` already enforces from the other side: read the issue's own
|
||||||
|
comments and the job log *first*.
|
||||||
|
|
||||||
|
## Iron Laws
|
||||||
|
|
||||||
|
1. **Never suggest "try X and see what happens."** Every hypothesis must have a specific expected outcome if correct.
|
||||||
|
2. **Generate 3-5 hypotheses, ordered by likelihood (most likely first).** Fewer than 3 means you stopped thinking; more than 5 means you are not prioritizing.
|
||||||
|
3. **Diagnose only — do not fix in this skill.** The fix happens in a separate TDD cycle (load `tdd` skill) once a hypothesis is confirmed.
|
||||||
|
|
||||||
|
## When the bug spans components
|
||||||
|
|
||||||
|
A system with boundaries (CI → build → signing, API → service → database) hides the
|
||||||
|
question "which component is wrong?" behind the question "why is the output wrong?".
|
||||||
|
Specify, as a pre-step, what to log entering and leaving **each** boundary, plus
|
||||||
|
whether config and environment actually propagated. One run *that reproduces the failure* narrows the search to a
|
||||||
|
single component. On an intermittent fault a passing run narrows nothing, so leave the
|
||||||
|
instrumentation in across several runs rather than concluding from one.
|
||||||
|
|
||||||
|
This does not suspend Iron Law 2. Still emit 3-5 hypotheses — the boundary spec comes
|
||||||
|
first in the output, and each hypothesis says which boundary observation would kill it.
|
||||||
|
What the pre-step forbids is hypotheses that quietly assume a component is the faulty
|
||||||
|
one before any evidence says so.
|
||||||
|
|
||||||
|
## When several fixes have already failed
|
||||||
|
|
||||||
|
Three or more plausible fixes that failed to eliminate a symptom is not a run of bad
|
||||||
|
luck — it is evidence that nobody has identified the mechanism. (Fixes that changed
|
||||||
|
nothing at all count double: they are stronger evidence of a misidentified mechanism
|
||||||
|
than fixes that partially worked.) Each one has been narrowing a window rather than
|
||||||
|
closing it, and a fourth guess buys another tenth of the rate plus a layer of
|
||||||
|
interacting guards nobody can reason about.
|
||||||
|
|
||||||
|
Intake still gates this section. A report of three failed fixes with no artifacts fires
|
||||||
|
both rules at once, and intake wins: you cannot aim hypotheses at a premise you have no
|
||||||
|
evidence about either. Ask for the log, then aim.
|
||||||
|
|
||||||
|
Say that explicitly — the refusal is the finding, not a preamble to it. Then aim the
|
||||||
|
hypotheses one level up: the premise those fixes share is itself the thing to falsify.
|
||||||
|
This is not an exemption from the output format. It is the format, pointed at the
|
||||||
|
assumption instead of at the symptom.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### Step 1: Read the failure
|
||||||
|
|
||||||
|
Before forming hypotheses:
|
||||||
|
- Read the full error message and stack trace, not just the headline
|
||||||
|
- Read the file where the failure originated, around the failing line
|
||||||
|
- If the failure is from a test, read the test and the code under test
|
||||||
|
- Identify the **failure mode** — what actually went wrong (e.g. "nil pointer dereference in goroutine spawned by handler") not just what the error says ("runtime error")
|
||||||
|
|
||||||
|
### Step 2: Generate hypotheses
|
||||||
|
|
||||||
|
For each hypothesis, capture three things:
|
||||||
|
|
||||||
|
- **Mechanism:** what specific code path or state would produce this exact failure
|
||||||
|
- **Verification:** the single command or file inspection that confirms or denies it
|
||||||
|
- **Expected outcome if correct:** the specific output you would see
|
||||||
|
|
||||||
|
Order by likelihood, most likely first. **The ordering is the ranking — do not also
|
||||||
|
attach `high`/`medium`/`low` labels.** With five slots and three labels the labels stop
|
||||||
|
discriminating (two hypotheses are genuinely both "high" and the label cannot say which
|
||||||
|
comes first), and a reader then has two orderings that can disagree. Position already
|
||||||
|
carries it.
|
||||||
|
|
||||||
|
Where you can say *why* one outranks another — a prior brain entry, a matching symptom,
|
||||||
|
a recent change in that area — say that instead of a label. That is information; a label
|
||||||
|
is a restatement of the order it sits next to.
|
||||||
|
|
||||||
|
### Step 3: Output
|
||||||
|
|
||||||
|
Use this format:
|
||||||
|
|
||||||
|
```
|
||||||
|
HYPOTHESIS 1: <mechanism in one sentence>
|
||||||
|
WHY FIRST: <what makes it likeliest — a matching symptom, a recent change, a brain entry>
|
||||||
|
VERIFY: <exact command, or the bounded observation that discriminates>
|
||||||
|
EXPECTED IF CORRECT: <specific output, value, or condition>
|
||||||
|
|
||||||
|
HYPOTHESIS 2: <mechanism>
|
||||||
|
WHY FIRST: <or why it ranks here rather than first>
|
||||||
|
VERIFY: <exact command>
|
||||||
|
EXPECTED IF CORRECT: <specific output>
|
||||||
|
|
||||||
|
[... up to 5 ...]
|
||||||
|
|
||||||
|
RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
||||||
|
```
|
||||||
|
|
||||||
|
End with the recommendation, not a fix.
|
||||||
|
|
||||||
|
## Worked Example
|
||||||
|
|
||||||
|
**Failure:**
|
||||||
|
```
|
||||||
|
--- FAIL: TestInvoiceParser_HandlesEmptyPDF (0.00s)
|
||||||
|
parser_test.go:47: panic: runtime error: index out of range [0] with length 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
```
|
||||||
|
HYPOTHESIS 1: parser indexes into pages[0] before checking len(pages) > 0; empty PDFs produce a zero-page document
|
||||||
|
VERIFY: rg -n 'pages\[0\]' internal/parser/
|
||||||
|
EXPECTED IF CORRECT: at least one site reads pages[0] without a preceding length check
|
||||||
|
|
||||||
|
HYPOTHESIS 2: the test fixture is a zero-byte file rather than a valid empty PDF; pdf library returns nil pages slice instead of empty
|
||||||
|
VERIFY: ls -la testdata/empty.pdf && file testdata/empty.pdf
|
||||||
|
EXPECTED IF CORRECT: file size 0 bytes or "data" rather than "PDF document"
|
||||||
|
|
||||||
|
HYPOTHESIS 3: a recently changed dependency reordered the page-extraction API; pages[0] now refers to metadata, not content
|
||||||
|
VERIFY: git log --oneline -10 -- go.sum | grep -i pdf
|
||||||
|
EXPECTED IF CORRECT: a pdf-library bump in the last few commits
|
||||||
|
|
||||||
|
RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
| Anti-Pattern | Why It Fails |
|
||||||
|
|---|---|
|
||||||
|
| "Maybe try restarting the service" | Not a mechanism. Not falsifiable. Teaches nothing if it works. |
|
||||||
|
| "Could be a race condition" | Mechanism without specifics. Which two operations race? On what state? |
|
||||||
|
| "Let me add some print statements and see" | Skips hypothesis generation. Generates noise, not understanding. |
|
||||||
|
| Answering "what's the fix?" from a report with no artifact | Hypothesising into a vacuum. The output looks like analysis, which is what makes it worse than saying "I need the log". |
|
||||||
|
| Single hypothesis presented as fact | If you are sure, write the regression test, do not run a debug skill. |
|
||||||
|
| Mixing hypotheses with fix suggestions | The skill is diagnose-only. The fix is a TDD task on its hypothesis. |
|
||||||
|
|
||||||
|
## Brain MCP Integration
|
||||||
|
|
||||||
|
The brain holds prior debug sessions across the project. Use it to skip rediscovering known failure modes.
|
||||||
|
|
||||||
|
**At debug start:**
|
||||||
|
- Run `brain_query` with the error message snippet + the package name. On an intake stop you have neither — query the symptom and the component instead ("signed artifact missing CI"), which is worth doing precisely because it may name the artifact to ask for. Past sessions may have logged identical or similar failures with their resolved hypotheses.
|
||||||
|
- **A hit is evidence, so cite it inline in the hypothesis it informs** — `(brain: <entry-name>)` — rather than mentioning it once at the top. If a prior entry is why a hypothesis ranks first, the reader needs that at the hypothesis, not in a preamble. If a prior entry contradicts your ranking, say so and rank it anyway if the evidence here is better; the brain records what was true then.
|
||||||
|
|
||||||
|
**After a hypothesis is confirmed:**
|
||||||
|
- Run `brain_write` with the failure signature → confirmed mechanism. Future debug sessions on the same area get the answer immediately.
|
||||||
|
|
||||||
|
**Never:**
|
||||||
|
- Run `brain_write` for an unconfirmed hypothesis. Speculation in the brain pollutes future queries.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Call `session_log` once at the end of every phase to record the outcome.
|
||||||
|
Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
||||||
|
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||||
|
|
||||||
|
**At end of each phase:**
|
||||||
|
- `session_log` with `{session_id: "<this session's id>", skill: "debug", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
|
**Phases for this skill:** intake, read-failure, generate-hypotheses, output
|
||||||
|
|
||||||
|
`intake` exists so that refusing correctly is recordable. A run that stops for want of a
|
||||||
|
run id has done the right thing and logs `intake` / `pass`; without that phase the
|
||||||
|
refusal logs as a failed `read-failure`, and a metric that penalises the skill's best
|
||||||
|
behaviour teaches the next model to hypothesise into the vacuum instead.
|
||||||
|
|
||||||
|
**Status semantics:**
|
||||||
|
- `pass` — the phase's intended outcome was reached.
|
||||||
|
- `fail` — the phase's intended outcome was NOT reached.
|
||||||
|
- `skip` — phase was skipped intentionally.
|
||||||
|
|
||||||
|
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- After a hypothesis is confirmed, load `tdd` skill — write a failing regression test that proves the bug exists, then fix it.
|
||||||
|
- For test failures specifically caused by mock-vs-real divergence, also load `tdd/references/testing-anti-patterns.md`.
|
||||||
|
- Load `code-review` skill if the diagnosis surfaces a structural issue (god object, shotgun surgery) rather than a pointwise bug.
|
||||||
|
|
||||||
|
Techniques kept in `debug/references/`, inherited from the deleted
|
||||||
|
`superpowers-systematic-debugging` (#14):
|
||||||
|
|
||||||
|
- `root-cause-tracing.md` — load when the failure surfaces far from its trigger and the
|
||||||
|
stack is the only thread back to it.
|
||||||
|
- `defense-in-depth.md` — load after a root cause is confirmed, to decide which layers
|
||||||
|
should have caught it.
|
||||||
|
- `condition-based-waiting.md` — load whenever a timeout appears anywhere in the report,
|
||||||
|
including as one of the fixes already tried. A bumped timeout is a symptom. Not during an
|
||||||
|
intake stop: if you are not analysing yet, a symptom-analysis reference is on the wrong
|
||||||
|
side of the gate.
|
||||||
|
- `find-polluter.sh` — run when a test passes alone and fails in the suite, or when
|
||||||
|
something in the tree appears that no test admits to creating.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Diagnose only — Iron Law 3 is already an escalation rule: the fix is a separate TDD cycle,
|
||||||
|
not this skill's output.
|
||||||
|
|
||||||
|
Stop and hand back when: the report contains no failure text and no locator, in which case
|
||||||
|
name the specific artifact you need (run id, job log, stack trace) and stop rather than
|
||||||
|
hypothesising into a vacuum; or the evidence needed to discriminate between your top two
|
||||||
|
hypotheses requires access you do not have.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: discovery-framing
|
name: discovery-framing
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Problem-before-solution discovery/framing method"
|
||||||
|
use_when: "Framing/validating a problem before any solution; opportunity sizing"
|
||||||
description: Run the discovery / framing phase of product innovation — define and quantify a focused customer problem BEFORE any solution exists. Four movements: understand the problem space, learn about customers, identify and size problems, decide pursue/table/kill. Produces a problem brief per prioritized opportunity. Use when an idea is still vague, when someone is jumping to solutions before the problem is validated, when scoping early research, or when deciding which problems are worth pursuing. Trigger phrases include "frame the problem", "discovery", "problem statement", "is this worth building", "early research", "opportunity sizing", "we have an idea for", "how might we", "what problem are we solving".
|
description: Run the discovery / framing phase of product innovation — define and quantify a focused customer problem BEFORE any solution exists. Four movements: understand the problem space, learn about customers, identify and size problems, decide pursue/table/kill. Produces a problem brief per prioritized opportunity. Use when an idea is still vague, when someone is jumping to solutions before the problem is validated, when scoping early research, or when deciding which problems are worth pursuing. Trigger phrases include "frame the problem", "discovery", "problem statement", "is this worth building", "early research", "opportunity sizing", "we have an idea for", "how might we", "what problem are we solving".
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -149,3 +152,12 @@ hand-wavy. Crisp solution + fuzzy problem = reverse-engineered justification. St
|
|||||||
|
|
||||||
> Named instances of both war stories are client-tagged and held out of this public skill
|
> Named instances of both war stories are client-tagged and held out of this public skill
|
||||||
> pending client-store decision. The transferable lesson lives here; the wrapper stays out.
|
> pending client-store decision. The transferable lesson lives here; the wrapper stays out.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Advisory. Produces a problem brief; commits to nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the evidence needed to size a problem requires talking to
|
||||||
|
customers you cannot reach. Sizing a problem from assumptions and presenting it as
|
||||||
|
discovery is worse than saying the number is unknown — it launders a guess into a brief
|
||||||
|
that later decisions will cite.
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: document-to-markdown
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Read .docx/.xlsx/.pptx/.epub/.msg and other formats the Read tool cannot open"
|
||||||
|
use_when: "You need the contents of a binary document — Word, Excel, PowerPoint, EPUB, Outlook .msg, a zip of documents — and Read returns nothing useful"
|
||||||
|
description: Convert a binary document to Markdown locally with markitdown. No network, no LLM, safe for confidential material. Says when to use the docmark service instead.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Document → Markdown
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`Read` opens PDFs, images and notebooks natively. It does **not** open `.docx`,
|
||||||
|
`.xlsx`, `.pptx`, `.epub`, `.msg` or `.zip`. This skill converts those locally.
|
||||||
|
|
||||||
|
One command, no install step, no service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from 'markitdown[docx,pdf,pptx,xls,xlsx,outlook]' markitdown FILE > /tmp/out.md
|
||||||
|
```
|
||||||
|
|
||||||
|
First run downloads 38 packages (~10s); afterwards it is cached and instant.
|
||||||
|
|
||||||
|
## When to use this
|
||||||
|
|
||||||
|
You have a file on disk whose contents you need. That is the whole trigger.
|
||||||
|
|
||||||
|
## When NOT to use this — use `docmark` instead
|
||||||
|
|
||||||
|
Three cases, and only three:
|
||||||
|
|
||||||
|
1. **A scanned PDF or an image whose content is pixels, not text.** Extracting
|
||||||
|
that needs a vision model. The estate's only audited path from an image to a
|
||||||
|
model is `docmark`, whose `sovereignty.py` constructs an LLM client *only*
|
||||||
|
when `DOCMARK_VISION_MODEL` matches `koala/*` or `iguana/*`, and silently
|
||||||
|
degrades to metadata-only otherwise. `tests/test_sovereignty.py` is the
|
||||||
|
load-bearing guard. Do not reach for a cloud vision model directly; that is
|
||||||
|
the boundary the gate exists to hold.
|
||||||
|
2. **You are writing an in-cluster service** (something in `dispatch`,
|
||||||
|
`ingestion`, `cobalt-dingo`). Call `docmark` over MCP rather than bundling a
|
||||||
|
converter into each service. Its NetworkPolicy already admits `supervisor`
|
||||||
|
and `dispatch`.
|
||||||
|
3. **The file is larger than ~25 MiB** or you want the input/output caps
|
||||||
|
enforced centrally.
|
||||||
|
|
||||||
|
For a text-bearing document, the service returns **byte-identical output** to
|
||||||
|
the local command — measured 2026-09-24 on the same `.docx`, tables included.
|
||||||
|
A network hop, a bearer token and an ingress for the same bytes is not a
|
||||||
|
trade worth making.
|
||||||
|
|
||||||
|
## Confidential material
|
||||||
|
|
||||||
|
**Conversion is local and involves no LLM.** `markitdown` uses an LLM client
|
||||||
|
only to describe images; with none configured it does not call anything. No
|
||||||
|
bytes leave the machine.
|
||||||
|
|
||||||
|
That makes this safe for client material, unlike anything that routes through a
|
||||||
|
model. Two cautions:
|
||||||
|
|
||||||
|
- **Do not install `markitdown[all]`.** It pulls `az-doc-intel` and
|
||||||
|
`az-content-understanding` — Azure cloud document-intelligence converters.
|
||||||
|
They need explicit endpoint configuration to activate, but the extras list
|
||||||
|
above avoids having them on the path at all.
|
||||||
|
- The output is a file like any other. If the source was confidential, so is
|
||||||
|
`/tmp/out.md` — and the usual rule applies: it must not reach an open-egress
|
||||||
|
sink (ADR-0014).
|
||||||
|
|
||||||
|
## Verify the conversion, do not assume it
|
||||||
|
|
||||||
|
The failure mode is **a successful exit with empty output**. A `.docx` template
|
||||||
|
with no body content converts to zero characters and exits 0; so does a scanned
|
||||||
|
PDF with no text layer. Both look like success.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from 'markitdown[docx,pdf,pptx,xls,xlsx,outlook]' markitdown FILE > /tmp/out.md
|
||||||
|
wc -c /tmp/out.md
|
||||||
|
```
|
||||||
|
|
||||||
|
If the byte count is 0 or implausibly small for the document's size, the
|
||||||
|
converter found no text layer — that is case 1 above, and it needs `docmark`
|
||||||
|
with a vision model, not a retry.
|
||||||
|
|
||||||
|
## What it converts
|
||||||
|
|
||||||
|
18 converters with the extras above:
|
||||||
|
|
||||||
|
```
|
||||||
|
Audio, BingSerp, Csv, Docx, Epub, Html, Image, Ipynb, OutlookMsg, Pdf,
|
||||||
|
PlainText, Pptx, Rss, Wikipedia, Xls, Xlsx, YouTube, Zip
|
||||||
|
```
|
||||||
|
|
||||||
|
`docmark` registers 22 — the same set plus `DocxWithOCR`, `PdfWithOCR`,
|
||||||
|
`PptxWithOCR`, `XlsxWithOCR`. **Those four are the entire difference**, and
|
||||||
|
they are exactly the vision path from case 1. Everything else is identical code
|
||||||
|
running in a different place.
|
||||||
|
|
||||||
|
## Reading the output
|
||||||
|
|
||||||
|
Convert to a file and `Read` it, rather than piping into the transcript —
|
||||||
|
a 200-page document produces a lot of Markdown and most of it will not be
|
||||||
|
what you needed. Convert, then grep or read the section you want.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
`docmark` ADR-0013 (`mathias/infra docs/decisions/`) owns the service; this
|
||||||
|
skill is the local path. The split was decided in `mathias/docmark#3` on
|
||||||
|
measurement, not preference: three of docmark's four plausible consumers are
|
||||||
|
better served without it, and the fourth — vision under a sovereignty gate — is
|
||||||
|
why it exists.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Local, read-only, single command. Nothing here is handed back mid-task.
|
||||||
|
|
||||||
|
Stop and hand back when:
|
||||||
|
|
||||||
|
- **The output is empty or near-empty.** Do not retry, and do not guess at the
|
||||||
|
contents. Say which file, what the byte count was, and that it needs a vision
|
||||||
|
path — then either use `docmark` with a local vision model, or ask. A
|
||||||
|
confident summary of a document you could not read is the worst possible
|
||||||
|
output of this skill.
|
||||||
|
- **The document is client material and the task would send its contents
|
||||||
|
anywhere** — a commit message, an issue, a PR body, a cloud model. That is
|
||||||
|
ADR-0014's refusal, and it is not this skill's call to make.
|
||||||
|
- **The file is encrypted or password-protected.** markitdown will fail; asking
|
||||||
|
for the password is a human's decision, not a retry.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: dream
|
name: dream
|
||||||
|
surfaces: [claude-code, library, chat]
|
||||||
|
summary: "Reflective consolidation pass over an agent's memory directory"
|
||||||
|
use_when: "Pruning/merging memory files, rebuilding a memory index"
|
||||||
description: >
|
description: >
|
||||||
Run a "dream" — a reflective memory consolidation pass over an agent's memory
|
Run a "dream" — a reflective memory consolidation pass over an agent's memory
|
||||||
directory. Use this skill whenever the user says "dream", "run a dream", "consolidate
|
directory. Use this skill whenever the user says "dream", "run a dream", "consolidate
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: experiment-spec
|
name: experiment-spec
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol"
|
||||||
|
use_when: "Scoping a research/ML phase instead of a feature"
|
||||||
description: Write a rigorous experiment spec for a research phase before any code or training runs. Use instead of feature-spec for scientific/ML research projects. Enforces falsifiable hypothesis, quantitative acceptance criteria, baseline comparison, and null-result protocol.
|
description: Write a rigorous experiment spec for a research phase before any code or training runs. Use instead of feature-spec for scientific/ML research projects. Enforces falsifiable hypothesis, quantitative acceptance criteria, baseline comparison, and null-result protocol.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -182,7 +185,7 @@ If silhouette ≤ 0.20:
|
|||||||
|
|
||||||
**After experiment concludes:**
|
**After experiment concludes:**
|
||||||
- `brain_write` to `jepa-fx/failures/` with any new failure modes discovered
|
- `brain_write` to `jepa-fx/failures/` with any new failure modes discovered
|
||||||
- `session_log` with outcome
|
- `session_log` with `{session_id: "<this session's id>", skill: "experiment-spec", phase: "conclude", final_status: "pass" | "fail" | "skip", message: "<one-line outcome>"}`
|
||||||
|
|
||||||
## Cross-References
|
## Cross-References
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: feature-spec
|
name: feature-spec
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Implementation-level feature spec (tighter than spec-driven-dev)"
|
||||||
|
use_when: "Scoping one feature inside an already-specced project"
|
||||||
description: Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it.
|
description: Write a tight implementation-level spec for a single feature or component before coding it. Use after a project-level spec exists (see spec-driven-dev) and you need to nail down one feature inside it.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,6 +32,90 @@ A feature spec is the implementation-level contract for one feature or component
|
|||||||
2. **An "Out of scope" section is mandatory.** If you do not draw the boundary, the implementer (you, future-you, or another agent) will guess wrong.
|
2. **An "Out of scope" section is mandatory.** If you do not draw the boundary, the implementer (you, future-you, or another agent) will guess wrong.
|
||||||
3. **Every technical decision in "Technical approach" must have a rationale.** A decision is any choice where a reasonable alternative existed and was not taken. "Use Postgres" is not a decision; "use Postgres because the rest of the stack uses it and we need transactions across user + invoice" is.
|
3. **Every technical decision in "Technical approach" must have a rationale.** A decision is any choice where a reasonable alternative existed and was not taken. "Use Postgres" is not a decision; "use Postgres because the rest of the stack uses it and we need transactions across user + invoice" is.
|
||||||
|
|
||||||
|
## EARS-lite acceptance criteria
|
||||||
|
|
||||||
|
Measurable criteria answer "is it done?". They do not always answer "what should it do
|
||||||
|
when X happens?" — and behavioural requirements written as prose are where ambiguity
|
||||||
|
survives review. For any criterion that is conditional or event-driven, use EARS-lite
|
||||||
|
instead of prose:
|
||||||
|
|
||||||
|
```
|
||||||
|
WHEN <trigger> THE SYSTEM SHALL <observable response>
|
||||||
|
WHILE <state> THE SYSTEM SHALL <observable response>
|
||||||
|
IF <unwanted condition> THEN THE SYSTEM SHALL <observable response>
|
||||||
|
```
|
||||||
|
|
||||||
|
There is a fourth, unconditional form. Use it for invariants — the properties that must
|
||||||
|
hold with no trigger at all, which are usually the ones that matter most:
|
||||||
|
|
||||||
|
```
|
||||||
|
THE SYSTEM SHALL <observable response>
|
||||||
|
```
|
||||||
|
|
||||||
|
Worked, deliberately from a domain unlike whatever you are specifying:
|
||||||
|
|
||||||
|
```
|
||||||
|
THE SYSTEM SHALL reject a firmware image whose signature does not verify
|
||||||
|
WHEN a sensor reports a temperature above the shutdown limit
|
||||||
|
THE SYSTEM SHALL open the contactor within 200ms
|
||||||
|
WHILE the contactor is open
|
||||||
|
THE SYSTEM SHALL refuse every start command
|
||||||
|
IF a sensor reading is missing or stale by more than one interval
|
||||||
|
THEN THE SYSTEM SHALL treat it as above the limit
|
||||||
|
```
|
||||||
|
|
||||||
|
The example is off-domain on purpose. An on-domain example gets copied: a spec written
|
||||||
|
against an invoice-flavoured example inherited a requirement about extraction confidence
|
||||||
|
that its feature request never mentioned, and the author only noticed on review. Read
|
||||||
|
the shapes, not the content.
|
||||||
|
|
||||||
|
Each conditional line names a trigger and one observable response, which usually maps
|
||||||
|
onto a single test. The `WHILE` and unconditional forms are universally quantified —
|
||||||
|
"every start command", "a firmware image" — so they map onto a property or a set of
|
||||||
|
cases, not one assertion. That is still worth writing: it closes the requirements half
|
||||||
|
of the audit trail, which is otherwise issue-body prose.
|
||||||
|
|
||||||
|
**This is an option, not a replacement.** Threshold criteria ("p99 < 200ms under 100
|
||||||
|
RPS") stay as they are — EARS phrasing adds nothing to a number. Use it for conditional
|
||||||
|
behaviour, error handling, and state-dependent rules.
|
||||||
|
|
||||||
|
## Context snapshot
|
||||||
|
|
||||||
|
The executor of this spec will not see the conversation that produced it, and often
|
||||||
|
cannot see the architecture document either — a different agent, a fresh session, a
|
||||||
|
different machine. A spec that says "see the architecture doc" hands them a lookup they
|
||||||
|
may not be able to perform.
|
||||||
|
|
||||||
|
So inline the specific facts this spec depends on, not the whole document:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Context snapshot
|
||||||
|
Taken from <source> @ <commit-or-date>. Re-check before implementing if stale.
|
||||||
|
|
||||||
|
- The mailbox poller writes rows to `invoices` with `state = 'received'`.
|
||||||
|
- The payment-file writer reads `invoices` and emits one file per batch.
|
||||||
|
- Money is int64 minor units plus an ISO-4217 code; float is banned repo-wide.
|
||||||
|
```
|
||||||
|
|
||||||
|
Three rules keep this from becoming a second source of truth, which is the real
|
||||||
|
objection to inlining and a good one:
|
||||||
|
|
||||||
|
1. **Snapshot what the implementer cannot look up and cannot proceed without.** That is
|
||||||
|
wider than "facts this spec's decisions rest on" — a table name may underpin no
|
||||||
|
decision here and still be the thing that blocks writing the migration. It is still
|
||||||
|
narrower than the whole document: if a fact changes nothing and blocks nothing, leave
|
||||||
|
it out, and say what you left out so the omission reads as deliberate.
|
||||||
|
2. **Always cite source and version.** A snapshot without provenance cannot be
|
||||||
|
revalidated, and an unrevalidatable copy is exactly the drift everyone fears.
|
||||||
|
3. **If you could not read the source either, say so in those words.** Writing a spec
|
||||||
|
against a document you cannot open is common and is not a reason to skip the section
|
||||||
|
— it is a reason to mark the whole block `UNVERIFIED — reconstructed, not read`, and
|
||||||
|
to make confirming it the implementer's first task, before the first failing test.
|
||||||
|
An unmarked guess is the only genuinely dangerous version.
|
||||||
|
4. **The snapshot is evidence, not authority.** Open the section with one line to that
|
||||||
|
effect — "where this disagrees with the source, the source wins and this spec is
|
||||||
|
stale" — rather than annotating each fact.
|
||||||
|
|
||||||
## Spec Template
|
## Spec Template
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
@@ -39,12 +126,21 @@ A feature spec is the implementation-level contract for one feature or component
|
|||||||
What problem does this feature solve? For whom? Why now?
|
What problem does this feature solve? For whom? Why now?
|
||||||
(2-4 sentences. If you cannot answer "why now," ask before writing the rest.)
|
(2-4 sentences. If you cannot answer "why now," ask before writing the rest.)
|
||||||
|
|
||||||
|
## Context snapshot
|
||||||
|
[Facts the implementer cannot look up, with source and version. See "Context snapshot" above.]
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
- [ ] Criterion 1 — measurable and verifiable
|
- [ ] Criterion 1 — measurable and verifiable
|
||||||
- [ ] Criterion 2 — measurable and verifiable
|
- [ ] Criterion 2 — measurable and verifiable
|
||||||
- [ ] Criterion 3 — measurable and verifiable
|
- [ ] Criterion 3 — measurable and verifiable
|
||||||
|
|
||||||
|
### Behavioural criteria (EARS-lite)
|
||||||
|
|
||||||
|
[Conditional and event-driven behaviour, one trigger and one observable response per
|
||||||
|
line. Not checkboxes — these are properties, and several need more than one test each.
|
||||||
|
Omit the subsection entirely if the feature has no conditional behaviour.]
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
Non-negotiable requirements the solution must satisfy.
|
Non-negotiable requirements the solution must satisfy.
|
||||||
@@ -149,7 +245,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
|||||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||||
|
|
||||||
**At end of each phase:**
|
**At end of each phase:**
|
||||||
- `session_log` with `{skill: "feature-spec", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "feature-spec", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** draft
|
**Phases for this skill:** draft
|
||||||
|
|
||||||
@@ -158,11 +254,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
|
|||||||
- `fail` — the phase's intended outcome was NOT reached (spec rejected, blocked on unresolved questions).
|
- `fail` — the phase's intended outcome was NOT reached (spec rejected, blocked on unresolved questions).
|
||||||
- `skip` — phase was skipped intentionally.
|
- `skip` — phase was skipped intentionally.
|
||||||
|
|
||||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
|
||||||
|
|
||||||
## Mode 2 Routing Note
|
|
||||||
|
|
||||||
Spec writing requires interpretation of intent and tradeoff reasoning — work where Claude generally outperforms a 9-13B local model. This skill is intentionally **not** a Mode 2 routing candidate. In Mode 2, the routing layer should leave `feature-spec` calls on Claude.
|
|
||||||
|
|
||||||
## Cross-References
|
## Cross-References
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: gitea-ci
|
name: gitea-ci
|
||||||
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, buildah image build, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
|
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.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Gitea CI/CD Pipeline Setup
|
# Gitea CI/CD Pipeline Setup
|
||||||
@@ -31,7 +34,7 @@ Before writing any YAML, collect the following. Use AskUserQuestion for anything
|
|||||||
| Container registry | Ask or check existing config | `localhost:5000` on koala, or external |
|
| Container registry | Ask or check existing config | `localhost:5000` on koala, or external |
|
||||||
| k3s namespace | k8s/ manifests or ask | Deploy target |
|
| k3s namespace | k8s/ manifests or ask | Deploy target |
|
||||||
| GitHub mirror repo | Ask | Target for the mirror job |
|
| GitHub mirror repo | Ask | Target for the mirror job |
|
||||||
| Image name | Ask or derive from repo name | Used in buildah + k3s steps |
|
| Image name | Ask or derive from repo name | Used in buildctl + k3s steps |
|
||||||
|
|
||||||
Check the project's `CLAUDE.md` for infra notes first — often this answers most questions without needing to ask.
|
Check the project's `CLAUDE.md` for infra notes first — often this answers most questions without needing to ask.
|
||||||
|
|
||||||
@@ -133,35 +136,46 @@ jobs:
|
|||||||
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# infra#309: do NOT paste a build implementation here. The estate has
|
||||||
|
# exactly one, mathias/infra's ci-templates/build-image.sh, and each
|
||||||
|
# repo carries a byte-identical copy at .gitea/build-image.sh that a
|
||||||
|
# drift gate keeps current.
|
||||||
|
#
|
||||||
|
# Copying the implementation is what made the buildah->buildctl
|
||||||
|
# migration (infra#241) an eleven-repo sprint, and what left this very
|
||||||
|
# skill handing out the broken version for weeks after the app repos
|
||||||
|
# were fixed. Per-repo differences are INPUTS, not edits to the copy.
|
||||||
|
#
|
||||||
|
# To adopt in a new repo:
|
||||||
|
# 1. add the repo to mathias/infra ci-templates/adopters.txt
|
||||||
|
# 2. run `task ci:sync` in mathias/infra -- it writes the copy
|
||||||
|
# 3. commit .gitea/build-image.sh in your repo
|
||||||
|
# 4. call it as below
|
||||||
|
#
|
||||||
|
# Skipping step 1 leaves the copy unmanaged: it works today and falls
|
||||||
|
# silently behind the next time the canonical file changes.
|
||||||
|
#
|
||||||
|
# Inputs: IMAGE (required, usually the workflow-level env), SHA_TAG,
|
||||||
|
# VERSION_TAG, GIT_SHA, IMAGE_SOURCE, BUILD_ARGS, EXTRA_LABELS,
|
||||||
|
# CONTEXT, DOCKERFILE_DIR, DOCKERFILE_NAME, PUSH_LATEST. Read the
|
||||||
|
# canonical file's header for the full list.
|
||||||
|
#
|
||||||
|
# `bash <script>`, not a direct exec: a file written through the Gitea
|
||||||
|
# API lands 0644, and CI that depends on a file mode is a trap.
|
||||||
- name: Build and push to local registry
|
- name: Build and push to local registry
|
||||||
run: |
|
env:
|
||||||
REGISTRY="localhost:5000"
|
SHA_TAG: ${{ steps.meta.outputs.sha-tag }}
|
||||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
VERSION_TAG: ${{ steps.meta.outputs.version-tag }}
|
||||||
buildah build \
|
GIT_SHA: ${{ github.sha }}
|
||||||
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
IMAGE_SOURCE: ${{ github.repositoryUrl }}
|
||||||
--label "org.opencontainers.image.source=${{ github.repositoryUrl }}" \
|
run: bash .gitea/build-image.sh
|
||||||
-t ${REF} \
|
|
||||||
-t ${REGISTRY}/${{ env.IMAGE }}:latest \
|
|
||||||
.
|
|
||||||
buildah push --tls-verify=false ${REF}
|
|
||||||
buildah push --tls-verify=false ${REGISTRY}/${{ env.IMAGE }}:latest
|
|
||||||
[[ -n "${{ steps.meta.outputs.version-tag }}" ]] && \
|
|
||||||
buildah push --tls-verify=false \
|
|
||||||
${REF} \
|
|
||||||
${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }} || true
|
|
||||||
echo "✓ Image pushed to ${REF}"
|
|
||||||
|
|
||||||
- name: Smoke test
|
# No smoke test step. The one this skill used to recommend ran
|
||||||
run: |
|
# `sudo k3s ctr run` against the host's containerd -- that worked when
|
||||||
REGISTRY="localhost:5000"
|
# the runner was the Arch host, and is impossible now that it is a pod:
|
||||||
REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}"
|
# no sudo, no ctr, no containerd socket (`sudo: command not found`).
|
||||||
CNAME="smoke-${{ steps.meta.outputs.sha-tag }}"
|
# The deploy job's `Verify rollout` + `Confirm pod running new image`
|
||||||
sudo k3s ctr images pull --plain-http ${REF}
|
# below assert the same property against the real workload.
|
||||||
OUTPUT=$(timeout 5 sudo k3s ctr run --rm ${REF} ${CNAME} /<binary-name> 2>&1 || true)
|
|
||||||
sudo k3s ctr containers delete ${CNAME} 2>/dev/null || true
|
|
||||||
echo "$OUTPUT" | grep -q "<project-name>" \
|
|
||||||
&& echo "✓ Smoke test passed" \
|
|
||||||
|| echo "⚠ Smoke test inconclusive: $OUTPUT"
|
|
||||||
|
|
||||||
# ── 3. Deploy to k3s ────────────────────────────────────────────────────────
|
# ── 3. Deploy to k3s ────────────────────────────────────────────────────────
|
||||||
deploy:
|
deploy:
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: grill-me
|
name: grill-me
|
||||||
|
surfaces: [claude-code, library, chat]
|
||||||
|
summary: "Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem"
|
||||||
|
use_when: "Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod"
|
||||||
description: Critically interrogates plans, ideas, and proposals to make them sharper, more robust, and better grounded — with the explicit goal of producing a meaningfully better plan by the end. Use this skill whenever the user wants to stress-test a plan, prepare for hard questions, check if an idea is ready to act on, validate a hypothesis, or explicitly asks to be challenged, questioned, or grilled. Also trigger when a conversation has been exploring an idea for a while and it's time to converge — use this skill to check if the idea is actually ready. Trigger phrases include "grill me", "challenge this", "poke holes", "am I missing something", "is this ready", "stress-test this", "devil's advocate", "what am I not seeing".
|
description: Critically interrogates plans, ideas, and proposals to make them sharper, more robust, and better grounded — with the explicit goal of producing a meaningfully better plan by the end. Use this skill whenever the user wants to stress-test a plan, prepare for hard questions, check if an idea is ready to act on, validate a hypothesis, or explicitly asks to be challenged, questioned, or grilled. Also trigger when a conversation has been exploring an idea for a while and it's time to converge — use this skill to check if the idea is actually ready. Trigger phrases include "grill me", "challenge this", "poke holes", "am I missing something", "is this ready", "stress-test this", "devil's advocate", "what am I not seeing".
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -145,3 +148,12 @@ This skill maps to the **convergence moments** in the Double Diamond workflow:
|
|||||||
- **Mid-Diamond 2 (Develop):** Use Quick Poke when an agent's experiment result is ambiguous and you're deciding whether to continue or pivot.
|
- **Mid-Diamond 2 (Develop):** Use Quick Poke when an agent's experiment result is ambiguous and you're deciding whether to continue or pivot.
|
||||||
|
|
||||||
The skill does NOT replace human judgment at convergence points — it sharpens the plan so that judgment is better informed.
|
The skill does NOT replace human judgment at convergence points — it sharpens the plan so that judgment is better informed.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Advisory. Produces sharper questions and a better plan; decides nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the plan's weak point is a decision that is the operator's to
|
||||||
|
make rather than a gap in reasoning — surface it as a question with options and a
|
||||||
|
recommendation, and do not grill past it. Also stop when a round produces no new
|
||||||
|
objection: interrogation past that point manufactures doubt rather than finding it.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
name: lineage
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Backward decision archaeology — was this tried before, and does the reason it failed still hold"
|
||||||
|
use_when: "Before proposing something that feels new; when an idea keeps resurfacing; when you inherit code whose reason is gone"
|
||||||
|
description: Backward decision archaeology. Find out whether an idea was already tried here, why it was dropped, and whether that reason still applies — using git history, decision records and the issue tracker rather than asking whoever is still around. Use before proposing a change that feels novel, when inheriting unexplained code, or when the same idea keeps being re-litigated.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lineage
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Most "new" ideas in an established codebase are revivals. The valuable question is never
|
||||||
|
"is this a good idea" in the abstract — it is **"was this tried, what killed it, and is
|
||||||
|
the killer still alive?"**
|
||||||
|
|
||||||
|
A past failure is not evidence against trying again. It is context for trying again
|
||||||
|
differently, and the difference between those two readings is usually one commit message.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Before proposing something that feels new in a codebase older than you
|
||||||
|
- When an idea keeps resurfacing and being re-argued from scratch
|
||||||
|
- When you inherit code and cannot tell whether a constraint is load-bearing or vestigial
|
||||||
|
- Before writing an ADR — the reason a thing was dropped belongs in your Context section
|
||||||
|
|
||||||
|
**Do not use for:** code you wrote this week, or a question the current code answers.
|
||||||
|
Archaeology on a live construction site is just reading.
|
||||||
|
|
||||||
|
## The tool
|
||||||
|
|
||||||
|
```sh
|
||||||
|
skills/lineage/lineage.sh <term> [repo-path]
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs the searches people skip because they are fiddly to type. In particular
|
||||||
|
`git log -S<term>`, which finds commits where the term's **occurrence count changed** —
|
||||||
|
so it surfaces the *removal* as well as the introduction. A plain `--grep` never finds a
|
||||||
|
deletion, and the deletion is the whole point.
|
||||||
|
|
||||||
|
It also lists files that once carried the term and no longer exist, and names the two
|
||||||
|
sources it cannot search for you.
|
||||||
|
|
||||||
|
**A no-match is not a finding.** The first version of this tool said a null meant "this is
|
||||||
|
new here"; a pressure test caught it asserting exactly that about the most-discussed
|
||||||
|
removed feature in this repo — the search term merely had a space in it. One string
|
||||||
|
against one repo proves nothing. Try at least three variants (the identifier name, a
|
||||||
|
one-word form, the name it would have had) and the brain and tracker searches before
|
||||||
|
telling anyone an idea is novel. This is the same rule as the anti-patterns table below,
|
||||||
|
and it is there because the tool broke it.
|
||||||
|
|
||||||
|
Note also that `--grep` takes a regex and `-S` takes a literal string, so a term
|
||||||
|
containing `.`, `[`, `*` or `+` matches differently in the two halves of the output.
|
||||||
|
|
||||||
|
## The sequence
|
||||||
|
|
||||||
|
**1. Search on the identifier, not the concept.** If the thing had a name in the code —
|
||||||
|
a function, a flag, a config key — `git log --all -S<name>` typically returns exactly two
|
||||||
|
commits, its birth and its death, on one screen. That is the fastest path to the answer in
|
||||||
|
this whole sequence.
|
||||||
|
|
||||||
|
Path-scoped log (`git log --all -- <file>`) is the weaker move and it is the one people
|
||||||
|
reach for: a function deleted from a file that still exists is the *dominant* removal
|
||||||
|
shape, and the path search buries it under every other commit that touched the file.
|
||||||
|
|
||||||
|
Concept words are the fallback, and they are lossy — a term with a space in it will miss
|
||||||
|
the record entirely. Try several spellings before you conclude anything.
|
||||||
|
|
||||||
|
**2. Read the removal commit's full body, not its subject.**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git log -1 --format=%B <sha>
|
||||||
|
git show --stat <sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
This is where the answer almost always is. A subject line says *what*; the body says
|
||||||
|
*why*, and the why is what you came for. Read the diff too — what was removed alongside
|
||||||
|
it tells you how load-bearing it was.
|
||||||
|
|
||||||
|
**3. Search outside git — that is where this estate keeps its reasoning.** Two sources,
|
||||||
|
both routinely decisive, neither reachable from a commit log:
|
||||||
|
|
||||||
|
- **The brain** (`brain_query`). Postmortems and generalized rules live there. In the run
|
||||||
|
that shaped this skill, the single best artifact — the rule abstracted from the incident,
|
||||||
|
with detection one-liners — came from the brain and from nowhere else.
|
||||||
|
- **The issue tracker, including comments.** The commit records *that* something was
|
||||||
|
removed; the argument for it is usually in a comment. Search closed issues especially —
|
||||||
|
a closed issue is where a decision usually is.
|
||||||
|
|
||||||
|
A git-only search finds the what and misses the why.
|
||||||
|
|
||||||
|
**4. Test whether the failure conditions still hold — empirically, not from the record.**
|
||||||
|
This is the step that makes the whole exercise worth doing, and it is the one that gets
|
||||||
|
hand-waved. Write the conditions down as separate claims, then **run a command per claim**:
|
||||||
|
|
||||||
|
| Claim from the record | How you check it today |
|
||||||
|
|---|---|
|
||||||
|
| "absolute links break across two path roots" | `readlink` one, `test -e` it on this host |
|
||||||
|
| "it is gitignored, so nobody sees the rot" | `grep` the ignore file |
|
||||||
|
| "the detector does not cover it" | `grep` the detector for the path it scans |
|
||||||
|
|
||||||
|
Twenty seconds each, and it converts *"the record claims X"* into *"X is true right now"* —
|
||||||
|
which is the only form of the answer worth giving. A constraint that has since been
|
||||||
|
removed turns a dead idea live; one that is still true kills a revival on the spot.
|
||||||
|
|
||||||
|
**5. Separate the mechanism from the goal.** A revival proposal usually bundles both, and
|
||||||
|
they can have opposite verdicts: the goal is still worth wanting, the mechanism is the one
|
||||||
|
that failed. Splitting them is often the whole finding — you end up agreeing with the
|
||||||
|
proposer and rejecting their implementation, which is a far more useful answer than yes
|
||||||
|
or no.
|
||||||
|
|
||||||
|
## What "it was tried and failed" can mean
|
||||||
|
|
||||||
|
| Reading | Tell | What to do |
|
||||||
|
|---|---|---|
|
||||||
|
| **Structurally impossible** | The removal commit says *why* it cannot work, not that it broke | Believe it. Look for a different mechanism for the same goal |
|
||||||
|
| **Failed under conditions that changed** | The blocker names a version, a host, a tool that has since moved | Re-open it, citing what changed |
|
||||||
|
| **Failed on execution** | It broke, was reverted, nobody diagnosed it | Genuinely undecided. The revival is legitimate; budget for the diagnosis |
|
||||||
|
| **Never actually tried** | Discussed, an issue exists, no commit | There is no evidence either way. Say so rather than inheriting someone's pessimism |
|
||||||
|
| **Still there, in a different shape** | The idea returned under another name | The most missable one — check before you build it twice |
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Answer the question that was asked, then show your work:
|
||||||
|
|
||||||
|
- **Verdict** — tried / not tried / tried and later restored differently
|
||||||
|
- **The decisive artifact** — commit sha, issue number, or ADR, so the reader can check
|
||||||
|
- **What killed it**, quoted from the source rather than paraphrased
|
||||||
|
- **Whether that still applies**, per condition, not as a single yes or no
|
||||||
|
|
||||||
|
If the decisive evidence is one commit message, quote it. A conclusion that makes the
|
||||||
|
reader re-run your search has not saved them anything.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
| Anti-pattern | Why it fails |
|
||||||
|
|---|---|
|
||||||
|
| `--grep` only | Finds discussion, misses deletions. The deletion is the evidence |
|
||||||
|
| Reading the commit subject and stopping | Subjects say what; you need why |
|
||||||
|
| Treating a past failure as a verdict | Conditions change. That is the entire question |
|
||||||
|
| Git-only search | The argument is usually in the issue tracker |
|
||||||
|
| "It was tried and it failed" with no sha | Unverifiable, and it will be re-litigated next quarter |
|
||||||
|
| Assuming absence means it was never considered | It may have been rejected in a conversation. Say what you searched |
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only. Reports what the record says; changes nothing and revives nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the record is genuinely silent — no commit, no issue, no ADR.
|
||||||
|
State what you searched and that nothing exists, rather than inferring intent from
|
||||||
|
absence. Someone deciding not to build a thing and nobody ever considering it look
|
||||||
|
identical in git, and only one of them is an argument.
|
||||||
|
|
||||||
|
Also stop when the answer turns on why a *person* did something and the record does not
|
||||||
|
say. Ask them. Reconstructing a colleague's reasoning from a diff is how a guess acquires
|
||||||
|
a citation.
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- `adr` — what you find here belongs in the Context section of the ADR you are about to
|
||||||
|
write. If the thing was decided before, your ADR supersedes that one; say so.
|
||||||
|
- `debug` — the same discipline aimed at a failure rather than a decision: evidence
|
||||||
|
before hypothesis, and the removal commit is evidence.
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# lineage.sh — decision archaeology for one term.
|
||||||
|
#
|
||||||
|
# Answers "why does X exist, was it tried before, and is this 'new' idea a
|
||||||
|
# revival?"
|
||||||
|
#
|
||||||
|
# Its real value is that it makes you run `git log -S` at all — that is a nudge,
|
||||||
|
# not a capability. -S finds commits where the term's occurrence COUNT changed,
|
||||||
|
# so it surfaces a removal; --grep only finds people talking about one. On an
|
||||||
|
# identifier name (`wire_claude_repo`) -S typically returns exactly two commits,
|
||||||
|
# birth and death, which is the fastest path to the answer in this whole file.
|
||||||
|
#
|
||||||
|
# Note the two searches do not match the same way: --grep takes a regex, -S takes
|
||||||
|
# a literal string. A term containing . [ * + behaves differently in each.
|
||||||
|
#
|
||||||
|
# Usage: lineage.sh <term> [repo-path]
|
||||||
|
#
|
||||||
|
# Read-only: nothing here writes, and it never touches the index or worktree.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
[ $# -ge 1 ] || { echo "usage: lineage.sh <term> [repo-path]" >&2; exit 2; }
|
||||||
|
TERM_=$1
|
||||||
|
REPO=${2:-.}
|
||||||
|
|
||||||
|
git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1 || {
|
||||||
|
echo "lineage: $REPO is not a git repository" >&2; exit 2; }
|
||||||
|
|
||||||
|
# Say which repo, always. The default is the cwd, and in a worktree-heavy setup
|
||||||
|
# that is routinely not the repo the caller means — a confident null against the
|
||||||
|
# wrong tree is the worst thing this tool can produce.
|
||||||
|
TOPLEVEL=$(git -C "$REPO" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$REPO")
|
||||||
|
printf 'searching: %s term: "%s"\n' "$TOPLEVEL" "$TERM_"
|
||||||
|
|
||||||
|
FMT='%h %ad %an %s'
|
||||||
|
DATE='--date=short'
|
||||||
|
found=0
|
||||||
|
|
||||||
|
section() { printf '\n== %s ==\n' "$1"; }
|
||||||
|
|
||||||
|
# 1. Commits whose MESSAGE mentions it — the stated reasoning.
|
||||||
|
msgs=$(git -C "$REPO" log --all -i --grep="$TERM_" --format="$FMT" $DATE 2>/dev/null || true)
|
||||||
|
if [ -n "$msgs" ]; then
|
||||||
|
found=1
|
||||||
|
section "Commits that talk about it (message)"
|
||||||
|
printf '%s\n' "$msgs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Commits where the term was ADDED or REMOVED — the actual history.
|
||||||
|
# -S counts occurrences, so a deletion shows up here and nowhere else.
|
||||||
|
picks=$(git -C "$REPO" log --all -S"$TERM_" --format="$FMT" $DATE 2>/dev/null || true)
|
||||||
|
if [ -n "$picks" ]; then
|
||||||
|
found=1
|
||||||
|
section "Commits where it appeared or disappeared (-S)"
|
||||||
|
printf '%s\n' "$picks"
|
||||||
|
printf '\n Oldest is the introduction. Anything after it that removed the term is\n'
|
||||||
|
printf ' the interesting one: read that commit before proposing this again.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Files that once contained it and no longer exist.
|
||||||
|
deleted=$(git -C "$REPO" log --all --diff-filter=D --name-only --format='%h' -S"$TERM_" 2>/dev/null \
|
||||||
|
| grep -v '^[0-9a-f]\{7,\}$' | grep -v '^$' | sort -u || true)
|
||||||
|
if [ -n "$deleted" ]; then
|
||||||
|
found=1
|
||||||
|
section "Files deleted that carried it"
|
||||||
|
printf '%s\n' "$deleted"
|
||||||
|
printf '\n Recover one with: git -C %s show <sha>^:<path>\n' "$REPO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Decision records naming it.
|
||||||
|
docs=$(git -C "$REPO" grep -l -i "$TERM_" -- 'docs/adr/*' 'docs/decisions/*' 'docs/*.md' 'ADR*' 2>/dev/null || true)
|
||||||
|
if [ -n "$docs" ]; then
|
||||||
|
found=1
|
||||||
|
section "Decision records naming it"
|
||||||
|
printf '%s\n' "$docs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This estate keeps most of its reasoning OUTSIDE the repo, so the two searches
|
||||||
|
# this tool cannot run are usually the decisive ones. Say so every time rather
|
||||||
|
# than letting their absence read as a null result.
|
||||||
|
section "Not searched here — do these by hand"
|
||||||
|
printf 'brain: brain_query "%s" — decisions and postmortems live there, not in docs/\n' "$TERM_"
|
||||||
|
printf 'gitea: closed issues and their COMMENTS. The argument for a removal is\n'
|
||||||
|
printf ' usually in a comment; the commit only records that it happened.\n'
|
||||||
|
|
||||||
|
if [ "$found" -eq 0 ]; then
|
||||||
|
printf '\nno match for the exact string "%s" in %s.\n' "$TERM_" "$TOPLEVEL"
|
||||||
|
printf 'This is NOT evidence the idea is new. It is one string against one repo.\n'
|
||||||
|
printf 'Before concluding anything, try: the identifier or function name; a\n'
|
||||||
|
printf 'one-word variant (spaces and punctuation break the match); the name it\n'
|
||||||
|
printf 'would have had; and the brain and issue searches above.\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n-- what this cannot tell you --------------------------------------------\n'
|
||||||
|
printf 'Why it was removed, if it was. The commit subject is a summary; read the\n'
|
||||||
|
printf 'body, the PR, and the issue it closed. A past failure under conditions that\n'
|
||||||
|
printf 'no longer hold is not evidence against trying again — it is context for\n'
|
||||||
|
printf 'trying again differently.\n'
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: own-the-task
|
name: own-the-task
|
||||||
|
surfaces: [claude-code, library, chat]
|
||||||
|
summary: "Delegation ownership — Intake → Brief → Plan → Execute"
|
||||||
|
use_when: "A task is handed over open-ended; \"take this off my plate\"; before a plan exists"
|
||||||
description: Take ownership of a delegated task like a hired employee — interview first, write a handover brief, propose a plan, then execute. Use whenever the user says "take this off my plate", "own this", "act as my AI employee", "interview me first", "ask me questions before you start", or delegates anything open-ended where the real goal, the why, and the definition of done are not yet on the table. Trigger even when "interview" is never said — any delegation where getting the intent wrong would waste the whole effort belongs here.
|
description: Take ownership of a delegated task like a hired employee — interview first, write a handover brief, propose a plan, then execute. Use whenever the user says "take this off my plate", "own this", "act as my AI employee", "interview me first", "ask me questions before you start", or delegates anything open-ended where the real goal, the why, and the definition of done are not yet on the table. Trigger even when "interview" is never said — any delegation where getting the intent wrong would waste the whole effort belongs here.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: planning
|
name: planning
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Planning and task breakdown"
|
||||||
|
use_when: "Sprint planning, task decomposition"
|
||||||
description: Decompose work into small, verifiable, ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable units.
|
description: Decompose work into small, verifiable, ordered tasks. Use when you have a spec or clear requirements and need to break work into implementable units.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: playwright
|
name: playwright
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring"
|
||||||
|
use_when: "Adding browser tests to any project with a web UI"
|
||||||
description: Add and run Playwright E2E tests for any project with a web UI — covers setup, HTMX-specific patterns, SSE/streaming, Taskfile integration, and CI wiring.
|
description: Add and run Playwright E2E tests for any project with a web UI — covers setup, HTMX-specific patterns, SSE/streaming, Taskfile integration, and CI wiring.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: problem-analysis
|
name: problem-analysis
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Deep problem understanding before coding"
|
||||||
|
use_when: "Starting any non-trivial task"
|
||||||
description: Deeply understand a problem before coding begins. Identify the real problem, stakeholders, constraints, and success criteria. Use before starting any non-trivial feature.
|
description: Deeply understand a problem before coding begins. Identify the real problem, stakeholders, constraints, and success criteria. Use before starting any non-trivial feature.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: refactoring
|
name: refactoring
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Systematic refactoring methodology (Fowler)"
|
||||||
|
use_when: "When improving existing code"
|
||||||
description: Systematic refactoring methodology based on Martin Fowler's catalog. Apply during the REFACTOR phase of TDD, never while making code changes that modify behavior.
|
description: Systematic refactoring methodology based on Martin Fowler's catalog. Apply during the REFACTOR phase of TDD, never while making code changes that modify behavior.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: regulatory-risk-assessment
|
name: regulatory-risk-assessment
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Structured risk register for regulated-industry features"
|
||||||
|
use_when: "Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data"
|
||||||
description: Produce a structured regulatory risk assessment / risk-register entry for regulated-industry features (banking, payments, PSD2/PSR, DORA, AML). Use when filing a CAD issue that needs a Risk:LOW/MEDIUM/HIGH level, or designing features touching payments, auth, data storage, or external APIs ("what are the risks?", "compliance gate", "risk register").
|
description: Produce a structured regulatory risk assessment / risk-register entry for regulated-industry features (banking, payments, PSD2/PSR, DORA, AML). Use when filing a CAD issue that needs a Risk:LOW/MEDIUM/HIGH level, or designing features touching payments, auth, data storage, or external APIs ("what are the risks?", "compliance gate", "risk register").
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
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".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Secret hygiene for agent sessions
|
||||||
|
|
||||||
|
Goal: stop the recurring leak pattern where a diagnostic command renders a
|
||||||
|
SOPS-encrypted value or a Kubernetes Secret value into the session
|
||||||
|
transcript, forcing a rotation.
|
||||||
|
|
||||||
|
**Confirmed past leaks** (see `mathias/infra#39` rotation umbrella):
|
||||||
|
- `POSTGRES_PASSWORD` — `kubectl exec ... env` rendered envFrom value
|
||||||
|
- `INFRA_MCP_TOKEN` — `sops -d ingress.enc.yaml` rendered the bearer in the configuration-snippet
|
||||||
|
- `brain_app` postgres DSN — pre-fix ingestion pod log (hyperguild#20)
|
||||||
|
|
||||||
|
Three are pattern, not bug.
|
||||||
|
|
||||||
|
## Rules — never do these
|
||||||
|
|
||||||
|
| Forbidden | Why | Use instead |
|
||||||
|
|---|---|---|
|
||||||
|
| `kubectl exec <pod> -- env` (or `printenv`) | Renders every envFrom + valueFrom.secretKeyRef plaintext | `kubectl describe pod <pod>` — k8s redacts secretKeyRef in describe |
|
||||||
|
| `sops -d <file>` to inspect | Whole-file decrypt dumps everything | `sops --extract '["key"]' <file>` for pinpoint reads |
|
||||||
|
| `kubectl get secret ... -o jsonpath='{.data}'` (whole map) | Returns base64 of every key; trivial to b64-decode the lot | `... -o jsonpath='{.data.SPECIFIC_KEY}'` for the one key needed |
|
||||||
|
| Echoing a generated token to verify length / content | Renders the new token into transcript, defeating the rotation | Verify functionally: `curl ... -H "Authorization: Bearer $(cat /tmp/tokfile)"` — never echo `$NEW_TOK` |
|
||||||
|
| Storing a token in a shell variable then running `echo` / `set` / `env` | Same as above | Pipe through file (mode 600) or stdin to the next consumer |
|
||||||
|
|
||||||
|
## Rules — do these instead
|
||||||
|
|
||||||
|
- **Verify by side-effect, not by value.** If a token rotation should make
|
||||||
|
the old token return 401 and the new one return 200, test those two
|
||||||
|
HTTP responses. Never check `$OLD == $LEAKED` by printing it.
|
||||||
|
- **Pinpoint SOPS reads.** When you need ONE key from a SOPS file, always
|
||||||
|
`sops --extract '["..."]'`. The shape of the key path is safe to log;
|
||||||
|
the value isn't.
|
||||||
|
- **Stash tokens in `/dev/shm/<unique>` mode 600** and pass via filename,
|
||||||
|
not env. `shred -u` after use. Avoid `/tmp` if the system has `/dev/shm`.
|
||||||
|
- **If you must dump a secret to confirm wiring**, immediately rotate it
|
||||||
|
in the same commit train. Treat the dump as "this is now compromised".
|
||||||
|
|
||||||
|
## Rotation chain pattern when a token is already exposed
|
||||||
|
|
||||||
|
If a token was leaked in a prior step of the same session, chain two
|
||||||
|
rotations:
|
||||||
|
|
||||||
|
1. **T1**: replace the leaked value with a new one. The verification step
|
||||||
|
may also leak T1 (acceptable risk).
|
||||||
|
2. **T2**: replace T1 with a fresh value. Run this without any decrypt
|
||||||
|
step that emits to stdout. The new value lives only in the encrypted
|
||||||
|
file + the consumer's secret store (`~/.llmkeys`, the k8s Secret).
|
||||||
|
|
||||||
|
Example T2 path (Python, never decrypts to stdout):
|
||||||
|
|
||||||
|
```python
|
||||||
|
new_t2 = subprocess.check_output(["openssl","rand","-hex","32"]).decode().strip()
|
||||||
|
plaintext = subprocess.check_output(["sops","-d", path]).decode() # in-memory only
|
||||||
|
patched = re.sub(r'Bearer [a-f0-9]+(?=")', f"Bearer {new_t2}", plaintext)
|
||||||
|
# write patched via /dev/shm tmp + sops -e → write back
|
||||||
|
# DO NOT print(patched), DO NOT print(new_t2)
|
||||||
|
```
|
||||||
|
|
||||||
|
After T2 lands via GitOps + Flux, update consumers from the file stash
|
||||||
|
without echoing.
|
||||||
|
|
||||||
|
## Detection ideas (not blocking — telemetry)
|
||||||
|
|
||||||
|
- Add a `bash` pre-hook in Claude harness that refuses obvious patterns
|
||||||
|
(`kubectl exec * env*`, `sops -d *.enc.yaml`). Filed as a follow-up.
|
||||||
|
- Run a periodic grep over `~/.claude/projects/*/sessions/*.jsonl` for
|
||||||
|
high-entropy strings matching `[a-f0-9]{32,}` to catch new leak surface.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `gitea.d-ma.be/mathias/infra#39` — umbrella credential rotation issue with confirmed-leak roster.
|
||||||
|
- `gitea.d-ma.be/mathias/infra#44` — POSTGRES_PASSWORD rotation procedure (template for future incidents).
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: session-retrospective
|
name: session-retrospective
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Surface non-obvious learnings from a session log"
|
||||||
|
use_when: "After a coding session ends, before context is lost"
|
||||||
description: Review a completed coding session (from brain/sessions/*.jsonl) and surface non-obvious learnings worth preserving. Use after a session ends, before context is lost.
|
description: Review a completed coding session (from brain/sessions/*.jsonl) and surface non-obvious learnings worth preserving. Use after a session ends, before context is lost.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -113,7 +116,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
|||||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||||
|
|
||||||
**At end of each phase:**
|
**At end of each phase:**
|
||||||
- `session_log` with `{skill: "session-retrospective", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "session-retrospective", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** surfaced
|
**Phases for this skill:** surfaced
|
||||||
|
|
||||||
@@ -122,11 +125,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
|
|||||||
- `fail` — the phase's intended outcome was NOT reached (retrospective could not produce coherent candidates).
|
- `fail` — the phase's intended outcome was NOT reached (retrospective could not produce coherent candidates).
|
||||||
- `skip` — phase was skipped intentionally (no novel learnings worth surfacing).
|
- `skip` — phase was skipped intentionally (no novel learnings worth surfacing).
|
||||||
|
|
||||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
|
||||||
|
|
||||||
## Mode 2 Routing Note
|
|
||||||
|
|
||||||
Reading long session logs and pulling themes is high-volume, mechanical pattern-recognition work — a strong Mode 2 routing candidate. Until Plan 6 ships the routing pod, treat as Mode 1 only.
|
|
||||||
|
|
||||||
## Cross-References
|
## Cross-References
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: solid
|
name: solid
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "SOLID principles with Go examples"
|
||||||
|
use_when: "Design review, architecture decisions"
|
||||||
description: Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions.
|
description: Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -255,5 +258,15 @@ Domain types know nothing about HTTP, SQL, or external APIs.
|
|||||||
|
|
||||||
- `references/solid-principles.md` — canonical SOLID reference with TypeScript examples
|
- `references/solid-principles.md` — canonical SOLID reference with TypeScript examples
|
||||||
- `references/go-adaptation.md` — this workspace's Go-specific rewrite of each principle
|
- `references/go-adaptation.md` — this workspace's Go-specific rewrite of each principle
|
||||||
|
- `references/python-adaptation.md` — the Python rewrite: Protocol over ABC, and what does not carry over from Go
|
||||||
- Load `clean-code` skill for naming and structure
|
- Load `clean-code` skill for naming and structure
|
||||||
- Load `code-review` skill for detecting violations during review
|
- Load `code-review` skill for detecting violations during review
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Advisory. Names principle violations and suggests structure; writes no code.
|
||||||
|
|
||||||
|
Stop and hand back when: applying a principle would require a change larger than the one
|
||||||
|
under discussion. "This wants dependency inversion" on a two-line fix is a design
|
||||||
|
conversation, not a review comment, and forcing it through as one is how a small change
|
||||||
|
acquires an architecture.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# SOLID Principles: Python Adaptation
|
||||||
|
|
||||||
|
`references/go-adaptation.md` rewrites each principle for Go. This file does the same
|
||||||
|
for Python, and marks the places where the Go rewrite's advice does *not* carry over.
|
||||||
|
|
||||||
|
The canonical Python stack lives in `~/dev/.context/AGENT.md`; it is not restated here.
|
||||||
|
|
||||||
|
## The one idea that changes everything: `Protocol`
|
||||||
|
|
||||||
|
Go's interfaces are structural — a type satisfies one by having the methods, with no
|
||||||
|
declaration. Python has the same thing since 3.8, and it is the right default:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
class InvoiceStore(Protocol):
|
||||||
|
def get(self, invoice_id: InvoiceId) -> Invoice: ...
|
||||||
|
def save(self, invoice: Invoice) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Define the Protocol where it is *consumed*, not where it is implemented.** This is
|
||||||
|
Go's "accept interfaces, return structs" and it is what makes DIP real: the high-level
|
||||||
|
module owns the abstraction, and the adapter depends on it rather than the reverse.
|
||||||
|
- **Prefer `Protocol` to `ABC`.** An ABC forces implementers to inherit, which couples
|
||||||
|
them to your package and makes a third-party class impossible to adapt without a
|
||||||
|
wrapper. Use an ABC only when you genuinely need shared implementation, not just a
|
||||||
|
contract.
|
||||||
|
- Add `@runtime_checkable` only if you actually call `isinstance` — and if you are
|
||||||
|
calling `isinstance` against a Protocol, check whether the real problem is a missing
|
||||||
|
polymorphic method (an LSP smell).
|
||||||
|
|
||||||
|
## S — Single Responsibility
|
||||||
|
|
||||||
|
Same as Go: the unit is the module, not just the class. `utils.py` is the Python god
|
||||||
|
package — it cannot have one reason to change because it has no subject.
|
||||||
|
|
||||||
|
Python-specific tell: a class whose `__init__` stores arguments and whose only other
|
||||||
|
method is `run()` or `execute()`. That is a function with ceremony. Write the function.
|
||||||
|
|
||||||
|
## O — Open/Closed
|
||||||
|
|
||||||
|
Extension in Python usually means **passing behaviour in**, not subclassing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Closed for modification: adding a rule does not touch this function
|
||||||
|
def apply_rules(invoice: Invoice, rules: Sequence[Rule]) -> Invoice: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`functools.singledispatch` is the idiomatic open extension point when dispatch is on
|
||||||
|
type and the set of types grows outside your module. Beware the alternative that looks
|
||||||
|
Pythonic and is not: monkey-patching is modification pretending to be extension, and it
|
||||||
|
is invisible at the call site.
|
||||||
|
|
||||||
|
## L — Liskov
|
||||||
|
|
||||||
|
The Python-specific violation is **signature widening/narrowing that the type checker
|
||||||
|
would catch if you let it**. Under `mypy --strict`, an override that narrows a parameter
|
||||||
|
type or widens a return type is an error — which means most LSP violations in Python are
|
||||||
|
type errors you chose not to see. This is the strongest argument for `--strict` in a
|
||||||
|
design document rather than a style one.
|
||||||
|
|
||||||
|
Raising a *new* exception type in an override is the violation `mypy` will not catch:
|
||||||
|
callers wrote `except StoreError`, and your subclass raises `TimeoutError`. Exceptions
|
||||||
|
are part of the contract; Python just will not check them for you.
|
||||||
|
|
||||||
|
## I — Interface Segregation
|
||||||
|
|
||||||
|
Protocols make this cheap: define the narrow one you need at the call site.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class InvoiceReader(Protocol):
|
||||||
|
def get(self, invoice_id: InvoiceId) -> Invoice: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
A function that only reads should accept `InvoiceReader`, not the full `InvoiceStore`.
|
||||||
|
The test double then implements one method instead of eight, which is the practical
|
||||||
|
payoff and the reason this principle survives contact with real code.
|
||||||
|
|
||||||
|
## D — Dependency Inversion
|
||||||
|
|
||||||
|
Constructor injection, exactly as in Go. Two Python-specific hazards:
|
||||||
|
|
||||||
|
- **Import-time side effects are hidden dependencies.** A module that opens a database
|
||||||
|
connection or reads an env var at import has a dependency nothing can inject and no
|
||||||
|
test can replace. Configuration belongs in `pydantic-settings` objects passed in.
|
||||||
|
- **Default arguments are compile-time bindings.** `def send(client: Client = Client())`
|
||||||
|
constructs the default once, at definition, and welds it to every caller. Default to
|
||||||
|
`None` and construct inside, or require it.
|
||||||
|
|
||||||
|
## What does not carry over from the Go rewrite
|
||||||
|
|
||||||
|
- **"Return structs, accept interfaces"** — the returning half is about Go's zero values
|
||||||
|
and nil interfaces. In Python, returning a Protocol type is fine and often better.
|
||||||
|
- **Small interfaces because of embedding** — Python has no embedding; composition is
|
||||||
|
explicit delegation, which is more verbose but reads honestly.
|
||||||
|
- **Package-level responsibility as the unit of deployment** — Python packaging is much
|
||||||
|
looser. The module boundary is still the right unit for *reasoning*, but do not expect
|
||||||
|
it to be enforced.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `references/solid-principles.md` — the canonical, language-neutral reference
|
||||||
|
- `references/go-adaptation.md` — the Go rewrite; contrast is instructive
|
||||||
|
- `clean-code/references/python-adaptation.md` — style-level Python conventions
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: spec-driven-dev
|
name: spec-driven-dev
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Spec-driven development for PMs"
|
||||||
|
use_when: "Writing specs and requirements"
|
||||||
description: Write a structured specification before writing any code. Use when starting a new project, feature, or significant change. Adapted for a PM-first workflow where why comes before how.
|
description: Write a structured specification before writing any code. Use when starting a new project, feature, or significant change. Adapted for a PM-first workflow where why comes before how.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -197,6 +200,56 @@ Dev: [full command]
|
|||||||
[Anything unresolved that needs input before implementation begins]
|
[Anything unresolved that needs input before implementation begins]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## EARS-lite acceptance criteria
|
||||||
|
|
||||||
|
Measurable criteria answer "is it done?". They do not always answer "what should it do
|
||||||
|
when X happens?" — and behavioural requirements written as prose are where ambiguity
|
||||||
|
survives review. For any criterion that is conditional or event-driven, use EARS-lite
|
||||||
|
instead of prose:
|
||||||
|
|
||||||
|
```
|
||||||
|
WHEN <trigger> THE SYSTEM SHALL <observable response>
|
||||||
|
WHILE <state> THE SYSTEM SHALL <observable response>
|
||||||
|
IF <unwanted condition> THEN THE SYSTEM SHALL <observable response>
|
||||||
|
```
|
||||||
|
|
||||||
|
There is a fourth, unconditional form. Use it for invariants — the properties that must
|
||||||
|
hold with no trigger at all, which are usually the ones that matter most:
|
||||||
|
|
||||||
|
```
|
||||||
|
THE SYSTEM SHALL <observable response>
|
||||||
|
```
|
||||||
|
|
||||||
|
Worked, deliberately from a domain unlike whatever you are specifying:
|
||||||
|
|
||||||
|
```
|
||||||
|
THE SYSTEM SHALL reject a firmware image whose signature does not verify
|
||||||
|
WHEN a sensor reports a temperature above the shutdown limit
|
||||||
|
THE SYSTEM SHALL open the contactor within 200ms
|
||||||
|
WHILE the contactor is open
|
||||||
|
THE SYSTEM SHALL refuse every start command
|
||||||
|
IF a sensor reading is missing or stale by more than one interval
|
||||||
|
THEN THE SYSTEM SHALL treat it as above the limit
|
||||||
|
```
|
||||||
|
|
||||||
|
The example is off-domain on purpose. An on-domain example gets copied: a spec written
|
||||||
|
against an invoice-flavoured example inherited a requirement about extraction confidence
|
||||||
|
that its feature request never mentioned, and the author only noticed on review. Read
|
||||||
|
the shapes, not the content.
|
||||||
|
|
||||||
|
Each conditional line names a trigger and one observable response, which usually maps
|
||||||
|
onto a single test. The `WHILE` and unconditional forms are universally quantified —
|
||||||
|
"every start command", "a firmware image" — so they map onto a property or a set of
|
||||||
|
cases, not one assertion. That is still worth writing: it closes the requirements half
|
||||||
|
of the audit trail, which is otherwise issue-body prose.
|
||||||
|
|
||||||
|
**This is an option, not a replacement.** Threshold criteria ("p99 < 200ms under 100
|
||||||
|
RPS") stay as they are — EARS phrasing adds nothing to a number. Use it for conditional
|
||||||
|
behaviour, error handling, and state-dependent rules.
|
||||||
|
|
||||||
|
For the executor-facing handoff, `feature-spec` also carries a **Context snapshot**
|
||||||
|
section — inline the architecture facts the implementer cannot look up.
|
||||||
|
|
||||||
## Reframing Vague Requirements
|
## Reframing Vague Requirements
|
||||||
|
|
||||||
When you receive a vague requirement, translate it into specific success criteria before writing any spec content:
|
When you receive a vague requirement, translate it into specific success criteria before writing any spec content:
|
||||||
@@ -279,7 +332,7 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
|||||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||||
|
|
||||||
**At end of each phase:**
|
**At end of each phase:**
|
||||||
- `session_log` with `{skill: "spec-driven-dev", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "spec-driven-dev", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** specify, plan, tasks, implement
|
**Phases for this skill:** specify, plan, tasks, implement
|
||||||
|
|
||||||
@@ -288,7 +341,7 @@ treats `pass` as success, `fail` as failure, `skip` as neither.
|
|||||||
- `fail` — the phase's intended outcome was NOT reached (gate blocked, rework required).
|
- `fail` — the phase's intended outcome was NOT reached (gate blocked, rework required).
|
||||||
- `skip` — phase was skipped intentionally.
|
- `skip` — phase was skipped intentionally.
|
||||||
|
|
||||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future call to a local model. If your skill never logs, the routing pod sees no data.
|
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a given call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way.
|
||||||
|
|
||||||
## Cross-References
|
## Cross-References
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: stage-gate-review
|
name: stage-gate-review
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Stage-gate product governance (go/no-go council)"
|
||||||
|
use_when: "Preparing a Product Council ask; building a go/no-go checklist; gate review"
|
||||||
description: Run or prepare a stage-gate product governance review — the go/no-go checkpoint where a council approves an initiative to advance through fixed innovation stages (Frame → Concept → Prototype → Validate → Scale), each gate guarded by a per-discipline readiness checklist. Use when preparing a Product Council / steering-group ask, building a go/no-go checklist, designing an innovation governance process, or auditing whether an initiative is genuinely ready to advance vs faking the gate. Trigger phrases include "stage gate", "product council", "go/no-go", "advance to the next stage", "is this ready to progress", "gate review", "phase gate", "innovation governance".
|
description: Run or prepare a stage-gate product governance review — the go/no-go checkpoint where a council approves an initiative to advance through fixed innovation stages (Frame → Concept → Prototype → Validate → Scale), each gate guarded by a per-discipline readiness checklist. Use when preparing a Product Council / steering-group ask, building a go/no-go checklist, designing an innovation governance process, or auditing whether an initiative is genuinely ready to advance vs faking the gate. Trigger phrases include "stage gate", "product council", "go/no-go", "advance to the next stage", "is this ready to progress", "gate review", "phase gate", "innovation governance".
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,6 +74,9 @@ legal waived"), never silently dropped. Silent omission is the most common way a
|
|||||||
3. Mark every discipline: cleared / waived (with reason) / gap.
|
3. Mark every discipline: cleared / waived (with reason) / gap.
|
||||||
4. If any gap is on a discipline that gates the whole bet, **do not bring the ask** — close
|
4. If any gap is on a discipline that gates the whole bet, **do not bring the ask** — close
|
||||||
the gap or bring a feedback-only ask instead.
|
the gap or bring a feedback-only ask instead.
|
||||||
|
5. Load `grill-me` (Full Grill) on the ask before bringing it. A council's hard questions are
|
||||||
|
cheaper to answer in private first, and a gap found by grilling is a gap you close rather
|
||||||
|
than one the gate reports.
|
||||||
|
|
||||||
**Chairing a review (you are the council):**
|
**Chairing a review (you are the council):**
|
||||||
1. Ask for the evidence behind each "cleared", not the claim. Claims are theatre.
|
1. Ask for the evidence behind each "cleared", not the claim. Claims are theatre.
|
||||||
@@ -145,3 +151,12 @@ stays in the client boundary**:
|
|||||||
- Council/forum names, internal stage labels, internal tooling, named contacts, strategy
|
- Council/forum names, internal stage labels, internal tooling, named contacts, strategy
|
||||||
specifics, deal terms → client dir/org only, local models only, never the public brain.
|
specifics, deal terms → client dir/org only, local models only, never the public brain.
|
||||||
- This skill carries the **pattern**; the client carries the **wrapper**. Never merge them.
|
- This skill carries the **pattern**; the client carries the **wrapper**. Never merge them.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Advisory. Prepares or chairs a gate; the council decides, not this skill.
|
||||||
|
|
||||||
|
Stop and hand back when: a discipline's evidence is missing rather than weak. A gap on a
|
||||||
|
gating discipline means do not bring the ask — bring a feedback-only ask instead. Marking
|
||||||
|
it "cleared with a note" is precisely the gate theatre the anti-patterns section is
|
||||||
|
about.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: brainstorming
|
name: brainstorming
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Structured divergent brainstorming"
|
||||||
|
use_when: "Generating or expanding a set of ideas"
|
||||||
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
|
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
|
||||||
---
|
---
|
||||||
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ Use this template when dispatching a spec document reviewer subagent.
|
|||||||
**Dispatch after:** Spec document is written to docs/superpowers/specs/
|
**Dispatch after:** Spec document is written to docs/superpowers/specs/
|
||||||
|
|
||||||
```
|
```
|
||||||
Task tool (general-purpose):
|
Agent tool (`subagent_type: "general-purpose"`):
|
||||||
description: "Review spec document"
|
description: "Review spec document"
|
||||||
prompt: |
|
prompt: |
|
||||||
You are a spec document reviewer. Verify this spec is complete and ready for planning.
|
You are a spec document reviewer. Verify this spec is complete and ready for planning.
|
||||||
+26
-6
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: dispatching-parallel-agents
|
name: dispatching-parallel-agents
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Fan work out across parallel subagents"
|
||||||
|
use_when: "Decomposing work for concurrent agents"
|
||||||
description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
|
description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -65,13 +68,30 @@ Each agent gets:
|
|||||||
|
|
||||||
### 3. Dispatch in Parallel
|
### 3. Dispatch in Parallel
|
||||||
|
|
||||||
```typescript
|
Issue every dispatch in **one** message — several `Agent` tool uses in a single
|
||||||
// In Claude Code / AI environment
|
assistant turn run concurrently; one per turn runs them in series.
|
||||||
Task("Fix agent-tool-abort.test.ts failures")
|
|
||||||
Task("Fix batch-completion-behavior.test.ts failures")
|
|
||||||
Task("Fix tool-approval-race-conditions.test.ts failures")
|
|
||||||
// All three run concurrently
|
|
||||||
```
|
```
|
||||||
|
Agent(description: "fix abort tests", subagent_type: "general-purpose", prompt: ...)
|
||||||
|
Agent(description: "fix batch tests", subagent_type: "general-purpose", prompt: ...)
|
||||||
|
Agent(description: "fix approval tests", subagent_type: "general-purpose", prompt: ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Options that matter for this pattern:
|
||||||
|
|
||||||
|
- `run_in_background: true` (the default) returns immediately and notifies on
|
||||||
|
completion. Set it to `false` only when your very next action depends on the result.
|
||||||
|
- `isolation: "worktree"` gives an agent its own git worktree. Use it whenever two
|
||||||
|
agents might touch the same files — parallel writers to one tree is the failure this
|
||||||
|
skill's independence rule exists to avoid, and a worktree makes it structural rather
|
||||||
|
than a matter of discipline.
|
||||||
|
- `model` overrides the model per agent; cheap mechanical fan-out does not need the
|
||||||
|
same model as the reasoning that planned it.
|
||||||
|
|
||||||
|
For a fan-out with real structure — phases, a verify pass per finding, fan-in — the
|
||||||
|
`Workflow` tool expresses it directly with `agent()`, `parallel()` and `pipeline()`.
|
||||||
|
It spawns many agents and costs accordingly, so it runs **only on explicit opt-in from
|
||||||
|
the operator**; do not reach for it just because a task looks parallel.
|
||||||
|
|
||||||
### 4. Review and Integrate
|
### 4. Review and Integrate
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: executing-plans
|
name: executing-plans
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Executing a written plan step by step"
|
||||||
|
use_when: "Running an approved plan to completion"
|
||||||
description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
|
description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,14 +14,16 @@ Load plan, review critically, execute all tasks, report when complete.
|
|||||||
|
|
||||||
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
|
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
|
||||||
|
|
||||||
**Note:** Tell your human partner that Superpowers works much better with access to subagents. The quality of its work will be significantly higher if run on a platform with subagent support (such as Claude Code or Codex). If subagents are available, use superpowers:subagent-driven-development instead of this skill.
|
**Note:** This skill is the fallback for harnesses with no subagents. Claude Code has
|
||||||
|
them — the `Agent` tool — so on this fleet prefer `superpowers-subagent-driven-development`
|
||||||
|
and use this skill only where `Agent` is genuinely unavailable (Crush, Pi, OpenCode).
|
||||||
|
|
||||||
## The Process
|
## The Process
|
||||||
|
|
||||||
### Step 1: Load and Review Plan
|
### Step 1: Load and Review Plan
|
||||||
1. Read plan file
|
1. Read plan file
|
||||||
2. Review critically - identify any questions or concerns about the plan
|
2. Review critically - identify any questions or concerns about the plan
|
||||||
3. If concerns: Raise them with your human partner before starting
|
3. If concerns: Raise them with the operator before starting
|
||||||
4. If no concerns: Create TodoWrite and proceed
|
4. If no concerns: Create TodoWrite and proceed
|
||||||
|
|
||||||
### Step 2: Execute Tasks
|
### Step 2: Execute Tasks
|
||||||
+3
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: finishing-a-development-branch
|
name: finishing-a-development-branch
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Cleanly finishing/landing a dev branch"
|
||||||
|
use_when: "Wrapping a branch → merge/PR"
|
||||||
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
||||||
---
|
---
|
||||||
|
|
||||||
+15
-10
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: receiving-code-review
|
name: receiving-code-review
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Acting on received review feedback"
|
||||||
|
use_when: "Responding to review comments"
|
||||||
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,6 +14,8 @@ Code review requires technical evaluation, not emotional performance.
|
|||||||
|
|
||||||
**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
|
**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
|
||||||
|
|
||||||
|
This skill starts where `superpowers-requesting-code-review` ends. The substance of what a reviewer should have checked is `code-review`.
|
||||||
|
|
||||||
## The Response Pattern
|
## The Response Pattern
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -27,7 +32,7 @@ WHEN receiving code review feedback:
|
|||||||
## Forbidden Responses
|
## Forbidden Responses
|
||||||
|
|
||||||
**NEVER:**
|
**NEVER:**
|
||||||
- "You're absolutely right!" (explicit CLAUDE.md violation)
|
- "You're absolutely right!" (performative agreement, not evaluation)
|
||||||
- "Great point!" / "Excellent feedback!" (performative)
|
- "Great point!" / "Excellent feedback!" (performative)
|
||||||
- "Let me implement that now" (before verification)
|
- "Let me implement that now" (before verification)
|
||||||
|
|
||||||
@@ -49,7 +54,7 @@ WHY: Items may be related. Partial understanding = wrong implementation.
|
|||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```
|
```
|
||||||
your human partner: "Fix 1-6"
|
the operator: "Fix 1-6"
|
||||||
You understand 1,2,3,6. Unclear on 4,5.
|
You understand 1,2,3,6. Unclear on 4,5.
|
||||||
|
|
||||||
❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
|
❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
|
||||||
@@ -58,7 +63,7 @@ You understand 1,2,3,6. Unclear on 4,5.
|
|||||||
|
|
||||||
## Source-Specific Handling
|
## Source-Specific Handling
|
||||||
|
|
||||||
### From your human partner
|
### From the operator
|
||||||
- **Trusted** - implement after understanding
|
- **Trusted** - implement after understanding
|
||||||
- **Still ask** if scope unclear
|
- **Still ask** if scope unclear
|
||||||
- **No performative agreement**
|
- **No performative agreement**
|
||||||
@@ -79,11 +84,11 @@ IF suggestion seems wrong:
|
|||||||
IF can't easily verify:
|
IF can't easily verify:
|
||||||
Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
|
Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
|
||||||
|
|
||||||
IF conflicts with your human partner's prior decisions:
|
IF conflicts with the operator's prior decisions:
|
||||||
Stop and discuss with your human partner first
|
Stop and discuss with the operator first
|
||||||
```
|
```
|
||||||
|
|
||||||
**your human partner's rule:** "External feedback - be skeptical, but check carefully"
|
**the operator's rule:** "External feedback - be skeptical, but check carefully"
|
||||||
|
|
||||||
## YAGNI Check for "Professional" Features
|
## YAGNI Check for "Professional" Features
|
||||||
|
|
||||||
@@ -95,7 +100,7 @@ IF reviewer suggests "implementing properly":
|
|||||||
IF used: Then implement properly
|
IF used: Then implement properly
|
||||||
```
|
```
|
||||||
|
|
||||||
**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
|
**the operator's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
|
||||||
|
|
||||||
## Implementation Order
|
## Implementation Order
|
||||||
|
|
||||||
@@ -118,13 +123,13 @@ Push back when:
|
|||||||
- Violates YAGNI (unused feature)
|
- Violates YAGNI (unused feature)
|
||||||
- Technically incorrect for this stack
|
- Technically incorrect for this stack
|
||||||
- Legacy/compatibility reasons exist
|
- Legacy/compatibility reasons exist
|
||||||
- Conflicts with your human partner's architectural decisions
|
- Conflicts with the operator's architectural decisions
|
||||||
|
|
||||||
**How to push back:**
|
**How to push back:**
|
||||||
- Use technical reasoning, not defensiveness
|
- Use technical reasoning, not defensiveness
|
||||||
- Ask specific questions
|
- Ask specific questions
|
||||||
- Reference working tests/code
|
- Reference working tests/code
|
||||||
- Involve your human partner if architectural
|
- Involve the operator if architectural
|
||||||
|
|
||||||
**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"
|
**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"
|
||||||
|
|
||||||
@@ -195,7 +200,7 @@ Reviewer: "Implement proper metrics tracking with database, date filters, CSV ex
|
|||||||
|
|
||||||
**Unclear Item (Good):**
|
**Unclear Item (Good):**
|
||||||
```
|
```
|
||||||
your human partner: "Fix items 1-6"
|
the operator: "Fix items 1-6"
|
||||||
You understand 1,2,3,6. Unclear on 4,5.
|
You understand 1,2,3,6. Unclear on 4,5.
|
||||||
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
|
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
|
||||||
```
|
```
|
||||||
+8
-1
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: requesting-code-review
|
name: requesting-code-review
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Requesting a code review well"
|
||||||
|
use_when: "Asking another agent/human to review"
|
||||||
description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
|
description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -9,6 +12,8 @@ Dispatch a code reviewer subagent to catch issues before they cascade. The revie
|
|||||||
|
|
||||||
**Core principle:** Review early, review often.
|
**Core principle:** Review early, review often.
|
||||||
|
|
||||||
|
This skill is the *commissioning* half. What the reviewer should look for is `code-review`; how to handle what comes back is `superpowers-receiving-code-review`.
|
||||||
|
|
||||||
## When to Request Review
|
## When to Request Review
|
||||||
|
|
||||||
**Mandatory:**
|
**Mandatory:**
|
||||||
@@ -31,7 +36,9 @@ HEAD_SHA=$(git rev-parse HEAD)
|
|||||||
|
|
||||||
**2. Dispatch code reviewer subagent:**
|
**2. Dispatch code reviewer subagent:**
|
||||||
|
|
||||||
Use Task tool with `general-purpose` type, fill template at `code-reviewer.md`
|
Use the `Agent` tool with `subagent_type: "general-purpose"`, filling the template at
|
||||||
|
`code-reviewer.md`. A reviewer needs no write access and benefits from none — if the
|
||||||
|
harness offers a read-only agent type, prefer it.
|
||||||
|
|
||||||
**Placeholders:**
|
**Placeholders:**
|
||||||
- `{DESCRIPTION}` - Brief summary of what you built
|
- `{DESCRIPTION}` - Brief summary of what you built
|
||||||
+1
-1
@@ -5,7 +5,7 @@ Use this template when dispatching a code reviewer subagent.
|
|||||||
**Purpose:** Review completed work against requirements and code quality standards before it cascades into more work.
|
**Purpose:** Review completed work against requirements and code quality standards before it cascades into more work.
|
||||||
|
|
||||||
```
|
```
|
||||||
Task tool (general-purpose):
|
Agent tool (`subagent_type: "general-purpose"`):
|
||||||
description: "Review code changes"
|
description: "Review code changes"
|
||||||
prompt: |
|
prompt: |
|
||||||
You are a Senior Code Reviewer with expertise in software architecture,
|
You are a Senior Code Reviewer with expertise in software architecture,
|
||||||
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: subagent-driven-development
|
name: subagent-driven-development
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Subagent-driven development workflow"
|
||||||
|
use_when: "Building via orchestrated subagents"
|
||||||
description: Use when executing implementation plans with independent tasks in the current session
|
description: Use when executing implementation plans with independent tasks in the current session
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +14,7 @@ Execute plan by dispatching fresh subagent per task, with two-stage review after
|
|||||||
|
|
||||||
**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration
|
**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration
|
||||||
|
|
||||||
**Continuous execution:** Do not pause to check in with your human partner between tasks. Execute all tasks from the plan without stopping. The only reasons to stop are: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. "Should I continue?" prompts and progress summaries waste their time — they asked you to execute the plan, so execute it.
|
**Continuous execution:** Do not pause to check in with the operator between tasks. Execute all tasks from the plan without stopping. The only reasons to stop are: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. "Should I continue?" prompts and progress summaries waste their time — they asked you to execute the plan, so execute it.
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ Use this template when dispatching a code quality reviewer subagent.
|
|||||||
**Only dispatch after spec compliance review passes.**
|
**Only dispatch after spec compliance review passes.**
|
||||||
|
|
||||||
```
|
```
|
||||||
Task tool (general-purpose):
|
Agent tool (`subagent_type: "general-purpose"`):
|
||||||
Use template at requesting-code-review/code-reviewer.md
|
Use template at requesting-code-review/code-reviewer.md
|
||||||
|
|
||||||
DESCRIPTION: [task summary, from implementer's report]
|
DESCRIPTION: [task summary, from implementer's report]
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
Use this template when dispatching an implementer subagent.
|
Use this template when dispatching an implementer subagent.
|
||||||
|
|
||||||
```
|
```
|
||||||
Task tool (general-purpose):
|
Agent tool (`subagent_type: "general-purpose"`):
|
||||||
description: "Implement Task N: [task name]"
|
description: "Implement Task N: [task name]"
|
||||||
prompt: |
|
prompt: |
|
||||||
You are implementing Task N: [task name]
|
You are implementing Task N: [task name]
|
||||||
+1
-1
@@ -5,7 +5,7 @@ Use this template when dispatching a spec compliance reviewer subagent.
|
|||||||
**Purpose:** Verify implementer built what was requested (nothing more, nothing less)
|
**Purpose:** Verify implementer built what was requested (nothing more, nothing less)
|
||||||
|
|
||||||
```
|
```
|
||||||
Task tool (general-purpose):
|
Agent tool (`subagent_type: "general-purpose"`):
|
||||||
description: "Review spec compliance for Task N"
|
description: "Review spec compliance for Task N"
|
||||||
prompt: |
|
prompt: |
|
||||||
You are reviewing whether an implementation matches its specification.
|
You are reviewing whether an implementation matches its specification.
|
||||||
+3
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: using-git-worktrees
|
name: using-git-worktrees
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Isolating work with git worktrees"
|
||||||
|
use_when: "Parallel/isolated work in a worktree"
|
||||||
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: using-superpowers
|
name: using-superpowers
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Meta: orienting in the superpowers set"
|
||||||
|
use_when: "Learning how to use the superpowers skills"
|
||||||
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
|
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
|
||||||
---
|
---
|
||||||
|
|
||||||
+5
-5
@@ -24,11 +24,11 @@ When a skill says to dispatch a named agent type, use `@generalist` with the ful
|
|||||||
|
|
||||||
| Skill instruction | Gemini CLI equivalent |
|
| Skill instruction | Gemini CLI equivalent |
|
||||||
|-------------------|----------------------|
|
|-------------------|----------------------|
|
||||||
| `Task tool (superpowers:implementer)` | `@generalist` with the filled `implementer-prompt.md` template |
|
| `Agent tool (superpowers:implementer)` | `@generalist` with the filled `implementer-prompt.md` template |
|
||||||
| `Task tool (superpowers:spec-reviewer)` | `@generalist` with the filled `spec-reviewer-prompt.md` template |
|
| `Agent tool (superpowers:spec-reviewer)` | `@generalist` with the filled `spec-reviewer-prompt.md` template |
|
||||||
| `Task tool (superpowers:code-reviewer)` | `@code-reviewer` (bundled agent) or `@generalist` with the filled review prompt |
|
| `Agent tool (superpowers:code-reviewer)` | `@code-reviewer` (bundled agent) or `@generalist` with the filled review prompt |
|
||||||
| `Task tool (superpowers:code-quality-reviewer)` | `@generalist` with the filled `code-quality-reviewer-prompt.md` template |
|
| `Agent tool (superpowers:code-quality-reviewer)` | `@generalist` with the filled `code-quality-reviewer-prompt.md` template |
|
||||||
| `Task tool (general-purpose)` with inline prompt | `@generalist` with your inline prompt |
|
| `Agent tool (general-purpose)` with inline prompt | `@generalist` with your inline prompt |
|
||||||
|
|
||||||
### Prompt filling
|
### Prompt filling
|
||||||
|
|
||||||
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: verification-before-completion
|
name: verification-before-completion
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Verify behavior before claiming done"
|
||||||
|
use_when: "Before marking any task complete"
|
||||||
description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
|
description: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ Skip any step = lying, not verifying
|
|||||||
## Why This Matters
|
## Why This Matters
|
||||||
|
|
||||||
From 24 failure memories:
|
From 24 failure memories:
|
||||||
- your human partner said "I don't believe you" - trust broken
|
- the operator said "I don't believe you" - trust broken
|
||||||
- Undefined functions shipped - would crash
|
- Undefined functions shipped - would crash
|
||||||
- Missing requirements shipped - incomplete features
|
- Missing requirements shipped - incomplete features
|
||||||
- Time wasted on false completion → redirect → rework
|
- Time wasted on false completion → redirect → rework
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: writing-plans
|
name: writing-plans
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Writing implementation plans"
|
||||||
|
use_when: "Planning substantial work before coding"
|
||||||
description: Use when you have a spec or requirements for a multi-step task, before touching code
|
description: Use when you have a spec or requirements for a multi-step task, before touching code
|
||||||
---
|
---
|
||||||
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ Use this template when dispatching a plan document reviewer subagent.
|
|||||||
**Dispatch after:** The complete plan is written.
|
**Dispatch after:** The complete plan is written.
|
||||||
|
|
||||||
```
|
```
|
||||||
Task tool (general-purpose):
|
Agent tool (`subagent_type: "general-purpose"`):
|
||||||
description: "Review plan document"
|
description: "Review plan document"
|
||||||
prompt: |
|
prompt: |
|
||||||
You are a plan document reviewer. Verify this plan is complete and ready for implementation.
|
You are a plan document reviewer. Verify this plan is complete and ready for implementation.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: writing-skills
|
name: writing-skills
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Authoring new SKILL.md skills"
|
||||||
|
use_when: "Creating a new reusable skill"
|
||||||
description: Use when creating new skills, editing existing skills, or verifying skills work before deployment
|
description: Use when creating new skills, editing existing skills, or verifying skills work before deployment
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -243,7 +246,7 @@ Always use subagents (50-100x context savings). REQUIRED: Use [other-skill-name]
|
|||||||
**Compress examples:**
|
**Compress examples:**
|
||||||
```markdown
|
```markdown
|
||||||
# ❌ BAD: Verbose example (42 words)
|
# ❌ BAD: Verbose example (42 words)
|
||||||
your human partner: "How did we handle authentication errors in React Router before?"
|
the operator: "How did we handle authentication errors in React Router before?"
|
||||||
You: I'll search past conversations for React Router authentication patterns.
|
You: I'll search past conversations for React Router authentication patterns.
|
||||||
[Dispatch subagent with search query: "React Router authentication error handling 401"]
|
[Dispatch subagent with search query: "React Router authentication error handling 401"]
|
||||||
|
|
||||||
@@ -315,7 +318,7 @@ digraph when_flowchart {
|
|||||||
|
|
||||||
See @graphviz-conventions.dot for graphviz style rules.
|
See @graphviz-conventions.dot for graphviz style rules.
|
||||||
|
|
||||||
**Visualizing for your human partner:** Use `render-graphs.js` in this directory to render a skill's flowcharts to SVG:
|
**Visualizing for the operator:** Use `render-graphs.js` in this directory to render a skill's flowcharts to SVG:
|
||||||
```bash
|
```bash
|
||||||
./render-graphs.js ../some-skill # Each diagram separately
|
./render-graphs.js ../some-skill # Each diagram separately
|
||||||
./render-graphs.js ../some-skill --combine # All diagrams in one SVG
|
./render-graphs.js ../some-skill --combine # All diagrams in one SVG
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: tdd
|
name: tdd
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Test-Driven Development enforcement"
|
||||||
|
use_when: "Starting any new feature or fixing a bug"
|
||||||
description: Write failing tests first, then minimal code to pass. Non-negotiable for all new features and bug fixes.
|
description: Write failing tests first, then minimal code to pass. Non-negotiable for all new features and bug fixes.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -97,13 +100,21 @@ go test -run TestRetryOperation_RetriesThreeTimes ./...
|
|||||||
```
|
```
|
||||||
|
|
||||||
Confirm:
|
Confirm:
|
||||||
- Test fails (not compilation errors)
|
- Test fails, and fails because the feature is missing
|
||||||
- Failure message is expected
|
- The failure message is the one you expected
|
||||||
- Fails because feature is missing (not typos)
|
- It is not failing on a typo in the test itself
|
||||||
|
|
||||||
**Test passes?** You're testing existing behavior. Fix test.
|
**Test passes?** You're testing existing behavior. Fix test.
|
||||||
|
|
||||||
**Compilation errors?** Fix errors, re-run until it fails correctly.
|
**Failing because the thing does not exist yet?** That is a legitimate red — a Go
|
||||||
|
compile error for an undefined function, a Python `ImportError` for a module you have
|
||||||
|
not written, a TypeScript "cannot find module". Read the message, confirm it names the
|
||||||
|
unit you are about to build, and proceed to GREEN. **Do not write a stub to make the
|
||||||
|
runner happier.** A stub is production code written before a failing test, which is the
|
||||||
|
Iron Law with extra steps.
|
||||||
|
|
||||||
|
**Failing on a typo or a broken import elsewhere?** Different thing. Fix it and re-run
|
||||||
|
until the failure is the one above.
|
||||||
|
|
||||||
### GREEN - Minimal Code
|
### GREEN - Minimal Code
|
||||||
|
|
||||||
@@ -163,6 +174,23 @@ Keep tests green. Don't add behavior.
|
|||||||
|
|
||||||
Next failing test for next behavior.
|
Next failing test for next behavior.
|
||||||
|
|
||||||
|
## Other languages
|
||||||
|
|
||||||
|
The discipline above is language-neutral; the examples below it are Go, because Go is
|
||||||
|
this fleet's default stack and one good example beats five mediocre ones.
|
||||||
|
|
||||||
|
For anything else, the per-language conventions live in reference files — load the one
|
||||||
|
you need rather than translating Go idioms by eye. Translation is where the mistakes
|
||||||
|
happen: the Go table-driven exemplar below branches on a `wantErr` column inside the
|
||||||
|
loop, which is idiomatic Go and an anti-pattern in pytest, where it produces a
|
||||||
|
conditional assertion and folds two behaviours into one test.
|
||||||
|
|
||||||
|
- `references/python.md` — pytest, `@pytest.mark.parametrize`, `mypy --strict` in the
|
||||||
|
RED gate, `pytest.raises(..., match=)`, fixtures vs helpers
|
||||||
|
- `references/typescript.md` — vitest/jest, `it.each`, `expect(...).rejects`
|
||||||
|
- `references/rust.md` — `#[cfg(test)]` modules, table-driven via arrays, `#[should_panic]`
|
||||||
|
- `references/java.md` — JUnit 5, `@ParameterizedTest`, `assertThrows`
|
||||||
|
|
||||||
## Go-Specific TDD Notes
|
## Go-Specific TDD Notes
|
||||||
|
|
||||||
### Table-Driven Tests (Preferred Pattern)
|
### Table-Driven Tests (Preferred Pattern)
|
||||||
@@ -326,12 +354,18 @@ Before marking work complete:
|
|||||||
- [ ] Watched each test fail before implementing
|
- [ ] Watched each test fail before implementing
|
||||||
- [ ] Each test failed for expected reason (feature missing, not typo)
|
- [ ] Each test failed for expected reason (feature missing, not typo)
|
||||||
- [ ] Wrote minimal code to pass each test
|
- [ ] Wrote minimal code to pass each test
|
||||||
- [ ] All tests pass: `go test ./...`
|
- [ ] All tests pass
|
||||||
- [ ] Race detector clean: `go test -race ./...`
|
- [ ] The language's static gate is clean — `go vet`, `mypy --strict`, `tsc --noEmit`,
|
||||||
|
`cargo clippy`. A red type-check is a red build; it does not wait for GREEN.
|
||||||
|
- [ ] The language's concurrency gate is clean **where one exists and applies** —
|
||||||
|
`go test -race`, `cargo test` under Miri for unsafe code. There is no equivalent
|
||||||
|
for a pure Python function, and a checklist box that can never be ticked teaches
|
||||||
|
people to skip checklists. Tick it or strike it deliberately; do not leave it
|
||||||
|
hanging.
|
||||||
- [ ] Tests use real code (mocks only if crossing a system boundary)
|
- [ ] Tests use real code (mocks only if crossing a system boundary)
|
||||||
- [ ] Edge cases and errors covered
|
- [ ] Edge cases and errors covered
|
||||||
|
|
||||||
Can't check all boxes? You skipped TDD. Start over.
|
Can't check **or deliberately strike** all boxes? You skipped TDD. Start over. A box struck with a stated reason is answered; a box left hanging is not.
|
||||||
|
|
||||||
## When Stuck
|
## When Stuck
|
||||||
|
|
||||||
@@ -340,7 +374,7 @@ Can't check all boxes? You skipped TDD. Start over.
|
|||||||
| Don't know how to test | Write wished-for API. Write assertion first. Ask Mathias. |
|
| Don't know how to test | Write wished-for API. Write assertion first. Ask Mathias. |
|
||||||
| Test too complicated | Design too complicated. Simplify interface. |
|
| Test too complicated | Design too complicated. Simplify interface. |
|
||||||
| Must mock everything | Code too coupled. Use dependency injection. |
|
| Must mock everything | Code too coupled. Use dependency injection. |
|
||||||
| Test setup huge | Extract helpers with `t.Helper()`. Still complex? Simplify design. |
|
| Test setup huge | Extract helpers (`t.Helper()` in Go, a local `__tracebackhide__ = True` in Python). Still complex? Simplify design. |
|
||||||
|
|
||||||
## Debugging Integration
|
## Debugging Integration
|
||||||
|
|
||||||
@@ -372,20 +406,20 @@ Pass-rate is computed downstream by the `/pass-rate` HTTP endpoint, which
|
|||||||
treats `pass` as success, `fail` as failure, `skip` as neither.
|
treats `pass` as success, `fail` as failure, `skip` as neither.
|
||||||
|
|
||||||
**At end of `red` phase:**
|
**At end of `red` phase:**
|
||||||
- `session_log` with `{skill: "tdd", phase: "red", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "tdd", phase: "red", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**At end of `green` phase:**
|
**At end of `green` phase:**
|
||||||
- `session_log` with `{skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "tdd", phase: "green", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**At end of `refactor` phase:**
|
**At end of `refactor` phase:**
|
||||||
- `session_log` with `{skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root: "<absolute path>"}`
|
- `session_log` with `{session_id: "<this session's id>", skill: "tdd", phase: "refactor", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Status semantics:**
|
**Status semantics:**
|
||||||
- `pass` — the phase's intended outcome was reached (red: test fails as expected; green: test passes; refactor: tests still pass after refactor).
|
- `pass` — the phase's intended outcome was reached (red: test fails as expected; green: test passes; refactor: tests still pass after refactor).
|
||||||
- `fail` — the phase's intended outcome was NOT reached (test compiled when it shouldn't, test still fails after green attempt, refactor broke tests).
|
- `fail` — the phase's intended outcome was NOT reached (test compiled when it shouldn't, test still fails after green attempt, refactor broke tests).
|
||||||
- `skip` — phase was skipped intentionally (e.g. refactor not warranted).
|
- `skip` — phase was skipped intentionally (e.g. refactor not warranted).
|
||||||
|
|
||||||
**Why this matters:** the routing pod (Plan 6) reads pass-rate to decide whether to route a future `tdd` call to a local model. If your skill never logs, the routing pod sees no data and may default-route or default-not-route in a way that doesn't reflect real performance.
|
**Why this matters:** pass-rate is what a future routing decision would be based on — whether a `tdd` call can be served by a local model instead of a cloud one. A skill that never logs contributes no evidence either way, and a decision made on no evidence is a coin flip wearing a metric.
|
||||||
|
|
||||||
## Final Rule
|
## Final Rule
|
||||||
|
|
||||||
@@ -395,7 +429,3 @@ Otherwise → not TDD
|
|||||||
```
|
```
|
||||||
|
|
||||||
No exceptions without Mathias's permission.
|
No exceptions without Mathias's permission.
|
||||||
|
|
||||||
## Mode 2 Routing Note
|
|
||||||
|
|
||||||
This skill is invoked identically whether the agent is running in Mode 1 (cloud Claude, no routing) or Mode 2 (client-local, supervisor routing layer). The routing pod (Plan 6) does not exist yet; until it does, treat this skill as Mode 1 only. The discipline does not change between modes — only the model behind the call.
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# TDD in Java
|
||||||
|
|
||||||
|
The Iron Law and Red-Green-Refactor in `SKILL.md` apply unchanged. This file replaces
|
||||||
|
the Go-Specific TDD Notes for Java. JUnit 5 (Jupiter) throughout.
|
||||||
|
|
||||||
|
## The first red is a compile error, and that is correct
|
||||||
|
|
||||||
|
`cannot find symbol: method splitVat` is a legitimate red. Do not stub the method with
|
||||||
|
`throw new UnsupportedOperationException()` to make the module compile — that is
|
||||||
|
production code before a failing test.
|
||||||
|
|
||||||
|
## Table-driven: `@ParameterizedTest`
|
||||||
|
|
||||||
|
```java
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@MethodSource("vatCases")
|
||||||
|
void roundsHalfToEven(String name, long amountMinor, BigDecimal rate, long net, long vat) {
|
||||||
|
assertEquals(new Split(net, vat), VatSplitter.split(amountMinor, rate));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> vatCases() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("tie rounds down to even", 14L, new BigDecimal("0.12"), 12L, 2L),
|
||||||
|
Arguments.of("tie rounds up to even", 42L, new BigDecimal("0.12"), 38L, 4L),
|
||||||
|
Arguments.of("exact division", 12500L, new BigDecimal("0.25"), 10000L, 2500L)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`name = "{0}"` puts the case label in the report — the `t.Run` analogue. `@CsvSource` is
|
||||||
|
terser for primitives; `@MethodSource` is the one to use as soon as a case carries a
|
||||||
|
`BigDecimal` or a domain object.
|
||||||
|
|
||||||
|
## Error cases get their own test
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void rejectsNegativeAmount() {
|
||||||
|
var e = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> VatSplitter.split(-1, new BigDecimal("0.25")));
|
||||||
|
assertTrue(e.getMessage().contains("non-negative"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Assert on the message, not only the type — `IllegalArgumentException` is thrown by half
|
||||||
|
the JDK, and a test that accepts any of them will keep passing for the wrong reason.
|
||||||
|
|
||||||
|
## Money is `BigDecimal` or `long` minor units, never `double`
|
||||||
|
|
||||||
|
`0.1 + 0.2 != 0.3` is not a rounding curiosity in a payments system, it is a defect.
|
||||||
|
Where a rounding mode matters, state it: `setScale(0, RoundingMode.HALF_EVEN)`. Banker's
|
||||||
|
rounding is the default expectation in Swedish accounting and is *not* Java's default.
|
||||||
|
|
||||||
|
## Assertions
|
||||||
|
|
||||||
|
Prefer AssertJ (`assertThat(x).isEqualTo(y)`) where the project already has it: the
|
||||||
|
failure messages name the actual value. Where it does not, plain JUnit assertions are
|
||||||
|
fine — do not add a dependency for aesthetics.
|
||||||
|
|
||||||
|
`assertAll` is the closest analogue to Go's soft `assert`: it reports every failure in
|
||||||
|
the group instead of stopping at the first.
|
||||||
|
|
||||||
|
## Mocks
|
||||||
|
|
||||||
|
Mockito is standard, and easy to overuse. Per `SKILL.md`, mock only across a system
|
||||||
|
boundary. A mocked class you own tests the mock; a `@SpringBootTest` that mocks the
|
||||||
|
repository tests neither the wiring nor the query.
|
||||||
|
|
||||||
|
## Gates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test # the failing test
|
||||||
|
./gradlew check # static gate: compiler warnings, spotless, errorprone
|
||||||
|
```
|
||||||
|
|
||||||
|
Java has no `-race`. For concurrent code the honest substitutes are a stress test with
|
||||||
|
`CountDownLatch` to force the interleaving, and jcstress for lock-free work. A pure
|
||||||
|
function should strike that checklist box deliberately.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user