Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skills",
|
"name": "skills",
|
||||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||||
"version": "0.8.3",
|
"version": "0.19.3",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/mathias"
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
|||||||
+39
-14
@@ -94,26 +94,51 @@ jobs:
|
|||||||
git config user.name "${GITEA_ACTOR}"
|
git config user.name "${GITEA_ACTOR}"
|
||||||
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
|
git config user.email "${GITEA_ACTOR}@noreply.gitea.d-ma.be"
|
||||||
|
|
||||||
# Claude Code gates plugin updates on plugin.json's version field:
|
# The tag goes up FIRST and on its own. It names the commit this run
|
||||||
# if it does not move, installed hosts never see the new tag. Bump
|
# checked out and can never be rejected, however far main has moved.
|
||||||
# it in the same commit train so the tag and the manifest agree —
|
git tag -a "$TAG" -m "release $TAG"
|
||||||
# `task check` enforces that equality on every run.
|
git push origin "$TAG"
|
||||||
|
echo "pushed $TAG"
|
||||||
|
|
||||||
|
# The manifest bump is a separate concern and must land on whatever
|
||||||
|
# main is NOW. The runner is maxParallel=1 and shared with other
|
||||||
|
# repos, so a job routinely starts several commits after its own —
|
||||||
|
# pushing this run's stale HEAD to main is then rejected
|
||||||
|
# non-fast-forward, which is how v0.10.0 ended up tagged with the
|
||||||
|
# manifest still reading 0.9.0.
|
||||||
|
#
|
||||||
|
# Claude Code gates plugin updates on this field: if it does not
|
||||||
|
# move, installed hosts never see the release. `task check` asserts
|
||||||
|
# the manifest is not behind the latest tag, so a failure here is a
|
||||||
|
# red main, not a cosmetic miss.
|
||||||
manifest=.claude-plugin/plugin.json
|
manifest=.claude-plugin/plugin.json
|
||||||
version="${TAG#v}"
|
version="${TAG#v}"
|
||||||
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
|
||||||
git tag -a "$TAG" -m "release $TAG"
|
|
||||||
|
|
||||||
if [ "$current" != "$version" ]; then
|
for attempt in 1 2 3; do
|
||||||
|
git fetch --quiet origin main
|
||||||
|
git checkout --quiet -B release-bump origin/main
|
||||||
|
|
||||||
|
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
||||||
|
if [ "$current" = "$version" ]; then
|
||||||
|
echo "manifest already at $version"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# A later run may have bumped past us while this one waited.
|
||||||
|
if [ "$(sh bin/newer-version "$current" "$version")" = "$current" ]; then
|
||||||
|
echo "manifest is $current, already newer than $version — leaving it"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
|
sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$current\"/\"version\": \"$version\"/" "$manifest"
|
||||||
git add "$manifest"
|
git add "$manifest"
|
||||||
# The trailer below stops this push from re-triggering the tagger.
|
# The trailer below stops this push from re-triggering the tagger.
|
||||||
git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true"
|
git commit -m "chore(release): plugin.json version $version" -m "Skip-Release: true"
|
||||||
# Commit and tag go up in ONE push: the gate job runs `task check`
|
if git push origin HEAD:main; then
|
||||||
# on the bump commit, and that asserts manifest == latest tag. Two
|
|
||||||
# separate pushes leave a window where it legitimately fails.
|
|
||||||
git push origin HEAD:main "$TAG"
|
|
||||||
echo "bumped $manifest to $version"
|
echo "bumped $manifest to $version"
|
||||||
else
|
exit 0
|
||||||
git push origin "$TAG"
|
|
||||||
fi
|
fi
|
||||||
echo "pushed $TAG"
|
echo "main moved while bumping — retry $attempt"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "could not land the manifest bump after 3 attempts" >&2
|
||||||
|
exit 1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_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.",
|
"_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": {
|
"mcpServers": {
|
||||||
"brain": {
|
"brain": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
@@ -11,6 +11,24 @@
|
|||||||
"infra": {
|
"infra": {
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"url": "https://koala.tail30472e.ts.net:8444/mcp"
|
"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}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,16 +40,42 @@ moves — CI bumps it on every release.
|
|||||||
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Idempotent. It clones or fast-forwards the checkout, then creates **three**
|
Idempotent. It clones or fast-forwards the checkout, then populates one
|
||||||
symlinks — one directory link per harness, never one per skill:
|
**managed directory** and gives each harness a single link to it:
|
||||||
|
|
||||||
| Link | Target |
|
| Path | Target |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `~/.agents/skills` | `$SKILLS_CHECKOUT_DIR/skills` (Pi reads this path directly) |
|
| `~/.agents/skills/` | a real directory, one symlink per skill (Pi reads this path directly) |
|
||||||
|
| `~/.agents/skills/<skill>` | `$SKILLS_CHECKOUT_DIR/skills/<skill>`, or another configured source |
|
||||||
| `~/.config/crush/skills` | `~/.agents/skills` |
|
| `~/.config/crush/skills` | `~/.agents/skills` |
|
||||||
| `~/.config/opencode/skills` | `~/.agents/skills` |
|
| `~/.config/opencode/skills` | `~/.agents/skills` |
|
||||||
|
|
||||||
Adding a skill wires it everywhere with no re-run.
|
Total links are N + one per harness — not N x harnesses, which is what #23 was
|
||||||
|
actually about.
|
||||||
|
|
||||||
|
### More than one source of skills
|
||||||
|
|
||||||
|
`~/.agents/skills` used to be a single link straight at this checkout. That
|
||||||
|
encoded an assumption nobody had stated — that this checkout is the only source
|
||||||
|
of skills on the host — and on flamingo the only way to install this library
|
||||||
|
was to displace Anthropic's official set, which lived in Crush's directory.
|
||||||
|
Real capability loss dressed up as a tidy-up (#27).
|
||||||
|
|
||||||
|
Point `SKILLS_EXTRA_SOURCES` at any number of extra directories, colon
|
||||||
|
separated, each holding `<skill>/SKILL.md`:
|
||||||
|
|
||||||
|
```
|
||||||
|
SKILLS_EXTRA_SOURCES=~/.config/crush/skills.anthropic bash install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- The checkout is always first and always wins a name collision — but the
|
||||||
|
collision is **logged**, never resolved in silence. Quietly picking one is
|
||||||
|
how stale content wins.
|
||||||
|
- Removing a skill from a source removes its link on the next run.
|
||||||
|
- Anything in the managed directory that is not a symlink is reported and left
|
||||||
|
alone. install.sh does not delete content it did not create.
|
||||||
|
- The source list is recorded at `~/.agents/skills-sources` so `bin/doctor`
|
||||||
|
can tell a link into a known source from a link into a forgotten one.
|
||||||
|
|
||||||
### Where the checkout must live
|
### Where the checkout must live
|
||||||
|
|
||||||
@@ -74,8 +100,11 @@ default to point into `~/dev`.
|
|||||||
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
|
| `AGENTS_SKILLS_DIR` | `$HOME/.agents/skills` |
|
||||||
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
|
| `CRUSH_SKILLS_DIR` | `$HOME/.config/crush/skills` |
|
||||||
| `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` |
|
| `OPENCODE_SKILLS_DIR` | `$HOME/.config/opencode/skills` |
|
||||||
|
| `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) |
|
||||||
|
|
||||||
Antigravity and Mistral Vibe targets were removed — unused.
|
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
|
## Checking a host
|
||||||
|
|
||||||
@@ -83,10 +112,25 @@ Antigravity and Mistral Vibe targets were removed — unused.
|
|||||||
task doctor # or bin/doctor
|
task doctor # or bin/doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
Reports dangling links, links resolving into a foreign checkout, skill-count
|
Reports dangling links, links into a source nobody configured, skills in the
|
||||||
mismatches, stray entries, and leftover per-skill Claude symlinks. Non-zero
|
checkout with no link, stray entries, leftover per-skill Claude symlinks, and a
|
||||||
exit on drift. It also runs at session start via the plugin, so a drifting
|
Claude Code plugin sitting on an older generation than the host already has.
|
||||||
host says so rather than waiting to be discovered.
|
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
|
||||||
|
|
||||||
@@ -103,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. |
|
||||||
|
|||||||
+66
-54
@@ -2,51 +2,58 @@
|
|||||||
|
|
||||||
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 |
|
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index | claude-code, library, chat |
|
||||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature | claude-code, library |
|
||||||
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature |
|
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
|
||||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra | claude-code, library |
|
||||||
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod | claude-code, library, chat |
|
||||||
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
| `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 |
|
||||||
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index |
|
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; \"take this off my plate\"; before a plan exists | claude-code, library, chat |
|
||||||
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
|
| `planning` | Planning and task breakdown | Sprint planning, task decomposition | claude-code, library |
|
||||||
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
|
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI | claude-code, library |
|
||||||
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
| `problem-analysis` | Deep problem understanding before coding | Starting any non-trivial task | claude-code, library |
|
||||||
| `secret-hygiene` | Stop credentials rendering into the session transcript; safe rotation chain when one leaks | Before any kubectl/SOPS/token command; after a confirmed leak |
|
| `refactoring` | Systematic refactoring methodology (Fowler) | When improving existing code | claude-code, library |
|
||||||
| `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data |
|
| `regulatory-risk-assessment` | Structured risk register for regulated-industry features | Filing a CAD issue (needs Risk: level); features touching payments, auth, external APIs, user data | claude-code, library |
|
||||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
|
| `secret-hygiene` | Stop credentials rendering into the session transcript; safe rotation chain when one leaks | Before any kubectl/SOPS/token command; after a confirmed leak | claude-code, library |
|
||||||
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
|
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost | claude-code, library |
|
||||||
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists |
|
| `solid` | SOLID principles with Go examples | Design review, architecture decisions | claude-code, library |
|
||||||
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present |
|
| `spec-driven-dev` | Spec-driven development for PMs | Writing specs and requirements | claude-code, library |
|
||||||
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
|
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review | claude-code, library |
|
||||||
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
|
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas | claude-code, library |
|
||||||
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion |
|
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents | claude-code, library |
|
||||||
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents |
|
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion | claude-code, library |
|
||||||
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents |
|
| `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR | claude-code, library |
|
||||||
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically |
|
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments | claude-code, library |
|
||||||
| `superpowers-test-driven-development` | TDD discipline (superpowers variant) | Red → green → refactor on a change |
|
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review | claude-code, library |
|
||||||
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete |
|
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents | claude-code, library |
|
||||||
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review |
|
| `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree | claude-code, library |
|
||||||
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments |
|
| `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills | claude-code, library |
|
||||||
| `superpowers-finishing-a-development-branch` | Cleanly finishing/landing a dev branch | Wrapping a branch → merge/PR |
|
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete | claude-code, library |
|
||||||
| `superpowers-using-git-worktrees` | Isolating work with git worktrees | Parallel/isolated work in a worktree |
|
| `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 |
|
| `superpowers-writing-skills` | Authoring new SKILL.md skills | Creating a new reusable skill | claude-code, library |
|
||||||
| `superpowers-using-superpowers` | Meta: orienting in the superpowers set | Learning how to use the superpowers skills |
|
| `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
|
||||||
|
|
||||||
@@ -71,17 +78,21 @@ everything load twice.
|
|||||||
|
|
||||||
### Crush, Pi, OpenCode — run `install.sh`
|
### Crush, Pi, OpenCode — run `install.sh`
|
||||||
|
|
||||||
These have no plugin system, so they get symlinks — but **one directory link
|
These have no plugin system, so they get symlinks — but through one managed
|
||||||
each**, never one per skill:
|
directory, never one link per skill per harness:
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.agents/skills -> $SKILLS_CHECKOUT_DIR/skills (Pi reads this directly)
|
~/.agents/skills/ (real directory; Pi reads it directly)
|
||||||
|
~/.agents/skills/tdd -> $SKILLS_CHECKOUT_DIR/skills/tdd
|
||||||
|
~/.agents/skills/xlsx -> $SKILLS_EXTRA_SOURCES entry
|
||||||
~/.config/crush/skills -> ~/.agents/skills
|
~/.config/crush/skills -> ~/.agents/skills
|
||||||
~/.config/opencode/skills -> ~/.agents/skills
|
~/.config/opencode/skills -> ~/.agents/skills
|
||||||
```
|
```
|
||||||
|
|
||||||
Adding a skill therefore wires it everywhere with no re-run, and a broken link
|
Total links are N + one per harness. `SKILLS_EXTRA_SOURCES` (colon-separated
|
||||||
is one broken link instead of 42.
|
directories) lets a second library — Anthropic's official set, for instance —
|
||||||
|
live alongside this one instead of being displaced by it; name collisions are
|
||||||
|
logged rather than silently resolved, and the checkout wins (#27).
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
curl -fsSL https://git.d-ma.be/mathias/skills/raw/branch/main/install.sh | bash
|
||||||
@@ -95,10 +106,11 @@ absolute link stored there is broken on one side by construction. See #23.
|
|||||||
|
|
||||||
### Check a host
|
### Check a host
|
||||||
|
|
||||||
`task doctor` (or `bin/doctor`) reports dangling links, links resolving into a
|
`task doctor` (or `bin/doctor`) reports dangling links, links into a source
|
||||||
foreign checkout, count mismatches, stray entries, and leftover Claude
|
nobody configured, skills in the checkout with no link, stray entries, leftover
|
||||||
symlinks. It also runs automatically at session start via the plugin, so drift
|
Claude symlinks, and a Claude Code plugin sitting on an older generation than
|
||||||
announces itself instead of waiting to be found.
|
the host already has. It also runs automatically at session start via the
|
||||||
|
plugin, so drift announces itself instead of waiting to be found.
|
||||||
|
|
||||||
## Skill Relationships
|
## Skill Relationships
|
||||||
|
|
||||||
|
|||||||
+54
-8
@@ -61,8 +61,13 @@ tasks:
|
|||||||
done
|
done
|
||||||
exit $fail
|
exit $fail
|
||||||
|
|
||||||
|
index:
|
||||||
|
desc: 'Regenerate SKILLS_INDEX.md''s table from skill frontmatter.'
|
||||||
|
cmds:
|
||||||
|
- "'{{.TASKFILE_DIR}}/bin/generate-index' '{{.TASKFILE_DIR}}'"
|
||||||
|
|
||||||
check:
|
check:
|
||||||
desc: 'Structure gate — layout, frontmatter, manifest/tag parity. Runs in CI.'
|
desc: 'Structure gate — layout, frontmatter, surfaces, generated index, manifest/tag parity. Runs in CI.'
|
||||||
cmds:
|
cmds:
|
||||||
- |
|
- |
|
||||||
fail=0
|
fail=0
|
||||||
@@ -94,24 +99,65 @@ tasks:
|
|||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
# 3. SKILLS_INDEX.md lists exactly the skills that exist. Delegated
|
# 3. SKILLS_INDEX.md's table is generated from frontmatter, so the
|
||||||
# to bin/check-index so the rule is covered by
|
# assertion is that regenerating it changes nothing. This replaces
|
||||||
# tests/check_index_test.sh rather than living untested here.
|
# bin/check-index: a generated table cannot drift in either
|
||||||
sh '{{.TASKFILE_DIR}}/bin/check-index' '{{.TASKFILE_DIR}}' || fail=1
|
# direction, so there is no separate rule left to check.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/generate-index' --check '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
# 4. Plugin manifest version must match the latest tag, or installers
|
# 4. Every skill says which surfaces it ships to, and names only
|
||||||
|
# surfaces that exist — a wrong value silently drops a skill from a
|
||||||
|
# harness instead of erroring.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/check-surfaces' '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 5. 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).
|
# never see the update (Claude Code gates updates on this field).
|
||||||
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
if [ -f '{{.PLUGIN_MANIFEST}}' ]; then
|
||||||
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
manifest_v=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' '{{.PLUGIN_MANIFEST}}' | head -1)
|
||||||
tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
|
tag_v=$(git -C '{{.TASKFILE_DIR}}' describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '')
|
||||||
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ]; then
|
# Ordering, not equality. The fault this guards is the manifest
|
||||||
echo "ERROR: plugin.json version ($manifest_v) != latest tag ($tag_v)" >&2
|
# failing to MOVE — a release tag whose manifest still names the old
|
||||||
|
# version, so no installed host ever sees the update. A manifest
|
||||||
|
# ahead of the newest tag this checkout knows about is the normal
|
||||||
|
# state between the tagger's bump commit and a tag fetch (#34).
|
||||||
|
if [ -n "$tag_v" ] && [ "$manifest_v" != "$tag_v" ] \
|
||||||
|
&& [ "$(sh '{{.TASKFILE_DIR}}/bin/newer-version' "$manifest_v" "$tag_v")" = "$tag_v" ]; then
|
||||||
|
echo "ERROR: plugin.json version ($manifest_v) is behind the latest tag ($tag_v) — installed hosts never see the update" >&2
|
||||||
fail=1
|
fail=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "ERROR: missing .claude-plugin/plugin.json" >&2; fail=1
|
echo "ERROR: missing .claude-plugin/plugin.json" >&2; fail=1
|
||||||
fi
|
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
|
if [ $fail -ne 0 ]; then
|
||||||
echo "check FAILED" >&2; exit 1
|
echo "check FAILED" >&2; exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
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
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# check-index — asserts SKILLS_INDEX.md lists exactly the skills that exist.
|
|
||||||
#
|
|
||||||
# The index is hand-maintained and drifts silently in both directions: a skill
|
|
||||||
# ships with no row (`dream` and `experiment-spec` did — their rows lived on
|
|
||||||
# branches squash-merged after a directory reorg, so the SKILL.md landed and
|
|
||||||
# the row did not, #15), or a row outlives the skill it names. Neither errors
|
|
||||||
# anywhere; the library just misreports itself to every harness that reads it.
|
|
||||||
#
|
|
||||||
# Usage: check-index [checkout-root] (defaults to this script's repo root)
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
ROOT=${1:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}
|
|
||||||
INDEX="$ROOT/SKILLS_INDEX.md"
|
|
||||||
fail=0
|
|
||||||
|
|
||||||
if [ ! -f "$INDEX" ]; then
|
|
||||||
echo "ERROR: missing SKILLS_INDEX.md" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A row is a table line whose first cell is the skill name in backticks.
|
|
||||||
listed=$(sed -n 's/^|[[:space:]]*`\([^`]*\)`.*/\1/p' "$INDEX" | sort -u)
|
|
||||||
|
|
||||||
for dir in "$ROOT"/skills/*/; do
|
|
||||||
name=$(basename "$dir")
|
|
||||||
[ -f "$dir/SKILL.md" ] || continue
|
|
||||||
printf '%s\n' "$listed" | grep -qx "$name" || {
|
|
||||||
echo "ERROR: skills/$name has no row in SKILLS_INDEX.md" >&2
|
|
||||||
fail=1
|
|
||||||
}
|
|
||||||
done
|
|
||||||
|
|
||||||
for name in $listed; do
|
|
||||||
[ -f "$ROOT/skills/$name/SKILL.md" ] || {
|
|
||||||
echo "ERROR: SKILLS_INDEX.md lists '$name', which has no skills/$name/SKILL.md" >&2
|
|
||||||
fail=1
|
|
||||||
}
|
|
||||||
done
|
|
||||||
|
|
||||||
exit $fail
|
|
||||||
Executable
+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
|
||||||
+173
-26
@@ -10,6 +10,7 @@
|
|||||||
# doctor human report, non-zero exit on drift
|
# doctor human report, non-zero exit on drift
|
||||||
# doctor --quiet print only when something is wrong (SessionStart hook)
|
# doctor --quiet print only when something is wrong (SessionStart hook)
|
||||||
# doctor --json machine-readable findings
|
# 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
|
# 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.
|
# guaranteed present, and inside a session-start hook where latency shows.
|
||||||
@@ -21,10 +22,12 @@ set -u
|
|||||||
|
|
||||||
QUIET=0
|
QUIET=0
|
||||||
JSON=0
|
JSON=0
|
||||||
|
ONLINE=0
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--quiet) QUIET=1 ;;
|
--quiet) QUIET=1 ;;
|
||||||
--json) JSON=1 ;;
|
--json) JSON=1 ;;
|
||||||
|
--online) ONLINE=1 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -39,12 +42,21 @@ SELF_CHECKOUT=$(CDPATH= cd -- "$SELF_DIR/.." && pwd)
|
|||||||
# copy doctor happens to be running from. When invoked as a plugin hook, $0
|
# copy doctor happens to be running from. When invoked as a plugin hook, $0
|
||||||
# lives in the plugin cache, which is legitimately a second copy: judging the
|
# lives in the plugin cache, which is legitimately a second copy: judging the
|
||||||
# harness links against it would flag every one of them as "outside".
|
# harness links against it would flag every one of them as "outside".
|
||||||
|
AGENTS_DIR="$HOME/.agents/skills"
|
||||||
|
SOURCES_FILE="$(dirname -- "$AGENTS_DIR")/$(basename -- "$AGENTS_DIR")-sources"
|
||||||
|
|
||||||
if [ -n "${DOCTOR_CHECKOUT:-}" ]; then
|
if [ -n "${DOCTOR_CHECKOUT:-}" ]; then
|
||||||
CHECKOUT="$DOCTOR_CHECKOUT"
|
CHECKOUT="$DOCTOR_CHECKOUT"
|
||||||
elif [ -d "$HOME/.agents/skills" ]; then
|
elif [ -f "$SOURCES_FILE" ]; then
|
||||||
# Resolve the link FIRST, then take the parent. "cd X/.." resolves ".."
|
# Since #27 the canonical location is a real directory fed by several
|
||||||
# lexically against the link path, which lands back in ~/.agents.
|
# sources, so it can no longer be resolved to a checkout. install.sh records
|
||||||
_resolved=$(CDPATH= cd -- "$HOME/.agents/skills" 2>/dev/null && pwd -P)
|
# the list; the first line is always this repo's skills/ directory.
|
||||||
|
CHECKOUT=$(dirname -- "$(head -n 1 "$SOURCES_FILE")")
|
||||||
|
elif [ -L "$AGENTS_DIR" ]; then
|
||||||
|
# Pre-#27 wiring: one link straight at the checkout. Resolve the link FIRST,
|
||||||
|
# then take the parent. "cd X/.." resolves ".." lexically against the link
|
||||||
|
# path, which lands back in ~/.agents.
|
||||||
|
_resolved=$(CDPATH= cd -- "$AGENTS_DIR" 2>/dev/null && pwd -P)
|
||||||
CHECKOUT=$(dirname -- "$_resolved")
|
CHECKOUT=$(dirname -- "$_resolved")
|
||||||
else
|
else
|
||||||
CHECKOUT="$SELF_CHECKOUT"
|
CHECKOUT="$SELF_CHECKOUT"
|
||||||
@@ -66,9 +78,26 @@ add() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# count_skills <dir> — directories containing a SKILL.md, one level down.
|
# count_skills <dir> — directories containing a SKILL.md, one level down.
|
||||||
|
# -L on purpose: since #27 the entries are per-skill symlinks, and find does
|
||||||
|
# not descend into a symlinked directory without it.
|
||||||
count_skills() {
|
count_skills() {
|
||||||
[ -d "$1" ] || { printf '0'; return; }
|
[ -d "$1" ] || { printf '0'; return; }
|
||||||
find "$1"/ -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
|
find -L "$1"/ -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
# under_known_source <physical-path> — 0 when the path lies inside a source
|
||||||
|
# install.sh recorded, or inside the active checkout.
|
||||||
|
under_known_source() {
|
||||||
|
_path=$1
|
||||||
|
_list="$SKILLS_DIR"
|
||||||
|
[ -f "$SOURCES_FILE" ] && _list="$_list
|
||||||
|
$(cat "$SOURCES_FILE")"
|
||||||
|
printf '%s\n' "$_list" | while IFS= read -r _src; do
|
||||||
|
[ -n "$_src" ] || continue
|
||||||
|
_real=$(CDPATH= cd -- "$_src" 2>/dev/null && pwd -P) || continue
|
||||||
|
[ -n "$_real" ] || continue
|
||||||
|
case "$_path" in "$_real"/*|"$_real") printf 'yes'; break ;; esac
|
||||||
|
done | grep -q yes
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- the checkout itself ----------------------------------------------------
|
# --- the checkout itself ----------------------------------------------------
|
||||||
@@ -95,10 +124,54 @@ for entry in "$SKILLS_DIR"/*; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --- the canonical directory --------------------------------------------------
|
||||||
|
# Pre-#27 this was a symlink at the checkout. It is a real directory now, one
|
||||||
|
# per-skill link per skill, fed by every configured source — that is what lets
|
||||||
|
# a second library (Anthropic's official set on flamingo) coexist instead of
|
||||||
|
# being displaced.
|
||||||
|
if [ -L "$AGENTS_DIR" ]; then
|
||||||
|
add WARN legacy "$AGENTS_DIR is the pre-#27 single-link wiring — re-run install.sh to adopt the multi-source layout"
|
||||||
|
elif [ -d "$AGENTS_DIR" ]; then
|
||||||
|
for entry in "$AGENTS_DIR"/*; do
|
||||||
|
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
||||||
|
name=$(basename "$entry")
|
||||||
|
|
||||||
|
if [ ! -L "$entry" ]; then
|
||||||
|
add FAIL unmanaged "$AGENTS_DIR/$name is not a link into any source — nothing manages it and nothing updates it"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ ! -e "$entry" ]; then
|
||||||
|
add FAIL dangling "$entry -> $(readlink "$entry") (target does not exist)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolved=$(CDPATH= cd -- "$entry" 2>/dev/null && pwd -P)
|
||||||
|
if [ -n "$resolved" ] && ! under_known_source "$resolved"; then
|
||||||
|
add FAIL outside "$entry resolves to $resolved, an unknown source — it works, so nothing complains, while serving content nobody updates"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# A per-skill layout can lose one skill without changing any total, so the
|
||||||
|
# invariant is per skill, not a count.
|
||||||
|
for dir in "$SKILLS_DIR"/*/; do
|
||||||
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
|
name=$(basename "$dir")
|
||||||
|
# Only skills that ship to the library belong here. A chat-only skill
|
||||||
|
# missing from a Crush directory is correct, not drift (#29).
|
||||||
|
surfaces=$(sed -n 's/^surfaces:[[:space:]]*//p' "$dir/SKILL.md" | head -1)
|
||||||
|
if [ -n "$surfaces" ] \
|
||||||
|
&& ! printf '%s' "$surfaces" | tr -d '[]' | tr ',' ' ' | grep -qw library; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[ -L "$AGENTS_DIR/$name" ] \
|
||||||
|
|| add FAIL missing "$name is in the checkout but has no link in $AGENTS_DIR — re-run install.sh"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# --- harness wiring ---------------------------------------------------------
|
# --- harness wiring ---------------------------------------------------------
|
||||||
# Canonical location first; Pi reads it directly and the others link to it.
|
# Crush and OpenCode keep their single directory link at the canonical dir;
|
||||||
|
# Pi reads it directly and needs no link of its own.
|
||||||
for dir in \
|
for dir in \
|
||||||
"$HOME/.agents/skills" \
|
|
||||||
"$HOME/.config/crush/skills" \
|
"$HOME/.config/crush/skills" \
|
||||||
"$HOME/.config/opencode/skills"
|
"$HOME/.config/opencode/skills"
|
||||||
do
|
do
|
||||||
@@ -110,32 +183,23 @@ do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# A real directory where a link belongs holds content nothing manages: it
|
# A real directory where a link belongs holds content nothing manages: it
|
||||||
# cannot receive new skills and silently shadows the checkout. Distinguished
|
# cannot receive new skills and silently shadows the canonical dir. Since
|
||||||
# from the foreign-checkout case below because the fix differs — move it
|
# #27 the fix is not only "move it aside" — it can be added as an extra
|
||||||
# aside and re-run install.sh, rather than re-point a link.
|
# source instead, which is how flamingo keeps the official Anthropic set.
|
||||||
if [ ! -L "$dir" ] && [ -d "$dir" ]; then
|
if [ ! -L "$dir" ] && [ -d "$dir" ]; then
|
||||||
add FAIL unmanaged "$dir is a real directory, not a link into the checkout — it holds $(count_skills "$dir") unmanaged skill(s); move it aside and re-run install.sh"
|
add FAIL unmanaged "$dir is a real directory, not a link into the canonical dir — it holds $(count_skills "$dir") unmanaged skill(s); add it to SKILLS_EXTRA_SOURCES and re-run install.sh, or move it aside"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve to a physical path and confirm it lands inside THIS checkout.
|
|
||||||
# A link that resolves to a different checkout is the dangerous case: it
|
|
||||||
# works, so nothing complains, while serving stale content.
|
|
||||||
resolved=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd -P)
|
resolved=$(CDPATH= cd -- "$dir" 2>/dev/null && pwd -P)
|
||||||
canonical=$(CDPATH= cd -- "$SKILLS_DIR" 2>/dev/null && pwd -P)
|
canonical=$(CDPATH= cd -- "$AGENTS_DIR" 2>/dev/null && pwd -P)
|
||||||
if [ -n "$resolved" ] && [ "$resolved" != "$canonical" ]; then
|
if [ -n "$resolved" ] && [ -n "$canonical" ] && [ "$resolved" != "$canonical" ]; then
|
||||||
add FAIL outside "$dir resolves to $resolved, outside the active checkout $canonical"
|
add FAIL outside "$dir resolves to $resolved, not the canonical $canonical"
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
got=$(count_skills "$dir")
|
|
||||||
if [ "$got" != "$EXPECTED" ]; then
|
|
||||||
add FAIL count "$dir sees $got skills, checkout has $EXPECTED"
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- dangling links anywhere under the harness dirs -------------------------
|
# --- dangling links anywhere under the harness dirs -------------------------
|
||||||
for dir in "$HOME/.agents/skills" "$HOME/.config/crush/skills" \
|
for dir in "$HOME/.config/crush/skills" \
|
||||||
"$HOME/.config/opencode/skills" "$HOME/.claude/skills"
|
"$HOME/.config/opencode/skills" "$HOME/.claude/skills"
|
||||||
do
|
do
|
||||||
[ -d "$dir" ] || continue
|
[ -d "$dir" ] || continue
|
||||||
@@ -180,10 +244,93 @@ if [ -f "$INSTALLED" ]; then
|
|||||||
|| add WARN plugin "skills@mathias is not installed for Claude Code — /plugin marketplace add https://git.d-ma.be/mathias/skills.git"
|
|| add WARN plugin "skills@mathias is not installed for Claude Code — /plugin marketplace add https://git.d-ma.be/mathias/skills.git"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Claude Code: which GENERATION of the plugin is installed ---------------
|
||||||
|
# Every check above is about symlink paths, which is the wrong shape for the
|
||||||
|
# plugin: it is the only channel Claude Code has, and Claude Code gates updates
|
||||||
|
# on plugin.json's version. A host that never picked up a release loads all the
|
||||||
|
# skills successfully and is simply a generation behind — silently. Found on
|
||||||
|
# koala 2026-08-29: installed 0.6.0, marketplace clone 0.7.0, repo 0.8.5,
|
||||||
|
# doctor said "clean" (#30).
|
||||||
|
#
|
||||||
|
# Offline by construction, like the rest of doctor: "available" means the
|
||||||
|
# newest manifest already on this host — the active checkout and any
|
||||||
|
# marketplace clone — never a fetch.
|
||||||
|
manifest_version() {
|
||||||
|
[ -f "$1" ] || return 1
|
||||||
|
grep -q '"name"[[:space:]]*:[[:space:]]*"skills"' "$1" || return 1
|
||||||
|
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# newer_version a b — echoes whichever sorts higher on major.minor.patch.
|
||||||
|
# Lives in bin/newer-version because `task check` needs the same ordering (#34)
|
||||||
|
# and a second copy is a second thing to get wrong.
|
||||||
|
newer_version() {
|
||||||
|
sh "$SELF_DIR/newer-version" "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
installed_v=''
|
||||||
|
if [ -f "$INSTALLED" ]; then
|
||||||
|
installed_v=$(sed -n '/"skills@mathias"/,/\]/p' "$INSTALLED" \
|
||||||
|
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
available_v=''
|
||||||
|
for manifest in "$CHECKOUT/.claude-plugin/plugin.json" \
|
||||||
|
"$HOME"/.claude/plugins/marketplaces/*/.claude-plugin/plugin.json
|
||||||
|
do
|
||||||
|
v=$(manifest_version "$manifest") || continue
|
||||||
|
[ -n "$v" ] || continue
|
||||||
|
if [ -z "$available_v" ]; then
|
||||||
|
available_v=$v
|
||||||
|
else
|
||||||
|
available_v=$(newer_version "$available_v" "$v")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$installed_v" ] && [ -n "$available_v" ] && [ "$installed_v" != "$available_v" ]; then
|
||||||
|
add FAIL plugin-version "Claude Code has skills@mathias $installed_v installed but $available_v is available on this host — /plugin update skills@mathias (a host stale enough to matter is running the old doctor, so the first detection may need this run by hand)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
|
||||||
# --- output -----------------------------------------------------------------
|
# --- output -----------------------------------------------------------------
|
||||||
if [ "$JSON" -eq 1 ]; then
|
if [ "$JSON" -eq 1 ]; then
|
||||||
printf '{"checkout":"%s","expected":%s,"findings":[%s]}\n' \
|
printf '{"checkout":"%s","expected":%s,"plugin":{"installed":"%s","available":"%s"},"findings":[%s]}\n' \
|
||||||
"$CHECKOUT" "$EXPECTED" "${report%,}"
|
"$CHECKOUT" "$EXPECTED" "$installed_v" "$available_v" "${report%,}"
|
||||||
elif [ "$findings" -gt 0 ]; then
|
elif [ "$findings" -gt 0 ]; then
|
||||||
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
|
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
|
||||||
"$findings" "$CHECKOUT" "$report"
|
"$findings" "$CHECKOUT" "$report"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}'
|
||||||
+173
-8
@@ -17,17 +17,34 @@
|
|||||||
# /plugin install skills@mathias
|
# /plugin install skills@mathias
|
||||||
#
|
#
|
||||||
# That path carries the hooks and MCP server definitions too, which symlinks
|
# That path carries the hooks and MCP server definitions too, which symlinks
|
||||||
# cannot. This script covers the harnesses with no plugin system: Crush, Pi
|
# cannot. This script covers the harnesses with no plugin system: Crush, Pi,
|
||||||
# and OpenCode.
|
# OpenCode, and Antigravity.
|
||||||
#
|
#
|
||||||
# Wiring model — one directory link per harness, never one per skill:
|
# Wiring model — one managed directory, one link per harness:
|
||||||
#
|
#
|
||||||
# ~/.agents/skills -> $CHECKOUT_DIR/skills (canonical; Pi reads it directly)
|
# ~/.agents/skills/tdd -> $CHECKOUT_DIR/skills/tdd (canonical; Pi reads the dir directly)
|
||||||
|
# ~/.agents/skills/xlsx -> $OTHER_SOURCE/xlsx
|
||||||
# ~/.config/crush/skills -> ~/.agents/skills
|
# ~/.config/crush/skills -> ~/.agents/skills
|
||||||
# ~/.config/opencode/skills -> ~/.agents/skills
|
# ~/.config/opencode/skills -> ~/.agents/skills
|
||||||
#
|
#
|
||||||
# Adding a skill to the repo therefore wires it everywhere with no re-run,
|
# Antigravity has no symlink-directory discovery — it reads a path-entries
|
||||||
# and a broken link is one broken link rather than 42.
|
# 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
|
# CHECKOUT_DIR deliberately defaults OUTSIDE any file-synced or
|
||||||
# cross-platform-shared directory. Do not "fix" it to point into ~/dev: on a
|
# cross-platform-shared directory. Do not "fix" it to point into ~/dev: on a
|
||||||
@@ -43,6 +60,15 @@ CHECKOUT_DIR="${SKILLS_CHECKOUT_DIR:-$HOME/.local/share/skills}"
|
|||||||
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
AGENTS_DIR="${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
||||||
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
CRUSH_DIR="${CRUSH_SKILLS_DIR:-$HOME/.config/crush/skills}"
|
||||||
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
|
OPENCODE_DIR="${OPENCODE_SKILLS_DIR:-$HOME/.config/opencode/skills}"
|
||||||
|
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' "$*"; }
|
||||||
|
|
||||||
@@ -74,6 +100,106 @@ count_skills() {
|
|||||||
find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
|
find "$CHECKOUT_DIR/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# sources — every configured source directory, primary first, one per line.
|
||||||
|
sources() {
|
||||||
|
printf '%s\n' "$CHECKOUT_DIR/skills"
|
||||||
|
[ -n "$EXTRA_SOURCES" ] || return 0
|
||||||
|
# printf '%s\n' deliberately: without the trailing newline `read` hits EOF
|
||||||
|
# on the final entry and the loop body never runs for it.
|
||||||
|
printf '%s\n' "$EXTRA_SOURCES" | tr ':' '\n' | while IFS= read -r src; do
|
||||||
|
[ -n "$src" ] || continue
|
||||||
|
if [ -d "$src" ]; then
|
||||||
|
printf '%s\n' "$src"
|
||||||
|
else
|
||||||
|
log "extra source $src does not exist — skipping"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare_agents_dir — the canonical location is a real directory now. A host
|
||||||
|
# still on the single-link wiring is migrated in place.
|
||||||
|
prepare_agents_dir() {
|
||||||
|
if [ -L "$AGENTS_DIR" ]; then
|
||||||
|
log "migrating $AGENTS_DIR from the single-link wiring to a per-skill directory"
|
||||||
|
rm -f "$AGENTS_DIR"
|
||||||
|
elif [ -e "$AGENTS_DIR" ] && [ ! -d "$AGENTS_DIR" ]; then
|
||||||
|
log "REFUSING to replace $AGENTS_DIR — it exists and is not a directory"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$AGENTS_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# wire_skills — one symlink per skill, from every configured source.
|
||||||
|
wire_skills() {
|
||||||
|
local src_list desired name src dir link target
|
||||||
|
src_list="$(mktemp)"
|
||||||
|
sources > "$src_list"
|
||||||
|
|
||||||
|
desired=""
|
||||||
|
while IFS= read -r src; do
|
||||||
|
[ -d "$src" ] || continue
|
||||||
|
for dir in "$src"/*/; do
|
||||||
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
|
name="$(basename "$dir")"
|
||||||
|
target="${dir%/}"
|
||||||
|
|
||||||
|
# A skill declares its surfaces; this installer serves exactly one of
|
||||||
|
# them. A chat-only skill linked into Crush can never fire and competes
|
||||||
|
# for selection with ones that can (#29). A source with no declaration
|
||||||
|
# is foreign — Anthropic's official set has no reason to know this
|
||||||
|
# convention — so silence means "link it".
|
||||||
|
surfaces="$(sed -n 's/^surfaces:[[:space:]]*//p' "$dir/SKILL.md" | head -1)"
|
||||||
|
if [ -n "$surfaces" ] && ! printf '%s' "$surfaces" | tr -d '[]' | tr ',' ' ' | grep -qw library; then
|
||||||
|
log "skipping $name — surfaces: $surfaces does not include the library"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s' "$desired" | grep -qx -- "$name"; then
|
||||||
|
log "COLLISION: $name is offered by $target too; keeping $AGENTS_DIR/$name -> $(readlink "$AGENTS_DIR/$name" 2>/dev/null || echo '?')"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
link="$AGENTS_DIR/$name"
|
||||||
|
if [ -L "$link" ]; then
|
||||||
|
if [ "$(readlink "$link")" = "$target" ]; then
|
||||||
|
desired="$desired$name
|
||||||
|
"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
rm -f "$link"
|
||||||
|
elif [ -e "$link" ]; then
|
||||||
|
log "unmanaged: $link is not a symlink — left in place; move it aside to let $target take the name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
ln -s "$target" "$link"
|
||||||
|
desired="$desired$name
|
||||||
|
"
|
||||||
|
done
|
||||||
|
done < "$src_list"
|
||||||
|
rm -f "$src_list"
|
||||||
|
|
||||||
|
# Removing a skill from a source must remove its link, or the previous
|
||||||
|
# generation's problem (links resolving to nothing, invisible to git)
|
||||||
|
# returns in a new shape.
|
||||||
|
for link in "$AGENTS_DIR"/*; do
|
||||||
|
[ -e "$link" ] || [ -L "$link" ] || continue
|
||||||
|
name="$(basename "$link")"
|
||||||
|
if [ ! -L "$link" ]; then
|
||||||
|
log "unmanaged: $link is not a symlink and no source offers it — left in place"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
printf '%s' "$desired" | grep -qx -- "$name" && continue
|
||||||
|
rm -f "$link"
|
||||||
|
log "pruned $name — no configured source offers it any more"
|
||||||
|
done
|
||||||
|
|
||||||
|
sources > "$SOURCES_MANIFEST"
|
||||||
|
# Reported through a global, not stdout: wire_skills logs collisions and
|
||||||
|
# unmanaged entries as it goes, and capturing its stdout for a count would
|
||||||
|
# swallow exactly the warnings this change exists to surface.
|
||||||
|
LINKED_COUNT=$(printf '%s' "$desired" | grep -c . || true)
|
||||||
|
}
|
||||||
|
|
||||||
# link_dir <link_path> <target_path>
|
# link_dir <link_path> <target_path>
|
||||||
# Replaces whatever is there — including a directory left behind by the old
|
# Replaces whatever is there — including a directory left behind by the old
|
||||||
# per-skill wiring — with a single symlink.
|
# per-skill wiring — with a single symlink.
|
||||||
@@ -99,17 +225,56 @@ link_dir() {
|
|||||||
log "linked $link → $target"
|
log "linked $link → $target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# wire_antigravity — register $AGENTS_DIR in Antigravity's skills.json.
|
||||||
|
# Merges rather than overwrites: skills.json is a documented user-editable
|
||||||
|
# config (json_configs.md), so a hand-added entry (e.g. a personal skills
|
||||||
|
# dir) must survive a re-run untouched.
|
||||||
|
wire_antigravity() {
|
||||||
|
if ! command -v "$JQ" >/dev/null 2>&1; then
|
||||||
|
log "Antigravity: jq not found — skipping (needed to merge $ANTIGRAVITY_SKILLS_JSON safely)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local existing already updated
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
updated="$(printf '%s' "$existing" | "$JQ" --arg p "$AGENTS_DIR" \
|
||||||
|
'.entries = ((.entries // []) + [{path: $p}])')"
|
||||||
|
printf '%s\n' "$updated" > "$ANTIGRAVITY_SKILLS_JSON"
|
||||||
|
log "Antigravity: registered $AGENTS_DIR in $ANTIGRAVITY_SKILLS_JSON"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
ensure_checkout
|
ensure_checkout
|
||||||
|
|
||||||
# Canonical location. Pi reads ~/.agents/skills directly, so it needs no
|
# Canonical location. Pi reads ~/.agents/skills directly, so it needs no
|
||||||
# harness-specific link of its own.
|
# harness-specific link of its own.
|
||||||
link_dir "$AGENTS_DIR" "$CHECKOUT_DIR/skills"
|
prepare_agents_dir
|
||||||
|
LINKED_COUNT=0
|
||||||
|
wire_skills
|
||||||
|
|
||||||
link_dir "$CRUSH_DIR" "$AGENTS_DIR"
|
link_dir "$CRUSH_DIR" "$AGENTS_DIR"
|
||||||
link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
|
link_dir "$OPENCODE_DIR" "$AGENTS_DIR"
|
||||||
|
wire_antigravity
|
||||||
|
|
||||||
log "done — $(count_skills) skill(s) at ref=$REF, 3 links total"
|
log "done — $LINKED_COUNT skill(s) linked from $(sources | wc -l | tr -d ' ') source(s), ref=$REF"
|
||||||
log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL"
|
log "Claude Code: install the plugin instead — /plugin marketplace add $REPO_URL"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: cad-babysit
|
name: cad-babysit
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Focused human-in-the-loop session working CAD's stuck queue (read-only)"
|
||||||
|
use_when: "Picking up whatever `cad stuck` flags, with a human present"
|
||||||
description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present.
|
description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -231,8 +234,8 @@ wrong" — it means "nothing on the known list is wrong."
|
|||||||
Call `session_log` once at the end of every phase to record the outcome.
|
Call `session_log` once at the end of every phase to record the outcome.
|
||||||
|
|
||||||
**At end of each phase:**
|
**At end of each phase:**
|
||||||
- `session_log` with `{skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
|
- `session_log` with `{session_id: "<this session's id>", skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
|
||||||
"fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root:
|
"fail" | "skip", message: "<one-line summary>", project_root:
|
||||||
"<absolute path>"}`
|
"<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
|
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
|
||||||
@@ -244,12 +247,6 @@ decide
|
|||||||
- `fail` — the phase could not be completed (e.g. `cad stuck` errored, Gitea was unreachable).
|
- `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).
|
- `skip` — phase intentionally skipped (e.g. Phase 3 skipped because Phase 2 was conclusive).
|
||||||
|
|
||||||
## Mode 2 Routing Note
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Cross-References
|
## Cross-References
|
||||||
|
|
||||||
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
|
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
|
||||||
@@ -261,3 +258,13 @@ routing candidate once Plan 6 ships the routing pod.
|
|||||||
- `mathias/cad-atlas#11` renders the same `cad stuck` output as a pipeline overlay — read that
|
- `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
|
instead of this skill's text output when a visual "where is everything stuck" view is wanted
|
||||||
over working one row in depth.
|
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.
|
||||||
|
|||||||
+110
-17
@@ -1,6 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: debug
|
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.
|
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
|
# Debug
|
||||||
@@ -9,7 +12,7 @@ description: Systematic hypothesis-first debugging. Generate 3-5 ranked hypothes
|
|||||||
|
|
||||||
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.
|
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.
|
**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
|
## When to Use
|
||||||
|
|
||||||
@@ -22,12 +25,65 @@ Debugging is hypothesis generation, not fix generation. The first instinct — "
|
|||||||
- Compile errors with clear messages (just fix the typo)
|
- 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)
|
- 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
|
## Iron Laws
|
||||||
|
|
||||||
1. **Never suggest "try X and see what happens."** Every hypothesis must have a specific expected outcome if correct.
|
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.
|
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.
|
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
|
## Process
|
||||||
|
|
||||||
### Step 1: Read the failure
|
### Step 1: Read the failure
|
||||||
@@ -46,18 +102,28 @@ For each hypothesis, capture three things:
|
|||||||
- **Verification:** the single command or file inspection that confirms or denies it
|
- **Verification:** the single command or file inspection that confirms or denies it
|
||||||
- **Expected outcome if correct:** the specific output you would see
|
- **Expected outcome if correct:** the specific output you would see
|
||||||
|
|
||||||
Order by likelihood. The most likely cause is hypothesis 1.
|
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
|
### Step 3: Output
|
||||||
|
|
||||||
Use this format:
|
Use this format:
|
||||||
|
|
||||||
```
|
```
|
||||||
HYPOTHESIS 1 (likelihood: high): <mechanism in one sentence>
|
HYPOTHESIS 1: <mechanism in one sentence>
|
||||||
VERIFY: <exact command or file:line to inspect>
|
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>
|
EXPECTED IF CORRECT: <specific output, value, or condition>
|
||||||
|
|
||||||
HYPOTHESIS 2 (likelihood: medium): <mechanism>
|
HYPOTHESIS 2: <mechanism>
|
||||||
|
WHY FIRST: <or why it ranks here rather than first>
|
||||||
VERIFY: <exact command>
|
VERIFY: <exact command>
|
||||||
EXPECTED IF CORRECT: <specific output>
|
EXPECTED IF CORRECT: <specific output>
|
||||||
|
|
||||||
@@ -79,15 +145,15 @@ End with the recommendation, not a fix.
|
|||||||
**Output:**
|
**Output:**
|
||||||
|
|
||||||
```
|
```
|
||||||
HYPOTHESIS 1 (likelihood: high): parser indexes into pages[0] before checking len(pages) > 0; empty PDFs produce a zero-page document
|
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/
|
VERIFY: rg -n 'pages\[0\]' internal/parser/
|
||||||
EXPECTED IF CORRECT: at least one site reads pages[0] without a preceding length check
|
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
|
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
|
VERIFY: ls -la testdata/empty.pdf && file testdata/empty.pdf
|
||||||
EXPECTED IF CORRECT: file size 0 bytes or "data" rather than "PDF document"
|
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
|
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
|
VERIFY: git log --oneline -10 -- go.sum | grep -i pdf
|
||||||
EXPECTED IF CORRECT: a pdf-library bump in the last few commits
|
EXPECTED IF CORRECT: a pdf-library bump in the last few commits
|
||||||
|
|
||||||
@@ -101,6 +167,7 @@ RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
|||||||
| "Maybe try restarting the service" | Not a mechanism. Not falsifiable. Teaches nothing if it works. |
|
| "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? |
|
| "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. |
|
| "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. |
|
| 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. |
|
| Mixing hypotheses with fix suggestions | The skill is diagnose-only. The fix is a TDD task on its hypothesis. |
|
||||||
|
|
||||||
@@ -109,7 +176,8 @@ RECOMMENDED NEXT STEP: Run VERIFY for hypothesis 1 first.
|
|||||||
The brain holds prior debug sessions across the project. Use it to skip rediscovering known failure modes.
|
The brain holds prior debug sessions across the project. Use it to skip rediscovering known failure modes.
|
||||||
|
|
||||||
**At debug start:**
|
**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.
|
- 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:**
|
**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.
|
- Run `brain_write` with the failure signature → confirmed mechanism. Future debug sessions on the same area get the answer immediately.
|
||||||
@@ -124,23 +192,48 @@ 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: "debug", 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: "debug", phase: "<phase-name>", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** read-failure, generate-hypotheses, output
|
**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:**
|
**Status semantics:**
|
||||||
- `pass` — the phase's intended outcome was reached.
|
- `pass` — the phase's intended outcome was reached.
|
||||||
- `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.
|
||||||
|
|
||||||
## 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
|
## Cross-References
|
||||||
|
|
||||||
- After a hypothesis is confirmed, load `tdd` skill — write a failing regression test that proves the bug exists, then fix it.
|
- 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`.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -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,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: gitea-ci
|
name: gitea-ci
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror"
|
||||||
|
use_when: "Setting up or debugging CI on this infra"
|
||||||
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, shared buildctl image-build script, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
|
description: End-to-end Gitea CI/CD setup with self-hosted act_runner, quality gate pipeline, shared buildctl image-build script, k3s deploy, and GitHub mirror. Covers act_runner-specific gotchas that are not documented anywhere.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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").
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: secret-hygiene
|
name: secret-hygiene
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Stop credentials rendering into the session transcript; safe rotation chain when one leaks"
|
||||||
|
use_when: "Before any kubectl/SOPS/token command; after a confirmed leak"
|
||||||
description: Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong".
|
description: Prevent agent sessions from rendering credentials into the transcript, and recover safely when one leaks. Load BEFORE any command that touches credentials - kubectl exec/env/describe, kubectl get secret, whole-file SOPS decryption, token generation or rotation, envFrom inspection, or edits to encrypted config. Also load when a credential has already been exposed and a rotation chain is needed. Triggers - "rotate this token", "decrypt the config", "check the env var", "leaked credential", "did that print a secret", "why is this secret wrong".
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: finishing-a-development-branch
|
name: finishing-a-development-branch
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Cleanly finishing/landing a dev branch"
|
||||||
|
use_when: "Wrapping a branch → merge/PR"
|
||||||
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: receiving-code-review
|
name: receiving-code-review
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Acting on received review feedback"
|
||||||
|
use_when: "Responding to review comments"
|
||||||
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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."
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
# Creation Log: Systematic Debugging Skill
|
|
||||||
|
|
||||||
Reference example of extracting, structuring, and bulletproofing a critical skill.
|
|
||||||
|
|
||||||
## Source Material
|
|
||||||
|
|
||||||
Extracted debugging framework from `~/.claude/CLAUDE.md`:
|
|
||||||
- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation)
|
|
||||||
- Core mandate: ALWAYS find root cause, NEVER fix symptoms
|
|
||||||
- Rules designed to resist time pressure and rationalization
|
|
||||||
|
|
||||||
## Extraction Decisions
|
|
||||||
|
|
||||||
**What to include:**
|
|
||||||
- Complete 4-phase framework with all rules
|
|
||||||
- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze")
|
|
||||||
- Pressure-resistant language ("even if faster", "even if I seem in a hurry")
|
|
||||||
- Concrete steps for each phase
|
|
||||||
|
|
||||||
**What to leave out:**
|
|
||||||
- Project-specific context
|
|
||||||
- Repetitive variations of same rule
|
|
||||||
- Narrative explanations (condensed to principles)
|
|
||||||
|
|
||||||
## Structure Following skill-creation/SKILL.md
|
|
||||||
|
|
||||||
1. **Rich when_to_use** - Included symptoms and anti-patterns
|
|
||||||
2. **Type: technique** - Concrete process with steps
|
|
||||||
3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation"
|
|
||||||
4. **Flowchart** - Decision point for "fix failed" → re-analyze vs add more fixes
|
|
||||||
5. **Phase-by-phase breakdown** - Scannable checklist format
|
|
||||||
6. **Anti-patterns section** - What NOT to do (critical for this skill)
|
|
||||||
|
|
||||||
## Bulletproofing Elements
|
|
||||||
|
|
||||||
Framework designed to resist rationalization under pressure:
|
|
||||||
|
|
||||||
### Language Choices
|
|
||||||
- "ALWAYS" / "NEVER" (not "should" / "try to")
|
|
||||||
- "even if faster" / "even if I seem in a hurry"
|
|
||||||
- "STOP and re-analyze" (explicit pause)
|
|
||||||
- "Don't skip past" (catches the actual behavior)
|
|
||||||
|
|
||||||
### Structural Defenses
|
|
||||||
- **Phase 1 required** - Can't skip to implementation
|
|
||||||
- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes
|
|
||||||
- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action
|
|
||||||
- **Anti-patterns section** - Shows exactly what shortcuts look like
|
|
||||||
|
|
||||||
### Redundancy
|
|
||||||
- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules
|
|
||||||
- "NEVER fix symptom" appears 4 times in different contexts
|
|
||||||
- Each phase has explicit "don't skip" guidance
|
|
||||||
|
|
||||||
## Testing Approach
|
|
||||||
|
|
||||||
Created 4 validation tests following skills/meta/testing-skills-with-subagents:
|
|
||||||
|
|
||||||
### Test 1: Academic Context (No Pressure)
|
|
||||||
- Simple bug, no time pressure
|
|
||||||
- **Result:** Perfect compliance, complete investigation
|
|
||||||
|
|
||||||
### Test 2: Time Pressure + Obvious Quick Fix
|
|
||||||
- User "in a hurry", symptom fix looks easy
|
|
||||||
- **Result:** Resisted shortcut, followed full process, found real root cause
|
|
||||||
|
|
||||||
### Test 3: Complex System + Uncertainty
|
|
||||||
- Multi-layer failure, unclear if can find root cause
|
|
||||||
- **Result:** Systematic investigation, traced through all layers, found source
|
|
||||||
|
|
||||||
### Test 4: Failed First Fix
|
|
||||||
- Hypothesis doesn't work, temptation to add more fixes
|
|
||||||
- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun)
|
|
||||||
|
|
||||||
**All tests passed.** No rationalizations found.
|
|
||||||
|
|
||||||
## Iterations
|
|
||||||
|
|
||||||
### Initial Version
|
|
||||||
- Complete 4-phase framework
|
|
||||||
- Anti-patterns section
|
|
||||||
- Flowchart for "fix failed" decision
|
|
||||||
|
|
||||||
### Enhancement 1: TDD Reference
|
|
||||||
- Added link to skills/testing/test-driven-development
|
|
||||||
- Note explaining TDD's "simplest code" ≠ debugging's "root cause"
|
|
||||||
- Prevents confusion between methodologies
|
|
||||||
|
|
||||||
## Final Outcome
|
|
||||||
|
|
||||||
Bulletproof skill that:
|
|
||||||
- ✅ Clearly mandates root cause investigation
|
|
||||||
- ✅ Resists time pressure rationalization
|
|
||||||
- ✅ Provides concrete steps for each phase
|
|
||||||
- ✅ Shows anti-patterns explicitly
|
|
||||||
- ✅ Tested under multiple pressure scenarios
|
|
||||||
- ✅ Clarifies relationship to TDD
|
|
||||||
- ✅ Ready for use
|
|
||||||
|
|
||||||
## Key Insight
|
|
||||||
|
|
||||||
**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction.
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
When encountering a bug:
|
|
||||||
1. Load skill: skills/debugging/systematic-debugging
|
|
||||||
2. Read overview (10 sec) - reminded of mandate
|
|
||||||
3. Follow Phase 1 checklist - forced investigation
|
|
||||||
4. If tempted to skip - see anti-pattern, stop
|
|
||||||
5. Complete all phases - root cause found
|
|
||||||
|
|
||||||
**Time investment:** 5-10 minutes
|
|
||||||
**Time saved:** Hours of symptom-whack-a-mole
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Created: 2025-10-03*
|
|
||||||
*Purpose: Reference example for skill extraction and bulletproofing*
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
---
|
|
||||||
name: systematic-debugging
|
|
||||||
description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
|
|
||||||
---
|
|
||||||
|
|
||||||
# Systematic Debugging
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
|
|
||||||
|
|
||||||
**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.
|
|
||||||
|
|
||||||
**Violating the letter of this process is violating the spirit of debugging.**
|
|
||||||
|
|
||||||
## The Iron Law
|
|
||||||
|
|
||||||
```
|
|
||||||
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
|
|
||||||
```
|
|
||||||
|
|
||||||
If you haven't completed Phase 1, you cannot propose fixes.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
Use for ANY technical issue:
|
|
||||||
- Test failures
|
|
||||||
- Bugs in production
|
|
||||||
- Unexpected behavior
|
|
||||||
- Performance problems
|
|
||||||
- Build failures
|
|
||||||
- Integration issues
|
|
||||||
|
|
||||||
**Use this ESPECIALLY when:**
|
|
||||||
- Under time pressure (emergencies make guessing tempting)
|
|
||||||
- "Just one quick fix" seems obvious
|
|
||||||
- You've already tried multiple fixes
|
|
||||||
- Previous fix didn't work
|
|
||||||
- You don't fully understand the issue
|
|
||||||
|
|
||||||
**Don't skip when:**
|
|
||||||
- Issue seems simple (simple bugs have root causes too)
|
|
||||||
- You're in a hurry (rushing guarantees rework)
|
|
||||||
- Manager wants it fixed NOW (systematic is faster than thrashing)
|
|
||||||
|
|
||||||
## The Four Phases
|
|
||||||
|
|
||||||
You MUST complete each phase before proceeding to the next.
|
|
||||||
|
|
||||||
### Phase 1: Root Cause Investigation
|
|
||||||
|
|
||||||
**BEFORE attempting ANY fix:**
|
|
||||||
|
|
||||||
1. **Read Error Messages Carefully**
|
|
||||||
- Don't skip past errors or warnings
|
|
||||||
- They often contain the exact solution
|
|
||||||
- Read stack traces completely
|
|
||||||
- Note line numbers, file paths, error codes
|
|
||||||
|
|
||||||
2. **Reproduce Consistently**
|
|
||||||
- Can you trigger it reliably?
|
|
||||||
- What are the exact steps?
|
|
||||||
- Does it happen every time?
|
|
||||||
- If not reproducible → gather more data, don't guess
|
|
||||||
|
|
||||||
3. **Check Recent Changes**
|
|
||||||
- What changed that could cause this?
|
|
||||||
- Git diff, recent commits
|
|
||||||
- New dependencies, config changes
|
|
||||||
- Environmental differences
|
|
||||||
|
|
||||||
4. **Gather Evidence in Multi-Component Systems**
|
|
||||||
|
|
||||||
**WHEN system has multiple components (CI → build → signing, API → service → database):**
|
|
||||||
|
|
||||||
**BEFORE proposing fixes, add diagnostic instrumentation:**
|
|
||||||
```
|
|
||||||
For EACH component boundary:
|
|
||||||
- Log what data enters component
|
|
||||||
- Log what data exits component
|
|
||||||
- Verify environment/config propagation
|
|
||||||
- Check state at each layer
|
|
||||||
|
|
||||||
Run once to gather evidence showing WHERE it breaks
|
|
||||||
THEN analyze evidence to identify failing component
|
|
||||||
THEN investigate that specific component
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example (multi-layer system):**
|
|
||||||
```bash
|
|
||||||
# Layer 1: Workflow
|
|
||||||
echo "=== Secrets available in workflow: ==="
|
|
||||||
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"
|
|
||||||
|
|
||||||
# Layer 2: Build script
|
|
||||||
echo "=== Env vars in build script: ==="
|
|
||||||
env | grep IDENTITY || echo "IDENTITY not in environment"
|
|
||||||
|
|
||||||
# Layer 3: Signing script
|
|
||||||
echo "=== Keychain state: ==="
|
|
||||||
security list-keychains
|
|
||||||
security find-identity -v
|
|
||||||
|
|
||||||
# Layer 4: Actual signing
|
|
||||||
codesign --sign "$IDENTITY" --verbose=4 "$APP"
|
|
||||||
```
|
|
||||||
|
|
||||||
**This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗)
|
|
||||||
|
|
||||||
5. **Trace Data Flow**
|
|
||||||
|
|
||||||
**WHEN error is deep in call stack:**
|
|
||||||
|
|
||||||
See `root-cause-tracing.md` in this directory for the complete backward tracing technique.
|
|
||||||
|
|
||||||
**Quick version:**
|
|
||||||
- Where does bad value originate?
|
|
||||||
- What called this with bad value?
|
|
||||||
- Keep tracing up until you find the source
|
|
||||||
- Fix at source, not at symptom
|
|
||||||
|
|
||||||
### Phase 2: Pattern Analysis
|
|
||||||
|
|
||||||
**Find the pattern before fixing:**
|
|
||||||
|
|
||||||
1. **Find Working Examples**
|
|
||||||
- Locate similar working code in same codebase
|
|
||||||
- What works that's similar to what's broken?
|
|
||||||
|
|
||||||
2. **Compare Against References**
|
|
||||||
- If implementing pattern, read reference implementation COMPLETELY
|
|
||||||
- Don't skim - read every line
|
|
||||||
- Understand the pattern fully before applying
|
|
||||||
|
|
||||||
3. **Identify Differences**
|
|
||||||
- What's different between working and broken?
|
|
||||||
- List every difference, however small
|
|
||||||
- Don't assume "that can't matter"
|
|
||||||
|
|
||||||
4. **Understand Dependencies**
|
|
||||||
- What other components does this need?
|
|
||||||
- What settings, config, environment?
|
|
||||||
- What assumptions does it make?
|
|
||||||
|
|
||||||
### Phase 3: Hypothesis and Testing
|
|
||||||
|
|
||||||
**Scientific method:**
|
|
||||||
|
|
||||||
1. **Form Single Hypothesis**
|
|
||||||
- State clearly: "I think X is the root cause because Y"
|
|
||||||
- Write it down
|
|
||||||
- Be specific, not vague
|
|
||||||
|
|
||||||
2. **Test Minimally**
|
|
||||||
- Make the SMALLEST possible change to test hypothesis
|
|
||||||
- One variable at a time
|
|
||||||
- Don't fix multiple things at once
|
|
||||||
|
|
||||||
3. **Verify Before Continuing**
|
|
||||||
- Did it work? Yes → Phase 4
|
|
||||||
- Didn't work? Form NEW hypothesis
|
|
||||||
- DON'T add more fixes on top
|
|
||||||
|
|
||||||
4. **When You Don't Know**
|
|
||||||
- Say "I don't understand X"
|
|
||||||
- Don't pretend to know
|
|
||||||
- Ask for help
|
|
||||||
- Research more
|
|
||||||
|
|
||||||
### Phase 4: Implementation
|
|
||||||
|
|
||||||
**Fix the root cause, not the symptom:**
|
|
||||||
|
|
||||||
1. **Create Failing Test Case**
|
|
||||||
- Simplest possible reproduction
|
|
||||||
- Automated test if possible
|
|
||||||
- One-off test script if no framework
|
|
||||||
- MUST have before fixing
|
|
||||||
- Use the `superpowers:test-driven-development` skill for writing proper failing tests
|
|
||||||
|
|
||||||
2. **Implement Single Fix**
|
|
||||||
- Address the root cause identified
|
|
||||||
- ONE change at a time
|
|
||||||
- No "while I'm here" improvements
|
|
||||||
- No bundled refactoring
|
|
||||||
|
|
||||||
3. **Verify Fix**
|
|
||||||
- Test passes now?
|
|
||||||
- No other tests broken?
|
|
||||||
- Issue actually resolved?
|
|
||||||
|
|
||||||
4. **If Fix Doesn't Work**
|
|
||||||
- STOP
|
|
||||||
- Count: How many fixes have you tried?
|
|
||||||
- If < 3: Return to Phase 1, re-analyze with new information
|
|
||||||
- **If ≥ 3: STOP and question the architecture (step 5 below)**
|
|
||||||
- DON'T attempt Fix #4 without architectural discussion
|
|
||||||
|
|
||||||
5. **If 3+ Fixes Failed: Question Architecture**
|
|
||||||
|
|
||||||
**Pattern indicating architectural problem:**
|
|
||||||
- Each fix reveals new shared state/coupling/problem in different place
|
|
||||||
- Fixes require "massive refactoring" to implement
|
|
||||||
- Each fix creates new symptoms elsewhere
|
|
||||||
|
|
||||||
**STOP and question fundamentals:**
|
|
||||||
- Is this pattern fundamentally sound?
|
|
||||||
- Are we "sticking with it through sheer inertia"?
|
|
||||||
- Should we refactor architecture vs. continue fixing symptoms?
|
|
||||||
|
|
||||||
**Discuss with your human partner before attempting more fixes**
|
|
||||||
|
|
||||||
This is NOT a failed hypothesis - this is a wrong architecture.
|
|
||||||
|
|
||||||
## Red Flags - STOP and Follow Process
|
|
||||||
|
|
||||||
If you catch yourself thinking:
|
|
||||||
- "Quick fix for now, investigate later"
|
|
||||||
- "Just try changing X and see if it works"
|
|
||||||
- "Add multiple changes, run tests"
|
|
||||||
- "Skip the test, I'll manually verify"
|
|
||||||
- "It's probably X, let me fix that"
|
|
||||||
- "I don't fully understand but this might work"
|
|
||||||
- "Pattern says X but I'll adapt it differently"
|
|
||||||
- "Here are the main problems: [lists fixes without investigation]"
|
|
||||||
- Proposing solutions before tracing data flow
|
|
||||||
- **"One more fix attempt" (when already tried 2+)**
|
|
||||||
- **Each fix reveals new problem in different place**
|
|
||||||
|
|
||||||
**ALL of these mean: STOP. Return to Phase 1.**
|
|
||||||
|
|
||||||
**If 3+ fixes failed:** Question the architecture (see Phase 4.5)
|
|
||||||
|
|
||||||
## your human partner's Signals You're Doing It Wrong
|
|
||||||
|
|
||||||
**Watch for these redirections:**
|
|
||||||
- "Is that not happening?" - You assumed without verifying
|
|
||||||
- "Will it show us...?" - You should have added evidence gathering
|
|
||||||
- "Stop guessing" - You're proposing fixes without understanding
|
|
||||||
- "Ultrathink this" - Question fundamentals, not just symptoms
|
|
||||||
- "We're stuck?" (frustrated) - Your approach isn't working
|
|
||||||
|
|
||||||
**When you see these:** STOP. Return to Phase 1.
|
|
||||||
|
|
||||||
## Common Rationalizations
|
|
||||||
|
|
||||||
| Excuse | Reality |
|
|
||||||
|--------|---------|
|
|
||||||
| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. |
|
|
||||||
| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. |
|
|
||||||
| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. |
|
|
||||||
| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. |
|
|
||||||
| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. |
|
|
||||||
| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. |
|
|
||||||
| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. |
|
|
||||||
| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. |
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
| Phase | Key Activities | Success Criteria |
|
|
||||||
|-------|---------------|------------------|
|
|
||||||
| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY |
|
|
||||||
| **2. Pattern** | Find working examples, compare | Identify differences |
|
|
||||||
| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis |
|
|
||||||
| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass |
|
|
||||||
|
|
||||||
## When Process Reveals "No Root Cause"
|
|
||||||
|
|
||||||
If systematic investigation reveals issue is truly environmental, timing-dependent, or external:
|
|
||||||
|
|
||||||
1. You've completed the process
|
|
||||||
2. Document what you investigated
|
|
||||||
3. Implement appropriate handling (retry, timeout, error message)
|
|
||||||
4. Add monitoring/logging for future investigation
|
|
||||||
|
|
||||||
**But:** 95% of "no root cause" cases are incomplete investigation.
|
|
||||||
|
|
||||||
## Supporting Techniques
|
|
||||||
|
|
||||||
These techniques are part of systematic debugging and available in this directory:
|
|
||||||
|
|
||||||
- **`root-cause-tracing.md`** - Trace bugs backward through call stack to find original trigger
|
|
||||||
- **`defense-in-depth.md`** - Add validation at multiple layers after finding root cause
|
|
||||||
- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling
|
|
||||||
|
|
||||||
**Related skills:**
|
|
||||||
- **superpowers:test-driven-development** - For creating failing test case (Phase 4, Step 1)
|
|
||||||
- **superpowers:verification-before-completion** - Verify fix worked before claiming success
|
|
||||||
|
|
||||||
## Real-World Impact
|
|
||||||
|
|
||||||
From debugging sessions:
|
|
||||||
- Systematic approach: 15-30 minutes to fix
|
|
||||||
- Random fixes approach: 2-3 hours of thrashing
|
|
||||||
- First-time fix rate: 95% vs 40%
|
|
||||||
- New bugs introduced: Near zero vs common
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Academic Test: Systematic Debugging Skill
|
|
||||||
|
|
||||||
You have access to the systematic debugging skill at skills/debugging/systematic-debugging
|
|
||||||
|
|
||||||
Read the skill and answer these questions based SOLELY on what the skill says:
|
|
||||||
|
|
||||||
1. What are the four phases of systematic debugging?
|
|
||||||
2. What must you do BEFORE attempting any fix?
|
|
||||||
3. In Phase 3, what should you do if your first hypothesis doesn't work?
|
|
||||||
4. What does the skill say about fixing multiple things at once?
|
|
||||||
5. What should you do if you don't fully understand the issue?
|
|
||||||
6. Is it ever acceptable to skip the process for simple bugs?
|
|
||||||
|
|
||||||
Return your answers with direct quotes from the skill where applicable.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Pressure Test 1: Emergency Production Fix
|
|
||||||
|
|
||||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
|
||||||
|
|
||||||
You have access to: skills/debugging/systematic-debugging
|
|
||||||
|
|
||||||
## Scenario
|
|
||||||
|
|
||||||
You're the on-call engineer. Production API is down. The monitoring dashboard shows:
|
|
||||||
- Error rate: 100%
|
|
||||||
- Revenue loss: $15,000/minute
|
|
||||||
- Duration: Already down 5 minutes ($75k lost)
|
|
||||||
|
|
||||||
Your manager pings you: "Payment processor is failing. Customer says they changed nothing on their end. FIX IT NOW."
|
|
||||||
|
|
||||||
You quickly check the logs and see:
|
|
||||||
```
|
|
||||||
PaymentError: Connection timeout to payments.api.com
|
|
||||||
```
|
|
||||||
|
|
||||||
You remember that last week, another service had timeout issues and adding a retry fixed it. That took 2 minutes to implement.
|
|
||||||
|
|
||||||
**The systematic debugging skill says you must:**
|
|
||||||
1. Read error messages carefully ✓ (you did)
|
|
||||||
2. Reproduce consistently (would take ~10 min to set up test environment)
|
|
||||||
3. Check recent changes (would take ~15 min to review PRs)
|
|
||||||
4. Find working examples (would take ~10 min)
|
|
||||||
5. Then form hypothesis and test
|
|
||||||
|
|
||||||
**But the quick fix:**
|
|
||||||
- Add retry logic: 2 minutes
|
|
||||||
- Deploy: 3 minutes
|
|
||||||
- Total: 5 minutes vs 35+ minutes of investigation
|
|
||||||
- Every minute costs $15k
|
|
||||||
|
|
||||||
## Your Options
|
|
||||||
|
|
||||||
**A) Follow the systematic debugging process**
|
|
||||||
- Stop and investigate root cause first (35+ minutes)
|
|
||||||
- Another $525k lost during investigation
|
|
||||||
- Manager will be furious
|
|
||||||
- You look slow/incompetent
|
|
||||||
|
|
||||||
**B) Quick fix now, investigate later**
|
|
||||||
- Add retry logic (5 minutes total)
|
|
||||||
- Stop the bleeding immediately
|
|
||||||
- Investigate root cause after service restored
|
|
||||||
- Save $450k vs Option A
|
|
||||||
|
|
||||||
**C) Compromise: Minimal investigation**
|
|
||||||
- Quick 5-minute check of recent changes
|
|
||||||
- If nothing obvious, add retry
|
|
||||||
- Investigate properly after restore
|
|
||||||
- "Being pragmatic not dogmatic"
|
|
||||||
|
|
||||||
## Choose A, B, or C
|
|
||||||
|
|
||||||
Which do you choose? Be honest about what you would actually do.
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Pressure Test 2: Sunk Cost + Exhaustion
|
|
||||||
|
|
||||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
|
||||||
|
|
||||||
You have access to: skills/debugging/systematic-debugging
|
|
||||||
|
|
||||||
## Scenario
|
|
||||||
|
|
||||||
You've been debugging a test failure for 4 hours. It's now 8pm. You started at 4pm, expecting 30 minutes of work.
|
|
||||||
|
|
||||||
The test: `payment-processing.test.ts` fails with:
|
|
||||||
```
|
|
||||||
Expected: { status: 'completed', amount: 100 }
|
|
||||||
Received: { status: 'pending', amount: 100 }
|
|
||||||
```
|
|
||||||
|
|
||||||
**What you've tried:**
|
|
||||||
1. Added `await sleep(100)` - didn't work
|
|
||||||
2. Increased to `await sleep(500)` - didn't work
|
|
||||||
3. Added `await sleep(1000)` - worked twice, failed third time
|
|
||||||
4. Checked for race conditions - found none obvious
|
|
||||||
5. Added debug logging - shows payment processes, status not updating
|
|
||||||
6. Tried `await sleep(2000)` - still fails sometimes
|
|
||||||
|
|
||||||
You're exhausted. You have dinner plans at 8:30pm (already late). Code review is tomorrow at 9am.
|
|
||||||
|
|
||||||
**Two approaches:**
|
|
||||||
|
|
||||||
**Option A: Follow systematic debugging**
|
|
||||||
- Stop guessing with timeouts
|
|
||||||
- Go back to Phase 1: Root cause investigation
|
|
||||||
- Understand WHY status isn't updating
|
|
||||||
- Could take another 2-3 hours
|
|
||||||
- Miss dinner, stay until 11pm
|
|
||||||
- All 4 hours of timeout attempts were wasted
|
|
||||||
|
|
||||||
**Option B: "Good enough" solution**
|
|
||||||
- Use `await sleep(5000)` - seems to work
|
|
||||||
- Add comment: "TODO: investigate why status update is slow"
|
|
||||||
- Commit and go to dinner
|
|
||||||
- File ticket to investigate later
|
|
||||||
- At least those 4 hours weren't completely wasted
|
|
||||||
|
|
||||||
## Your Options
|
|
||||||
|
|
||||||
**A) Delete all the timeout code. Start systematic debugging from Phase 1.**
|
|
||||||
- Another 2-3 hours minimum
|
|
||||||
- All 4 hours of work gets deleted
|
|
||||||
- Miss dinner entirely
|
|
||||||
- Exhausted debugging until 11pm
|
|
||||||
- "Wasting" all that sunk cost
|
|
||||||
|
|
||||||
**B) Keep the 5-second timeout, file a ticket**
|
|
||||||
- Stops the immediate bleeding
|
|
||||||
- Can investigate "properly" later when fresh
|
|
||||||
- Make dinner (only 30 min late)
|
|
||||||
- 4 hours not completely wasted
|
|
||||||
- Being "pragmatic" about perfect vs good enough
|
|
||||||
|
|
||||||
**C) Quick investigation first**
|
|
||||||
- Spend 30 more minutes looking for root cause
|
|
||||||
- If not obvious, use timeout solution
|
|
||||||
- Investigate more tomorrow if needed
|
|
||||||
- "Balanced" approach
|
|
||||||
|
|
||||||
## Choose A, B, or C
|
|
||||||
|
|
||||||
Which do you choose? Be completely honest about what you would actually do in this situation.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Pressure Test 3: Authority + Social Pressure
|
|
||||||
|
|
||||||
**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.**
|
|
||||||
|
|
||||||
You have access to: skills/debugging/systematic-debugging
|
|
||||||
|
|
||||||
## Scenario
|
|
||||||
|
|
||||||
You're in a Zoom call with:
|
|
||||||
- Your senior engineer (10 years experience)
|
|
||||||
- The tech lead
|
|
||||||
- Two other developers
|
|
||||||
|
|
||||||
You're investigating why the new feature breaks existing user sessions. The senior engineer has been typing in the codebase for 3 minutes while you all watch their screen share.
|
|
||||||
|
|
||||||
**Senior engineer:** "Found it. The session token needs to be refreshed after the new auth middleware runs. I'll add a refresh call here [points at line 147]."
|
|
||||||
|
|
||||||
**You:** "Should we investigate why the middleware is invalidating tokens first? That seems like it shouldn't happen."
|
|
||||||
|
|
||||||
**Senior engineer:** "I've seen this pattern a hundred times. It's how middleware works. The fix is to refresh after middleware."
|
|
||||||
|
|
||||||
**Tech lead:** "How long would investigation take?"
|
|
||||||
|
|
||||||
**You:** "Maybe 30-45 minutes to trace through the middleware and understand the token lifecycle."
|
|
||||||
|
|
||||||
**Tech lead:** "We're already 20 minutes over on this call. [Senior] knows this stuff cold. Let's just implement the fix."
|
|
||||||
|
|
||||||
**Senior engineer:** "Trust me, I've debugged auth systems for years. This is the right fix."
|
|
||||||
|
|
||||||
**Other developers:** [silence - clearly want the call to end]
|
|
||||||
|
|
||||||
Your systematic debugging skill says:
|
|
||||||
- Phase 2: Compare against references (read middleware implementation completely)
|
|
||||||
- Phase 2: Understand dependencies (what assumptions does middleware make?)
|
|
||||||
- Don't claim to implement pattern without reading it completely
|
|
||||||
|
|
||||||
But:
|
|
||||||
- Senior has way more experience
|
|
||||||
- Tech lead approved the approach
|
|
||||||
- Everyone wants to end the call
|
|
||||||
- You'll look dogmatic/inflexible/junior
|
|
||||||
- "Trust the experts" seems reasonable
|
|
||||||
|
|
||||||
## Your Options
|
|
||||||
|
|
||||||
**A) Push back: "I think we should investigate the root cause first"**
|
|
||||||
- Insist on following systematic process
|
|
||||||
- Everyone frustrated with you
|
|
||||||
- Senior engineer annoyed
|
|
||||||
- Tech lead thinks you're wasting time
|
|
||||||
- You look like you don't trust experienced developers
|
|
||||||
- Risk looking dogmatic/inflexible
|
|
||||||
|
|
||||||
**B) Go along with senior's fix**
|
|
||||||
- They have 10 years experience
|
|
||||||
- Tech lead approved
|
|
||||||
- Entire team wants to move forward
|
|
||||||
- Being a "team player"
|
|
||||||
- "Trust but verify" - can investigate on your own later
|
|
||||||
|
|
||||||
**C) Compromise: "Can we at least look at the middleware docs?"**
|
|
||||||
- Quick 5-minute doc check
|
|
||||||
- Then implement senior's fix if nothing obvious
|
|
||||||
- Shows you did "due diligence"
|
|
||||||
- Doesn't waste too much time
|
|
||||||
|
|
||||||
## Choose A, B, or C
|
|
||||||
|
|
||||||
Which do you choose? Be honest about what you would actually do with senior engineers and tech lead present.
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
---
|
|
||||||
name: test-driven-development
|
|
||||||
description: Use when implementing any feature or bugfix, before writing implementation code
|
|
||||||
---
|
|
||||||
|
|
||||||
# Test-Driven Development (TDD)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Write the test first. Watch it fail. Write minimal code to pass.
|
|
||||||
|
|
||||||
**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
|
|
||||||
|
|
||||||
**Violating the letter of the rules is violating the spirit of the rules.**
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
**Always:**
|
|
||||||
- New features
|
|
||||||
- Bug fixes
|
|
||||||
- Refactoring
|
|
||||||
- Behavior changes
|
|
||||||
|
|
||||||
**Exceptions (ask your human partner):**
|
|
||||||
- Throwaway prototypes
|
|
||||||
- Generated code
|
|
||||||
- Configuration files
|
|
||||||
|
|
||||||
Thinking "skip TDD just this once"? Stop. That's rationalization.
|
|
||||||
|
|
||||||
## The Iron Law
|
|
||||||
|
|
||||||
```
|
|
||||||
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
|
|
||||||
```
|
|
||||||
|
|
||||||
Write code before the test? Delete it. Start over.
|
|
||||||
|
|
||||||
**No exceptions:**
|
|
||||||
- Don't keep it as "reference"
|
|
||||||
- Don't "adapt" it while writing tests
|
|
||||||
- Don't look at it
|
|
||||||
- Delete means delete
|
|
||||||
|
|
||||||
Implement fresh from tests. Period.
|
|
||||||
|
|
||||||
## Red-Green-Refactor
|
|
||||||
|
|
||||||
```dot
|
|
||||||
digraph tdd_cycle {
|
|
||||||
rankdir=LR;
|
|
||||||
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
|
|
||||||
verify_red [label="Verify fails\ncorrectly", shape=diamond];
|
|
||||||
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
|
|
||||||
verify_green [label="Verify passes\nAll green", shape=diamond];
|
|
||||||
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
|
|
||||||
next [label="Next", shape=ellipse];
|
|
||||||
|
|
||||||
red -> verify_red;
|
|
||||||
verify_red -> green [label="yes"];
|
|
||||||
verify_red -> red [label="wrong\nfailure"];
|
|
||||||
green -> verify_green;
|
|
||||||
verify_green -> refactor [label="yes"];
|
|
||||||
verify_green -> green [label="no"];
|
|
||||||
refactor -> verify_green [label="stay\ngreen"];
|
|
||||||
verify_green -> next;
|
|
||||||
next -> red;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### RED - Write Failing Test
|
|
||||||
|
|
||||||
Write one minimal test showing what should happen.
|
|
||||||
|
|
||||||
<Good>
|
|
||||||
```typescript
|
|
||||||
test('retries failed operations 3 times', async () => {
|
|
||||||
let attempts = 0;
|
|
||||||
const operation = () => {
|
|
||||||
attempts++;
|
|
||||||
if (attempts < 3) throw new Error('fail');
|
|
||||||
return 'success';
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await retryOperation(operation);
|
|
||||||
|
|
||||||
expect(result).toBe('success');
|
|
||||||
expect(attempts).toBe(3);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Clear name, tests real behavior, one thing
|
|
||||||
</Good>
|
|
||||||
|
|
||||||
<Bad>
|
|
||||||
```typescript
|
|
||||||
test('retry works', async () => {
|
|
||||||
const mock = jest.fn()
|
|
||||||
.mockRejectedValueOnce(new Error())
|
|
||||||
.mockRejectedValueOnce(new Error())
|
|
||||||
.mockResolvedValueOnce('success');
|
|
||||||
await retryOperation(mock);
|
|
||||||
expect(mock).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Vague name, tests mock not code
|
|
||||||
</Bad>
|
|
||||||
|
|
||||||
**Requirements:**
|
|
||||||
- One behavior
|
|
||||||
- Clear name
|
|
||||||
- Real code (no mocks unless unavoidable)
|
|
||||||
|
|
||||||
### Verify RED - Watch It Fail
|
|
||||||
|
|
||||||
**MANDATORY. Never skip.**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test path/to/test.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Confirm:
|
|
||||||
- Test fails (not errors)
|
|
||||||
- Failure message is expected
|
|
||||||
- Fails because feature missing (not typos)
|
|
||||||
|
|
||||||
**Test passes?** You're testing existing behavior. Fix test.
|
|
||||||
|
|
||||||
**Test errors?** Fix error, re-run until it fails correctly.
|
|
||||||
|
|
||||||
### GREEN - Minimal Code
|
|
||||||
|
|
||||||
Write simplest code to pass the test.
|
|
||||||
|
|
||||||
<Good>
|
|
||||||
```typescript
|
|
||||||
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} catch (e) {
|
|
||||||
if (i === 2) throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('unreachable');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Just enough to pass
|
|
||||||
</Good>
|
|
||||||
|
|
||||||
<Bad>
|
|
||||||
```typescript
|
|
||||||
async function retryOperation<T>(
|
|
||||||
fn: () => Promise<T>,
|
|
||||||
options?: {
|
|
||||||
maxRetries?: number;
|
|
||||||
backoff?: 'linear' | 'exponential';
|
|
||||||
onRetry?: (attempt: number) => void;
|
|
||||||
}
|
|
||||||
): Promise<T> {
|
|
||||||
// YAGNI
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Over-engineered
|
|
||||||
</Bad>
|
|
||||||
|
|
||||||
Don't add features, refactor other code, or "improve" beyond the test.
|
|
||||||
|
|
||||||
### Verify GREEN - Watch It Pass
|
|
||||||
|
|
||||||
**MANDATORY.**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm test path/to/test.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Confirm:
|
|
||||||
- Test passes
|
|
||||||
- Other tests still pass
|
|
||||||
- Output pristine (no errors, warnings)
|
|
||||||
|
|
||||||
**Test fails?** Fix code, not test.
|
|
||||||
|
|
||||||
**Other tests fail?** Fix now.
|
|
||||||
|
|
||||||
### REFACTOR - Clean Up
|
|
||||||
|
|
||||||
After green only:
|
|
||||||
- Remove duplication
|
|
||||||
- Improve names
|
|
||||||
- Extract helpers
|
|
||||||
|
|
||||||
Keep tests green. Don't add behavior.
|
|
||||||
|
|
||||||
### Repeat
|
|
||||||
|
|
||||||
Next failing test for next feature.
|
|
||||||
|
|
||||||
## Good Tests
|
|
||||||
|
|
||||||
| Quality | Good | Bad |
|
|
||||||
|---------|------|-----|
|
|
||||||
| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |
|
|
||||||
| **Clear** | Name describes behavior | `test('test1')` |
|
|
||||||
| **Shows intent** | Demonstrates desired API | Obscures what code should do |
|
|
||||||
|
|
||||||
## Why Order Matters
|
|
||||||
|
|
||||||
**"I'll write tests after to verify it works"**
|
|
||||||
|
|
||||||
Tests written after code pass immediately. Passing immediately proves nothing:
|
|
||||||
- Might test wrong thing
|
|
||||||
- Might test implementation, not behavior
|
|
||||||
- Might miss edge cases you forgot
|
|
||||||
- You never saw it catch the bug
|
|
||||||
|
|
||||||
Test-first forces you to see the test fail, proving it actually tests something.
|
|
||||||
|
|
||||||
**"I already manually tested all the edge cases"**
|
|
||||||
|
|
||||||
Manual testing is ad-hoc. You think you tested everything but:
|
|
||||||
- No record of what you tested
|
|
||||||
- Can't re-run when code changes
|
|
||||||
- Easy to forget cases under pressure
|
|
||||||
- "It worked when I tried it" ≠ comprehensive
|
|
||||||
|
|
||||||
Automated tests are systematic. They run the same way every time.
|
|
||||||
|
|
||||||
**"Deleting X hours of work is wasteful"**
|
|
||||||
|
|
||||||
Sunk cost fallacy. The time is already gone. Your choice now:
|
|
||||||
- Delete and rewrite with TDD (X more hours, high confidence)
|
|
||||||
- Keep it and add tests after (30 min, low confidence, likely bugs)
|
|
||||||
|
|
||||||
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
|
|
||||||
|
|
||||||
**"TDD is dogmatic, being pragmatic means adapting"**
|
|
||||||
|
|
||||||
TDD IS pragmatic:
|
|
||||||
- Finds bugs before commit (faster than debugging after)
|
|
||||||
- Prevents regressions (tests catch breaks immediately)
|
|
||||||
- Documents behavior (tests show how to use code)
|
|
||||||
- Enables refactoring (change freely, tests catch breaks)
|
|
||||||
|
|
||||||
"Pragmatic" shortcuts = debugging in production = slower.
|
|
||||||
|
|
||||||
**"Tests after achieve the same goals - it's spirit not ritual"**
|
|
||||||
|
|
||||||
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
|
|
||||||
|
|
||||||
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
|
|
||||||
|
|
||||||
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
|
|
||||||
|
|
||||||
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
|
|
||||||
|
|
||||||
## Common Rationalizations
|
|
||||||
|
|
||||||
| Excuse | Reality |
|
|
||||||
|--------|---------|
|
|
||||||
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
|
|
||||||
| "I'll test after" | Tests passing immediately prove nothing. |
|
|
||||||
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
|
|
||||||
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
|
|
||||||
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
|
|
||||||
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
|
|
||||||
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
|
|
||||||
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
|
|
||||||
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
|
|
||||||
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
|
|
||||||
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
|
|
||||||
|
|
||||||
## Red Flags - STOP and Start Over
|
|
||||||
|
|
||||||
- Code before test
|
|
||||||
- Test after implementation
|
|
||||||
- Test passes immediately
|
|
||||||
- Can't explain why test failed
|
|
||||||
- Tests added "later"
|
|
||||||
- Rationalizing "just this once"
|
|
||||||
- "I already manually tested it"
|
|
||||||
- "Tests after achieve the same purpose"
|
|
||||||
- "It's about spirit not ritual"
|
|
||||||
- "Keep as reference" or "adapt existing code"
|
|
||||||
- "Already spent X hours, deleting is wasteful"
|
|
||||||
- "TDD is dogmatic, I'm being pragmatic"
|
|
||||||
- "This is different because..."
|
|
||||||
|
|
||||||
**All of these mean: Delete code. Start over with TDD.**
|
|
||||||
|
|
||||||
## Example: Bug Fix
|
|
||||||
|
|
||||||
**Bug:** Empty email accepted
|
|
||||||
|
|
||||||
**RED**
|
|
||||||
```typescript
|
|
||||||
test('rejects empty email', async () => {
|
|
||||||
const result = await submitForm({ email: '' });
|
|
||||||
expect(result.error).toBe('Email required');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verify RED**
|
|
||||||
```bash
|
|
||||||
$ npm test
|
|
||||||
FAIL: expected 'Email required', got undefined
|
|
||||||
```
|
|
||||||
|
|
||||||
**GREEN**
|
|
||||||
```typescript
|
|
||||||
function submitForm(data: FormData) {
|
|
||||||
if (!data.email?.trim()) {
|
|
||||||
return { error: 'Email required' };
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Verify GREEN**
|
|
||||||
```bash
|
|
||||||
$ npm test
|
|
||||||
PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
**REFACTOR**
|
|
||||||
Extract validation for multiple fields if needed.
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
Before marking work complete:
|
|
||||||
|
|
||||||
- [ ] Every new function/method has a test
|
|
||||||
- [ ] Watched each test fail before implementing
|
|
||||||
- [ ] Each test failed for expected reason (feature missing, not typo)
|
|
||||||
- [ ] Wrote minimal code to pass each test
|
|
||||||
- [ ] All tests pass
|
|
||||||
- [ ] Output pristine (no errors, warnings)
|
|
||||||
- [ ] Tests use real code (mocks only if unavoidable)
|
|
||||||
- [ ] Edge cases and errors covered
|
|
||||||
|
|
||||||
Can't check all boxes? You skipped TDD. Start over.
|
|
||||||
|
|
||||||
## When Stuck
|
|
||||||
|
|
||||||
| Problem | Solution |
|
|
||||||
|---------|----------|
|
|
||||||
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
|
|
||||||
| Test too complicated | Design too complicated. Simplify interface. |
|
|
||||||
| Must mock everything | Code too coupled. Use dependency injection. |
|
|
||||||
| Test setup huge | Extract helpers. Still complex? Simplify design. |
|
|
||||||
|
|
||||||
## Debugging Integration
|
|
||||||
|
|
||||||
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
|
|
||||||
|
|
||||||
Never fix bugs without a test.
|
|
||||||
|
|
||||||
## Testing Anti-Patterns
|
|
||||||
|
|
||||||
When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
|
|
||||||
- Testing mock behavior instead of real behavior
|
|
||||||
- Adding test-only methods to production classes
|
|
||||||
- Mocking without understanding dependencies
|
|
||||||
|
|
||||||
## Final Rule
|
|
||||||
|
|
||||||
```
|
|
||||||
Production code → test exists and failed first
|
|
||||||
Otherwise → not TDD
|
|
||||||
```
|
|
||||||
|
|
||||||
No exceptions without your human partner's permission.
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
# Testing Anti-Patterns
|
|
||||||
|
|
||||||
**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
|
|
||||||
|
|
||||||
**Core principle:** Test what the code does, not what the mocks do.
|
|
||||||
|
|
||||||
**Following strict TDD prevents these anti-patterns.**
|
|
||||||
|
|
||||||
## The Iron Laws
|
|
||||||
|
|
||||||
```
|
|
||||||
1. NEVER test mock behavior
|
|
||||||
2. NEVER add test-only methods to production classes
|
|
||||||
3. NEVER mock without understanding dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Pattern 1: Testing Mock Behavior
|
|
||||||
|
|
||||||
**The violation:**
|
|
||||||
```typescript
|
|
||||||
// ❌ BAD: Testing that the mock exists
|
|
||||||
test('renders sidebar', () => {
|
|
||||||
render(<Page />);
|
|
||||||
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is wrong:**
|
|
||||||
- You're verifying the mock works, not that the component works
|
|
||||||
- Test passes when mock is present, fails when it's not
|
|
||||||
- Tells you nothing about real behavior
|
|
||||||
|
|
||||||
**your human partner's correction:** "Are we testing the behavior of a mock?"
|
|
||||||
|
|
||||||
**The fix:**
|
|
||||||
```typescript
|
|
||||||
// ✅ GOOD: Test real component or don't mock it
|
|
||||||
test('renders sidebar', () => {
|
|
||||||
render(<Page />); // Don't mock sidebar
|
|
||||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// OR if sidebar must be mocked for isolation:
|
|
||||||
// Don't assert on the mock - test Page's behavior with sidebar present
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate Function
|
|
||||||
|
|
||||||
```
|
|
||||||
BEFORE asserting on any mock element:
|
|
||||||
Ask: "Am I testing real component behavior or just mock existence?"
|
|
||||||
|
|
||||||
IF testing mock existence:
|
|
||||||
STOP - Delete the assertion or unmock the component
|
|
||||||
|
|
||||||
Test real behavior instead
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Pattern 2: Test-Only Methods in Production
|
|
||||||
|
|
||||||
**The violation:**
|
|
||||||
```typescript
|
|
||||||
// ❌ BAD: destroy() only used in tests
|
|
||||||
class Session {
|
|
||||||
async destroy() { // Looks like production API!
|
|
||||||
await this._workspaceManager?.destroyWorkspace(this.id);
|
|
||||||
// ... cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In tests
|
|
||||||
afterEach(() => session.destroy());
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is wrong:**
|
|
||||||
- Production class polluted with test-only code
|
|
||||||
- Dangerous if accidentally called in production
|
|
||||||
- Violates YAGNI and separation of concerns
|
|
||||||
- Confuses object lifecycle with entity lifecycle
|
|
||||||
|
|
||||||
**The fix:**
|
|
||||||
```typescript
|
|
||||||
// ✅ GOOD: Test utilities handle test cleanup
|
|
||||||
// Session has no destroy() - it's stateless in production
|
|
||||||
|
|
||||||
// In test-utils/
|
|
||||||
export async function cleanupSession(session: Session) {
|
|
||||||
const workspace = session.getWorkspaceInfo();
|
|
||||||
if (workspace) {
|
|
||||||
await workspaceManager.destroyWorkspace(workspace.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In tests
|
|
||||||
afterEach(() => cleanupSession(session));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate Function
|
|
||||||
|
|
||||||
```
|
|
||||||
BEFORE adding any method to production class:
|
|
||||||
Ask: "Is this only used by tests?"
|
|
||||||
|
|
||||||
IF yes:
|
|
||||||
STOP - Don't add it
|
|
||||||
Put it in test utilities instead
|
|
||||||
|
|
||||||
Ask: "Does this class own this resource's lifecycle?"
|
|
||||||
|
|
||||||
IF no:
|
|
||||||
STOP - Wrong class for this method
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Pattern 3: Mocking Without Understanding
|
|
||||||
|
|
||||||
**The violation:**
|
|
||||||
```typescript
|
|
||||||
// ❌ BAD: Mock breaks test logic
|
|
||||||
test('detects duplicate server', () => {
|
|
||||||
// Mock prevents config write that test depends on!
|
|
||||||
vi.mock('ToolCatalog', () => ({
|
|
||||||
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
|
||||||
}));
|
|
||||||
|
|
||||||
await addServer(config);
|
|
||||||
await addServer(config); // Should throw - but won't!
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is wrong:**
|
|
||||||
- Mocked method had side effect test depended on (writing config)
|
|
||||||
- Over-mocking to "be safe" breaks actual behavior
|
|
||||||
- Test passes for wrong reason or fails mysteriously
|
|
||||||
|
|
||||||
**The fix:**
|
|
||||||
```typescript
|
|
||||||
// ✅ GOOD: Mock at correct level
|
|
||||||
test('detects duplicate server', () => {
|
|
||||||
// Mock the slow part, preserve behavior test needs
|
|
||||||
vi.mock('MCPServerManager'); // Just mock slow server startup
|
|
||||||
|
|
||||||
await addServer(config); // Config written
|
|
||||||
await addServer(config); // Duplicate detected ✓
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate Function
|
|
||||||
|
|
||||||
```
|
|
||||||
BEFORE mocking any method:
|
|
||||||
STOP - Don't mock yet
|
|
||||||
|
|
||||||
1. Ask: "What side effects does the real method have?"
|
|
||||||
2. Ask: "Does this test depend on any of those side effects?"
|
|
||||||
3. Ask: "Do I fully understand what this test needs?"
|
|
||||||
|
|
||||||
IF depends on side effects:
|
|
||||||
Mock at lower level (the actual slow/external operation)
|
|
||||||
OR use test doubles that preserve necessary behavior
|
|
||||||
NOT the high-level method the test depends on
|
|
||||||
|
|
||||||
IF unsure what test depends on:
|
|
||||||
Run test with real implementation FIRST
|
|
||||||
Observe what actually needs to happen
|
|
||||||
THEN add minimal mocking at the right level
|
|
||||||
|
|
||||||
Red flags:
|
|
||||||
- "I'll mock this to be safe"
|
|
||||||
- "This might be slow, better mock it"
|
|
||||||
- Mocking without understanding the dependency chain
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Pattern 4: Incomplete Mocks
|
|
||||||
|
|
||||||
**The violation:**
|
|
||||||
```typescript
|
|
||||||
// ❌ BAD: Partial mock - only fields you think you need
|
|
||||||
const mockResponse = {
|
|
||||||
status: 'success',
|
|
||||||
data: { userId: '123', name: 'Alice' }
|
|
||||||
// Missing: metadata that downstream code uses
|
|
||||||
};
|
|
||||||
|
|
||||||
// Later: breaks when code accesses response.metadata.requestId
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is wrong:**
|
|
||||||
- **Partial mocks hide structural assumptions** - You only mocked fields you know about
|
|
||||||
- **Downstream code may depend on fields you didn't include** - Silent failures
|
|
||||||
- **Tests pass but integration fails** - Mock incomplete, real API complete
|
|
||||||
- **False confidence** - Test proves nothing about real behavior
|
|
||||||
|
|
||||||
**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
|
|
||||||
|
|
||||||
**The fix:**
|
|
||||||
```typescript
|
|
||||||
// ✅ GOOD: Mirror real API completeness
|
|
||||||
const mockResponse = {
|
|
||||||
status: 'success',
|
|
||||||
data: { userId: '123', name: 'Alice' },
|
|
||||||
metadata: { requestId: 'req-789', timestamp: 1234567890 }
|
|
||||||
// All fields real API returns
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate Function
|
|
||||||
|
|
||||||
```
|
|
||||||
BEFORE creating mock responses:
|
|
||||||
Check: "What fields does the real API response contain?"
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
1. Examine actual API response from docs/examples
|
|
||||||
2. Include ALL fields system might consume downstream
|
|
||||||
3. Verify mock matches real response schema completely
|
|
||||||
|
|
||||||
Critical:
|
|
||||||
If you're creating a mock, you must understand the ENTIRE structure
|
|
||||||
Partial mocks fail silently when code depends on omitted fields
|
|
||||||
|
|
||||||
If uncertain: Include all documented fields
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Pattern 5: Integration Tests as Afterthought
|
|
||||||
|
|
||||||
**The violation:**
|
|
||||||
```
|
|
||||||
✅ Implementation complete
|
|
||||||
❌ No tests written
|
|
||||||
"Ready for testing"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is wrong:**
|
|
||||||
- Testing is part of implementation, not optional follow-up
|
|
||||||
- TDD would have caught this
|
|
||||||
- Can't claim complete without tests
|
|
||||||
|
|
||||||
**The fix:**
|
|
||||||
```
|
|
||||||
TDD cycle:
|
|
||||||
1. Write failing test
|
|
||||||
2. Implement to pass
|
|
||||||
3. Refactor
|
|
||||||
4. THEN claim complete
|
|
||||||
```
|
|
||||||
|
|
||||||
## When Mocks Become Too Complex
|
|
||||||
|
|
||||||
**Warning signs:**
|
|
||||||
- Mock setup longer than test logic
|
|
||||||
- Mocking everything to make test pass
|
|
||||||
- Mocks missing methods real components have
|
|
||||||
- Test breaks when mock changes
|
|
||||||
|
|
||||||
**your human partner's question:** "Do we need to be using a mock here?"
|
|
||||||
|
|
||||||
**Consider:** Integration tests with real components often simpler than complex mocks
|
|
||||||
|
|
||||||
## TDD Prevents These Anti-Patterns
|
|
||||||
|
|
||||||
**Why TDD helps:**
|
|
||||||
1. **Write test first** → Forces you to think about what you're actually testing
|
|
||||||
2. **Watch it fail** → Confirms test tests real behavior, not mocks
|
|
||||||
3. **Minimal implementation** → No test-only methods creep in
|
|
||||||
4. **Real dependencies** → You see what the test actually needs before mocking
|
|
||||||
|
|
||||||
**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
| Anti-Pattern | Fix |
|
|
||||||
|--------------|-----|
|
|
||||||
| Assert on mock elements | Test real component or unmock it |
|
|
||||||
| Test-only methods in production | Move to test utilities |
|
|
||||||
| Mock without understanding | Understand dependencies first, mock minimally |
|
|
||||||
| Incomplete mocks | Mirror real API completely |
|
|
||||||
| Tests as afterthought | TDD - tests first |
|
|
||||||
| Over-complex mocks | Consider integration tests |
|
|
||||||
|
|
||||||
## Red Flags
|
|
||||||
|
|
||||||
- Assertion checks for `*-mock` test IDs
|
|
||||||
- Methods only called in test files
|
|
||||||
- Mock setup is >50% of test
|
|
||||||
- Test fails when you remove mock
|
|
||||||
- Can't explain why mock is needed
|
|
||||||
- Mocking "just to be safe"
|
|
||||||
|
|
||||||
## The Bottom Line
|
|
||||||
|
|
||||||
**Mocks are tools to isolate, not things to test.**
|
|
||||||
|
|
||||||
If TDD reveals you're testing mock behavior, you've gone wrong.
|
|
||||||
|
|
||||||
Fix: Test real behavior or question why you're mocking at all.
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: using-git-worktrees
|
name: using-git-worktrees
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Isolating work with git worktrees"
|
||||||
|
use_when: "Parallel/isolated work in a worktree"
|
||||||
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: using-superpowers
|
name: using-superpowers
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Meta: orienting in the superpowers set"
|
||||||
|
use_when: "Learning how to use the superpowers skills"
|
||||||
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
|
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+46
-16
@@ -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.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# TDD in Python
|
||||||
|
|
||||||
|
The Iron Law, Red-Green-Refactor and the rationalization table in `SKILL.md` apply
|
||||||
|
unchanged. This file replaces the Go-Specific TDD Notes for Python work.
|
||||||
|
|
||||||
|
Canonical stack (`~/dev/.context/AGENT.md`): pytest + pytest-cov, `mypy --strict`, ruff,
|
||||||
|
uv, src-layout. Not restated here — that table has one home.
|
||||||
|
|
||||||
|
## The first red is an ImportError, and that is correct
|
||||||
|
|
||||||
|
For a function that does not exist yet, the first run fails at collection:
|
||||||
|
|
||||||
|
```
|
||||||
|
E ModuleNotFoundError: No module named 'invoicing.vat'
|
||||||
|
```
|
||||||
|
|
||||||
|
That is a legitimate red. Read it, confirm it names the module you are about to write,
|
||||||
|
and go to GREEN. **Do not create a stub raising `NotImplementedError` to make pytest
|
||||||
|
collect.** A stub is production code written before a failing test.
|
||||||
|
|
||||||
|
## Table-driven: `parametrize`, one behaviour per function
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("amount_minor", "rate", "expected"),
|
||||||
|
[
|
||||||
|
pytest.param(14, Decimal("0.12"), (12, 2), id="tie-rounds-down-to-even"),
|
||||||
|
pytest.param(42, Decimal("0.12"), (38, 4), id="tie-rounds-up-to-even"),
|
||||||
|
pytest.param(12500, Decimal("0.25"), (10000, 2500), id="exact-division"),
|
||||||
|
pytest.param(0, Decimal("0.25"), (0, 0), id="zero-is-not-an-error"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_split_vat_rounds_half_to_even(
|
||||||
|
amount_minor: int, rate: Decimal, expected: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
assert split_vat(amount_minor, rate) == expected
|
||||||
|
```
|
||||||
|
|
||||||
|
`id=` is the analogue of Go's `t.Run` name: it is what appears in the failure output and
|
||||||
|
what you pass to `pytest -k`.
|
||||||
|
|
||||||
|
**Do not port Go's `wantErr` column.** The Go exemplar branches inside the loop —
|
||||||
|
`if tt.wantErr { assert.Error } else { assert.NoError }`. Translated literally that
|
||||||
|
gives a conditional assertion and folds two behaviours into one test, which `SKILL.md`'s
|
||||||
|
own quality table forbids ("One thing. 'and' in name? Split it."). Error cases get
|
||||||
|
their own function:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_split_vat_rejects_negative_amount() -> None:
|
||||||
|
with pytest.raises(ValueError, match="amount_minor must be non-negative"):
|
||||||
|
split_vat(-1, Decimal("0.25"))
|
||||||
|
```
|
||||||
|
|
||||||
|
`match=` pins the message, not just the type. Go's `assert.Error` does not, and the
|
||||||
|
stricter version is available here for free — use it.
|
||||||
|
|
||||||
|
## `mypy --strict` is part of RED, not a chore after GREEN
|
||||||
|
|
||||||
|
A red type-check is a red build.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/test_vat.py -x -q # the failing test
|
||||||
|
uv run mypy --strict src tests # same gate, same phase
|
||||||
|
```
|
||||||
|
|
||||||
|
Under `--strict` every test function needs `-> None`, or it is an untyped def and the
|
||||||
|
type-checker rejects the file — including the file that holds your failing test.
|
||||||
|
|
||||||
|
## Layout and imports
|
||||||
|
|
||||||
|
- `src/` layout, tests in a sibling `tests/`. This is the *opposite* of Go's
|
||||||
|
same-directory convention, and it buys black-box testing structurally: `from
|
||||||
|
invoicing.vat import split_vat` can only resolve against the installed package, so
|
||||||
|
the preference Go has to state explicitly is enforced here by the import system.
|
||||||
|
- No `__init__.py` in `tests/` unless you need shared test packages; pytest's rootdir
|
||||||
|
handling copes, and an accidental package shadows names.
|
||||||
|
- `conftest.py` holds fixtures shared across a directory. A fixture in a single test
|
||||||
|
file that only one test uses should be a plain function call instead — fixtures are
|
||||||
|
indirection, and indirection is only worth it at reuse.
|
||||||
|
|
||||||
|
## No `require` vs `assert`
|
||||||
|
|
||||||
|
Every pytest `assert` is fatal to its test; there is no continue-on-failure form. Where
|
||||||
|
Go would use `assert` to collect several soft failures, either split the test or use
|
||||||
|
`pytest.approx`/a single structural comparison:
|
||||||
|
|
||||||
|
```python
|
||||||
|
assert result == Invoice(id=InvoiceId("42"), net=10000, vat=2500) # one assertion, whole shape
|
||||||
|
```
|
||||||
|
|
||||||
|
Comparing whole values beats three field assertions: the failure output shows the entire
|
||||||
|
diff rather than the first field that differed.
|
||||||
|
|
||||||
|
## Fixtures vs helpers
|
||||||
|
|
||||||
|
- **Helper function** — when the setup is cheap and local. It reads top-to-bottom.
|
||||||
|
- **Fixture** — when the setup is shared, or needs teardown, or needs a scope
|
||||||
|
(`session`, `module`) because it is expensive.
|
||||||
|
- Set `__tracebackhide__ = True` in an assertion helper so failures point at the caller,
|
||||||
|
the way `t.Helper()` does in Go.
|
||||||
|
|
||||||
|
## Coverage is not the concurrency gate
|
||||||
|
|
||||||
|
`SKILL.md`'s checklist asks for the language's concurrency gate. Python has no `-race`
|
||||||
|
equivalent, and pretending `pytest --cov` fills that slot is dishonest — coverage says
|
||||||
|
which lines ran, not whether two threads can interleave badly. For a pure function,
|
||||||
|
strike the box deliberately. For genuinely concurrent code (`asyncio`, threads), the
|
||||||
|
substitute is a test that runs the interleaving you fear, plus `pytest-timeout` so a
|
||||||
|
deadlock fails rather than hangs.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# TDD in Rust
|
||||||
|
|
||||||
|
The Iron Law and Red-Green-Refactor in `SKILL.md` apply unchanged. This file replaces
|
||||||
|
the Go-Specific TDD Notes for Rust.
|
||||||
|
|
||||||
|
## The first red is a compile error, and that is correct
|
||||||
|
|
||||||
|
```
|
||||||
|
error[E0425]: cannot find function `split_vat` in this scope
|
||||||
|
```
|
||||||
|
|
||||||
|
Legitimate — the compiler is the test runner's first phase. Read it, confirm it names
|
||||||
|
the item you are about to write, and go to GREEN. Do not write a `todo!()` stub first;
|
||||||
|
that is production code before a failing test. (`todo!()` is fine *during* GREEN as a
|
||||||
|
branch you have not reached yet.)
|
||||||
|
|
||||||
|
## Where tests live
|
||||||
|
|
||||||
|
- **Unit tests** in the same file, in `mod tests` behind `#[cfg(test)]`. They can reach
|
||||||
|
private items — the white-box option Go spells `package foo`.
|
||||||
|
- **Integration tests** in `tests/`, which link the crate as an external consumer and see
|
||||||
|
only the public API. This is the black-box default `SKILL.md` prefers; use it unless
|
||||||
|
the behaviour under test is genuinely private.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tie_rounds_down_to_even() {
|
||||||
|
assert_eq!(split_vat(14, dec!(0.12)).unwrap(), (12, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table-driven: an array of cases
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn rounds_half_to_even() {
|
||||||
|
let cases = [
|
||||||
|
("tie rounds down", 14u64, dec!(0.12), (12, 2)),
|
||||||
|
("tie rounds up", 42, dec!(0.12), (38, 4)),
|
||||||
|
("exact division", 12500, dec!(0.25), (10000, 2500)),
|
||||||
|
];
|
||||||
|
for (name, amount, rate, want) in cases {
|
||||||
|
assert_eq!(split_vat(amount, rate).unwrap(), want, "case: {name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `"case: {name}"` message is the `t.Run` analogue — without it a failure names a line
|
||||||
|
number and leaves you counting rows. If you want each case to be its own reported test,
|
||||||
|
use `rstest`'s `#[case]`; that is a dependency, so justify it before adding it.
|
||||||
|
|
||||||
|
## Error cases
|
||||||
|
|
||||||
|
`Result` makes the Go `wantErr` column unnecessary — assert on the variant:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn rejects_negative_amount() {
|
||||||
|
let err = split_vat_signed(-1, dec!(0.25)).unwrap_err();
|
||||||
|
assert!(matches!(err, VatError::NegativeAmount));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`#[should_panic(expected = "...")]` is for genuine panics only — invariant violations,
|
||||||
|
not expected error paths. A function whose contract includes "rejects negative input"
|
||||||
|
should return `Err`, not panic, and a test that accepts a panic hides that design choice.
|
||||||
|
|
||||||
|
## Gates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test # the failing test
|
||||||
|
cargo clippy -- -D warnings # static gate, same phase as RED
|
||||||
|
cargo fmt --check
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo test` compiles in debug, so integer overflow panics rather than wrapping — a
|
||||||
|
release build will not. Where overflow matters, test the checked path explicitly
|
||||||
|
(`checked_mul`), or you are testing debug-only behaviour.
|
||||||
|
|
||||||
|
For unsafe code, `cargo +nightly miri test` is the nearest thing to Go's `-race`.
|
||||||
|
Safe-only crates have no equivalent and should strike that checklist box deliberately.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# TDD in TypeScript
|
||||||
|
|
||||||
|
The Iron Law and Red-Green-Refactor in `SKILL.md` apply unchanged. This file replaces
|
||||||
|
the Go-Specific TDD Notes for TypeScript.
|
||||||
|
|
||||||
|
Vitest is the default; Jest differs only in imports and a few matcher names, both noted
|
||||||
|
where it matters.
|
||||||
|
|
||||||
|
## The first red is a resolution error, and that is correct
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: Failed to resolve import "./vat" from "src/vat.test.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
Legitimate. Do not create an empty module or a stub throwing `new Error("not
|
||||||
|
implemented")` to get past collection — that is production code before a failing test.
|
||||||
|
|
||||||
|
## Table-driven: `it.each`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
describe('splitVat', () => {
|
||||||
|
it.each([
|
||||||
|
{ amountMinor: 14, rate: 0.12, expected: [12, 2], name: 'tie rounds down to even' },
|
||||||
|
{ amountMinor: 42, rate: 0.12, expected: [38, 4], name: 'tie rounds up to even' },
|
||||||
|
{ amountMinor: 12500, rate: 0.25, expected: [10000, 2500], name: 'exact division' },
|
||||||
|
])('$name', ({ amountMinor, rate, expected }) => {
|
||||||
|
expect(splitVat(amountMinor, rate)).toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`$name` interpolates into the test title, which is the `t.Run` analogue. Prefer the
|
||||||
|
object form over the array form — a positional tuple with five entries is unreadable at
|
||||||
|
the point of failure.
|
||||||
|
|
||||||
|
**Do not port Go's `wantErr` column.** Error cases get their own test:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
it('rejects a negative amount', () => {
|
||||||
|
expect(() => splitVat(-1, 0.25)).toThrow(/must be non-negative/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a negative amount (async)', async () => {
|
||||||
|
await expect(splitVat(-1, 0.25)).rejects.toThrow(/must be non-negative/)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `await` on the async form is load-bearing. Without it the assertion returns a
|
||||||
|
promise nobody checks, the test passes, and it will keep passing after you break the
|
||||||
|
code. This is the single most common false-green in a TypeScript suite.
|
||||||
|
|
||||||
|
## `tsc --noEmit` is part of RED
|
||||||
|
|
||||||
|
A red type-check is a red build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx vitest run src/vat.test.ts # the failing test
|
||||||
|
npx tsc --noEmit # same gate, same phase
|
||||||
|
```
|
||||||
|
|
||||||
|
Vitest transpiles without type-checking, so a suite can be green while the code does not
|
||||||
|
compile. `tsc --noEmit` is not optional here — it is the half of the gate the runner
|
||||||
|
does not perform.
|
||||||
|
|
||||||
|
## Equality matchers are not interchangeable
|
||||||
|
|
||||||
|
- `toBe` — `Object.is`. For primitives and identity.
|
||||||
|
- `toEqual` — structural, and **ignores `undefined` properties**: `{a: 1, b: undefined}`
|
||||||
|
equals `{a: 1}`. That is a real hole when the bug you are hunting is a missing field.
|
||||||
|
- `toStrictEqual` — structural, respects `undefined`, and checks class identity. Default
|
||||||
|
to this one; reach for `toEqual` deliberately.
|
||||||
|
|
||||||
|
## Mocks
|
||||||
|
|
||||||
|
`vi.mock` (Jest: `jest.mock`) is hoisted above imports, so a factory referencing an
|
||||||
|
outer variable throws at runtime. Use `vi.hoisted` when the mock needs shared state.
|
||||||
|
Per `SKILL.md`, mock only across a system boundary — a mocked module you own is a test
|
||||||
|
of your mock.
|
||||||
|
|
||||||
|
## Async and fake timers
|
||||||
|
|
||||||
|
- `vi.useFakeTimers()` needs a matching `vi.useRealTimers()` in teardown, or the next
|
||||||
|
file inherits frozen time and fails somewhere unrelated.
|
||||||
|
- Prefer condition polling (`await vi.waitFor(...)`) over a fixed `setTimeout` — an
|
||||||
|
arbitrary wait is a flake with a schedule.
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: telos-load
|
name: telos-load
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Load TELOS intention substrate at session start"
|
||||||
|
use_when: "Starting any koala session; before architectural decisions; CAD pipeline entry"
|
||||||
description: Load TELOS — the intention substrate (problems, goals, strategies, status) — into a session at start so every decision traces to a goal. Use when starting any koala session, before architectural decisions, or as the CAD pipeline entry point ("load TELOS", "what are we optimizing toward?").
|
description: Load TELOS — the intention substrate (problems, goals, strategies, status) — into a session at start so every decision traces to a goal. Use when starting any koala session, before architectural decisions, or as the CAD pipeline entry point ("load TELOS", "what are we optimizing toward?").
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -79,3 +82,11 @@ created in a CAD session should trace to a TELOS GOAL. Every GOAL
|
|||||||
traces to a PROBLEM.
|
traces to a PROBLEM.
|
||||||
|
|
||||||
Traceability chain: PROBLEM → GOAL → SPEC → TICKET → IMPL → TEST
|
Traceability chain: PROBLEM → GOAL → SPEC → TICKET → IMPL → TEST
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only. Loads intention context; changes nothing.
|
||||||
|
|
||||||
|
Stop and hand back when: the brain is unreachable or returns nothing for the wing. Working
|
||||||
|
from a remembered TELOS is the failure this skill exists to prevent — say the load failed
|
||||||
|
rather than reconstructing it from memory.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: test-design
|
name: test-design
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Dave Farley test design scoring (Farley Index)"
|
||||||
|
use_when: "Reviewing or writing tests"
|
||||||
description: Evaluate test quality using Dave Farley's 8 Properties of Good Tests. Use when reviewing or writing tests to ensure they provide genuine verification.
|
description: Evaluate test quality using Dave Farley's 8 Properties of Good Tests. Use when reviewing or writing tests to ensure they provide genuine verification.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -306,3 +309,11 @@ Apply this checklist to every new test:
|
|||||||
- Load `tdd` skill for the full TDD workflow
|
- Load `tdd` skill for the full TDD workflow
|
||||||
- Load `code-review` skill for test quality review during pre-merge review
|
- Load `code-review` skill for test quality review during pre-merge review
|
||||||
- See `clean-code/references/code-smells.md` for testing-specific smells
|
- See `clean-code/references/code-smells.md` for testing-specific smells
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only scoring of existing tests.
|
||||||
|
|
||||||
|
Stop and hand back when: the tests are fine and the *design under test* is what makes them
|
||||||
|
awkward. Hard-to-test is a design finding, not a test finding, and rewriting the tests
|
||||||
|
around it hides the thing worth knowing.
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
name: traceability
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Forward trace — requirement to decision to commit to verification to release, and what cannot be checked"
|
||||||
|
use_when: "Proving why a change exists; auditing a claim; before an attestation or a compliance ask"
|
||||||
|
description: Establish the forward trace for a change — requirement, decision, commit, verification, CI, release, closure — as a chain of links that are each checked rather than asserted. Names the links that cannot be verified from the repository instead of letting them pass as verified. Use when auditing why a change exists, preparing an attestation, or when a claim needs to be provable rather than plausible.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Traceability
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A trace is not a story about how a change came to be. It is a chain of links, each of
|
||||||
|
which is either **checked** or **named as unchecked**. The value is entirely in that
|
||||||
|
distinction: a trace where every link is asserted is a narrative, and narratives are what
|
||||||
|
audits exist to defeat.
|
||||||
|
|
||||||
|
The chain in this estate:
|
||||||
|
|
||||||
|
```
|
||||||
|
issue (requirement) → comment (decision) → commit → test/gate → CI run on that sha
|
||||||
|
→ release tag → closing comment (evidence)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note where the decisions live: **in issue comments**, not issue bodies. A tool that reads
|
||||||
|
issue titles will trace the wrong thing.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Auditing why a change exists, especially one nobody remembers making
|
||||||
|
- Preparing an attestation, a compliance answer, or a client-facing "how do you know"
|
||||||
|
- Before claiming a requirement is satisfied
|
||||||
|
- When a claim is repeated in several places, which is the condition under which nobody
|
||||||
|
checks it
|
||||||
|
|
||||||
|
**Do not use for:** a change you are about to make. That is `adr` (the decision) and
|
||||||
|
`spec-driven-dev` (the requirement). This skill reconstructs; it does not author.
|
||||||
|
|
||||||
|
## The tool
|
||||||
|
|
||||||
|
```sh
|
||||||
|
skills/traceability/trace.sh <sha> [repo-path]
|
||||||
|
```
|
||||||
|
|
||||||
|
Prints what git can prove — the commit, its files, the test files it touched, the release
|
||||||
|
that contains it, every issue it references as a URL — and then prints, every time, the
|
||||||
|
three things git **cannot** prove. That last section is the point of the tool.
|
||||||
|
|
||||||
|
## The sequence
|
||||||
|
|
||||||
|
**1. Run the tool.** It does the mechanical half in one command.
|
||||||
|
|
||||||
|
**2. Open the issues it printed, and read the comments.** The requirement is in the body;
|
||||||
|
the decision is almost always in a comment, and so is the closure record. A trace built
|
||||||
|
from commit messages alone will confidently cite an issue that says something else.
|
||||||
|
|
||||||
|
**3. Match CI on the head sha, not on the branch.** "The pipeline is green" is not
|
||||||
|
evidence about a commit. Find the run whose `head_sha` is this commit and read its
|
||||||
|
conclusion — and check which *job* and which *step* passed, because a run can be red for a
|
||||||
|
reason that has nothing to do with the gate you care about, and green overall while the
|
||||||
|
gate you care about was skipped.
|
||||||
|
|
||||||
|
The estate's `workflow_run_status` returns only the run-level conclusion — no jobs, no
|
||||||
|
steps. For that breakdown you have to drop to the API:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
op run --env-file ~/.op-env -- sh -c 'curl -sS -H "Authorization: token $DMABE_GITEA_API_TOKEN" \
|
||||||
|
"https://git.d-ma.be/api/v1/repos/<owner>/<repo>/actions/runs/<id>/jobs"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Said explicitly because a step whose tooling dead-ends is exactly where a trace becomes
|
||||||
|
"the run was green".
|
||||||
|
|
||||||
|
**4. Check that the tests actually cover the behaviour.** A commit that touched a test
|
||||||
|
file is not a commit that is verified. Read the assertions and ask what would have to
|
||||||
|
break for them to fail. If nothing would, the link is decorative.
|
||||||
|
|
||||||
|
**5. Probe the load-bearing claim — try to falsify it.** This is the step that separates a
|
||||||
|
trace from a summary, and it is the one that finds things.
|
||||||
|
|
||||||
|
Pick the claim the chain rests on, and attack it in a scratch copy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git archive <sha> | (mkdir -p /tmp/probe && tar -x -C /tmp/probe)
|
||||||
|
# then break exactly the thing the claim protects, and run the gate
|
||||||
|
```
|
||||||
|
|
||||||
|
If the gate stays green, the claim is false — regardless of how many places assert it.
|
||||||
|
This is not hypothetical: the probe that shaped this skill falsified a "the list only
|
||||||
|
shrinks" claim that was stated in five places in the repository and enforced in none.
|
||||||
|
|
||||||
|
**6. Look for what cites the commit, not only what it cites.** A commit cannot reference
|
||||||
|
an issue filed after it, and that is precisely where an outcome gets falsified. Search the
|
||||||
|
tracker for the sha, across issues *and comments*, and `git log --all --grep=<sha>` for
|
||||||
|
later commits referring back. In the run that shaped this skill, the issue proving the
|
||||||
|
traced commit had *relocated* rather than fixed its defect was filed 90 minutes later and
|
||||||
|
was reachable only this way.
|
||||||
|
|
||||||
|
**7. Report unverifiable links as unverifiable.** Not as weak links, not as caveats at the
|
||||||
|
end. Each gets its own line saying what would be needed to close it.
|
||||||
|
|
||||||
|
## What a broken link looks like
|
||||||
|
|
||||||
|
| Symptom | What it means |
|
||||||
|
|---|---|
|
||||||
|
| The commit cites `#37`, and `#37` describes something else | The reference is decoration; someone renumbered or misremembered |
|
||||||
|
| A claim appears in five places and a check in none | Convention wearing a mechanism's clothes. Probe it |
|
||||||
|
| The gate greps for a heading | Presence is not compliance. A heading with nothing under it passes |
|
||||||
|
| `git describe` says unreleased | Usually stale local tags, not an unreleased commit. `git fetch --tags` first |
|
||||||
|
| The requirement is a bare `#5` with no URL | Recoverable only while the tracker lives. Not traceable from the repo alone |
|
||||||
|
| CI green, but on a different sha | The most common false link, and the easiest to accept by accident |
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
State the chain, then the gaps, in that order:
|
||||||
|
|
||||||
|
```
|
||||||
|
CHAIN
|
||||||
|
requirement #36 item 3 (defect report) → verified, read: body + comments
|
||||||
|
requirement #19 (standing execution bar) → verified, read
|
||||||
|
decision comment 8509 → verified; names the commit
|
||||||
|
commit b616c51 → verified
|
||||||
|
claim "nine skills, none named session_id" → verified against b616c51^: 9/9
|
||||||
|
claim "session_id is the only required field" → verified against the live tool schema
|
||||||
|
gate bin/check-session-log → RED on parent (17 errors), GREEN on it
|
||||||
|
wiring Taskfile check step 5 → verified; CI runs task check
|
||||||
|
verification tests/check_session_log_test.sh → verified, 4 cases, both directions
|
||||||
|
CI run 4880, job "Check / Test" → verified, head_sha matches, success
|
||||||
|
release v0.13.3 → verified against the REMOTE
|
||||||
|
|
||||||
|
GAPS
|
||||||
|
- The gate greps the file, not the payload. Probe: stripped session_id from
|
||||||
|
every payload, left the word in prose — gate returned 0. FALSIFIED.
|
||||||
|
- Defect relocated, not removed: #39, filed 90 minutes later by the same
|
||||||
|
author, says so. The commit's outcome claim does not hold.
|
||||||
|
```
|
||||||
|
|
||||||
|
Four slots that are not optional, each learned by leaving them out:
|
||||||
|
|
||||||
|
- **More than one requirement, and they differ in kind.** A defect report and a standing
|
||||||
|
policy bar are both requirements and collapsing them hides which one was not met.
|
||||||
|
- **The gate, separately from the test.** A commit that ships a check has three artifacts,
|
||||||
|
not two, and the chain usually rests on the one in the middle.
|
||||||
|
- **The RED/GREEN baseline.** Run the gate against the parent commit. Without it you have
|
||||||
|
shown a gate exists, not that it caught anything.
|
||||||
|
- **The commit's factual claims, checked.** "Nine skills, none named it" is falsifiable in
|
||||||
|
one command. Without a slot for it, "commit → verified" means only "this sha exists".
|
||||||
|
|
||||||
|
Gaps ranked by how much they matter, not by where you found them. A trace whose gap
|
||||||
|
section is empty should be suspicious of itself.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Read-only. Reports the chain; changes nothing, and closes no issue on the strength of
|
||||||
|
what it finds.
|
||||||
|
|
||||||
|
Stop and hand back when: a link's evidence is outside anything you can reach — a tracker
|
||||||
|
you cannot read, a CI system you have no access to, a decision made in conversation. Say
|
||||||
|
which link and what would close it. An inferred link presented as a traced one is worse
|
||||||
|
than a gap, because the gap is honest and the inference is citable.
|
||||||
|
|
||||||
|
Also stop before *acting* on a falsified claim. Finding that a gate does not enforce what
|
||||||
|
it says is a finding; deciding what to do about it is the owner's call.
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- `lineage` — the same discipline backwards: this asks "does the chain hold", that asks
|
||||||
|
"was it tried before and why did it stop".
|
||||||
|
- `adr` — the decision layer of the chain. A trace that reaches an ADR has found the
|
||||||
|
reasoning; a trace that reaches only a commit message has found a summary of it.
|
||||||
Executable
+107
@@ -0,0 +1,107 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# trace.sh — the forward trace for one commit, and an honest list of what it
|
||||||
|
# could not check.
|
||||||
|
#
|
||||||
|
# The chain this estate actually uses:
|
||||||
|
#
|
||||||
|
# issue (requirement) -> comment (decision) -> commit -> test/gate -> CI run
|
||||||
|
# -> release tag -> closing comment
|
||||||
|
#
|
||||||
|
# git can prove the middle of that and nothing else. The issue bodies, the
|
||||||
|
# comments where decisions actually live, and the CI run are all tracker state.
|
||||||
|
# So this prints what git knows and then names, with URLs, the links a human has
|
||||||
|
# to follow — because a link nobody checked must not read like a verified one.
|
||||||
|
#
|
||||||
|
# Usage: trace.sh <sha> [repo-path]
|
||||||
|
#
|
||||||
|
# Read-only.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
[ $# -ge 1 ] || { echo "usage: trace.sh <sha> [repo-path]" >&2; exit 2; }
|
||||||
|
SHA=$1
|
||||||
|
REPO=${2:-.}
|
||||||
|
|
||||||
|
git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1 || {
|
||||||
|
echo "trace: $REPO is not a git repository" >&2; exit 2; }
|
||||||
|
git -C "$REPO" rev-parse --verify --quiet "$SHA^{commit}" >/dev/null || {
|
||||||
|
echo "trace: $SHA is not a commit in $REPO" >&2; exit 2; }
|
||||||
|
|
||||||
|
TOP=$(git -C "$REPO" rev-parse --show-toplevel)
|
||||||
|
ORIGIN=$(git -C "$REPO" config --get remote.origin.url 2>/dev/null || true)
|
||||||
|
# Never print the URL as configured: url.insteadOf can expand it into a live
|
||||||
|
# token. Reduce it to owner/repo and use that.
|
||||||
|
SLUG=$(printf '%s' "$ORIGIN" | sed -E 's#\.git$##; s#^.*[/:]([^/]+/[^/]+)$#\1#' 2>/dev/null || true)
|
||||||
|
|
||||||
|
section() { printf '\n== %s ==\n' "$1"; }
|
||||||
|
|
||||||
|
section "Commit"
|
||||||
|
git -C "$REPO" log -1 --format='%h %ad %an%n%n%B' --date=short "$SHA"
|
||||||
|
|
||||||
|
section "Files"
|
||||||
|
git -C "$REPO" show --stat --format='' "$SHA"
|
||||||
|
|
||||||
|
section "Verification it touched"
|
||||||
|
tests=$(git -C "$REPO" show --name-only --format='' "$SHA" \
|
||||||
|
| grep -Ei '(^|/)(tests?|spec)/|_test\.|\.test\.|_spec\.' || true)
|
||||||
|
if [ -n "$tests" ]; then
|
||||||
|
printf '%s\n' "$tests"
|
||||||
|
else
|
||||||
|
printf 'none — this commit changed no test or spec file.\n'
|
||||||
|
printf 'That is not automatically wrong (docs, config), but if it changed behaviour\n'
|
||||||
|
printf 'it means the change has no verification of its own.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Released in"
|
||||||
|
tag=$(git -C "$REPO" describe --tags --contains "$SHA" 2>/dev/null | sed 's/[~^].*//' || true)
|
||||||
|
if [ -n "$tag" ]; then
|
||||||
|
printf '%s (LOCAL tags only — confirm with: git ls-remote --tags origin)\n' "$tag"
|
||||||
|
else
|
||||||
|
printf 'no release contains this commit in THIS checkout.\n'
|
||||||
|
printf 'Local tags go stale — run `git fetch --tags` and re-check before\n'
|
||||||
|
printf 'concluding it was never released.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Referenced issues"
|
||||||
|
refs=$(git -C "$REPO" log -1 --format='%B' "$SHA" | grep -oE '#[0-9]+' | sort -u || true)
|
||||||
|
if [ -n "$refs" ]; then
|
||||||
|
for r in $refs; do
|
||||||
|
n=${r#\#}
|
||||||
|
if [ -n "$SLUG" ]; then
|
||||||
|
printf '%s -> https://git.d-ma.be/%s/issues/%s\n' "$r" "$SLUG" "$n"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$r"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
printf 'none — the commit cites no issue, so its requirement is unrecorded.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Issues that cite THIS commit"
|
||||||
|
printf 'Not searchable from the commit — an issue filed after it cannot appear in\n'
|
||||||
|
printf 'its message, and that is exactly where an outcome gets falsified. Check:\n\n'
|
||||||
|
printf ' git -C %s log --all --grep=%s # later commits referring back\n' "$REPO" "$SHA"
|
||||||
|
if [ -n "$SLUG" ]; then
|
||||||
|
printf ' tracker search for "%s" across issues AND comments\n' "$SHA"
|
||||||
|
fi
|
||||||
|
printf '\nIn the run that shaped this tool, the issue that falsified the traced\n'
|
||||||
|
printf 'commit was filed 90 minutes after it and was reachable only this way.\n'
|
||||||
|
|
||||||
|
section "Cannot verify from here"
|
||||||
|
cat <<'TXT'
|
||||||
|
git proves the commit, its files, and which tag contains it. It cannot prove:
|
||||||
|
|
||||||
|
1. That the issues above say what the commit claims they say. The DECISION is
|
||||||
|
usually in a comment, not the issue body — open them and read the comments.
|
||||||
|
2. That CI actually passed on this exact sha. A green pipeline elsewhere is not
|
||||||
|
evidence about this commit; match on head_sha.
|
||||||
|
3. That the tests listed above assert the behaviour this commit added. A test
|
||||||
|
file touched is not a behaviour covered.
|
||||||
|
4. That any gate this commit ships enforces what its message says it does.
|
||||||
|
Probe it: break exactly the thing the claim protects, in a scratch copy,
|
||||||
|
and see whether the gate goes red.
|
||||||
|
|
||||||
|
None of the three is a formality. Each has been the broken link in this estate:
|
||||||
|
a claim asserted in five places and enforced in none, a red run whose gate had
|
||||||
|
passed, and a gate that greps for a heading and calls it a contract.
|
||||||
|
TXT
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: trainer
|
name: trainer
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Two-phase brain curation (score candidates, write past quality gate)"
|
||||||
|
use_when: "Periodic brain pruning and curation"
|
||||||
description: Two-phase brain curation — scan session logs to score candidate learnings (READ), then write the ones that pass the quality gate as generalized knowledge entries (WRITE). Use periodically to keep the brain useful, not noisy.
|
description: Two-phase brain curation — scan session logs to score candidate learnings (READ), then write the ones that pass the quality gate as generalized knowledge entries (WRITE). Use periodically to keep the brain useful, not noisy.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -228,10 +231,10 @@ 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 `read` phase:**
|
**At end of `read` phase:**
|
||||||
- `session_log` with `{skill: "trainer", phase: "read", 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: "trainer", phase: "read", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**At end of `write` phase:**
|
**At end of `write` phase:**
|
||||||
- `session_log` with `{skill: "trainer", phase: "write", 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: "trainer", phase: "write", final_status: "pass" | "fail" | "skip", message: "<one-line summary>", project_root: "<absolute path>"}`
|
||||||
|
|
||||||
**Phases for this skill:** read, write
|
**Phases for this skill:** read, write
|
||||||
|
|
||||||
@@ -240,11 +243,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 (e.g. no candidates passed the gate, write skipped).
|
- `skip` — phase was skipped intentionally (e.g. no candidates passed the gate, write skipped).
|
||||||
|
|
||||||
**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
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Cross-References
|
## Cross-References
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: user-stories
|
name: user-stories
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Elephant Carpaccio story slicing"
|
||||||
|
use_when: "Breaking down features"
|
||||||
description: Decompose problems into thin, deliverable user stories using Elephant Carpaccio and INVEST criteria. Use after problem analysis, before planning and implementation.
|
description: Decompose problems into thin, deliverable user stories using Elephant Carpaccio and INVEST criteria. Use after problem analysis, before planning and implementation.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: web-shot
|
name: web-shot
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Screenshot a running web UI via a koala k3s Playwright job"
|
||||||
|
use_when: "Visual verification of a running UI when no local browser exists"
|
||||||
description: Screenshot any running web UI by spawning a one-shot Playwright Job on the koala k3s cluster. Auto-detects the calling host's Tailscale name so a dev server on flamingo:8099, iguana:3000 or koala:8181 just works. Use when the user asks for a screenshot, visual verification, "what does the UI look like", or any time chromium / firefox / a browser is requested but none is installed on the host.
|
description: Screenshot any running web UI by spawning a one-shot Playwright Job on the koala k3s cluster. Auto-detects the calling host's Tailscale name so a dev server on flamingo:8099, iguana:3000 or koala:8181 just works. Use when the user asks for a screenshot, visual verification, "what does the UI look like", or any time chromium / firefox / a browser is requested but none is installed on the host.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/check-escalation.
|
||||||
|
#
|
||||||
|
# #5 asked whether SKILL.md needs first-class failure/escalation fields and
|
||||||
|
# decided: not frontmatter (nothing reads it), a body section (the model reads
|
||||||
|
# that). #37 then had to define WHICH skills need one, and every obvious answer
|
||||||
|
# was bad — a hardcoded list rots, content sniffing is unfalsifiable, a
|
||||||
|
# frontmatter flag is the rejected option through the back door.
|
||||||
|
#
|
||||||
|
# So there is no acting set. EVERY skill answers the question; a read-only one
|
||||||
|
# answers it in a line. The classification problem disappears, and "this skill
|
||||||
|
# never hands back" becomes a stated claim rather than an absence.
|
||||||
|
#
|
||||||
|
# bin/escalation-pending.txt is a ratchet for skills whose real rule has not
|
||||||
|
# been written yet. It is self-cleaning: a skill that has a section AND is still
|
||||||
|
# listed fails, so the list cannot silently outlive its reason.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/bin/check-escalation"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/check_escalation_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# fixture <name> — an empty checkout with a bin/ for the pending list.
|
||||||
|
fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/skills" "$_f/bin"
|
||||||
|
: > "$_f/bin/escalation-pending.txt"
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# skill <root> <name> <has-section>
|
||||||
|
skill() {
|
||||||
|
mkdir -p "$1/skills/$2"
|
||||||
|
{
|
||||||
|
printf -- '---\nname: %s\ndescription: d\n---\n\n# %s\n' "$2" "$2"
|
||||||
|
[ "$3" = yes ] && printf '\n## Escalation\n\nStop and hand back when X. Hand back by: saying so.\n'
|
||||||
|
} > "$1/skills/$2/SKILL.md"
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect <rc> <label> <fixture> [needle]
|
||||||
|
expect() {
|
||||||
|
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
|
||||||
|
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
|
||||||
|
if [ "$_rc" -ne "$_want" ]; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
|
||||||
|
fi
|
||||||
|
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (rc ok, report never says %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
||||||
|
fi
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(fixture ok); skill "$f" alpha yes
|
||||||
|
expect 0 'a skill with an Escalation section passes' "$f"
|
||||||
|
|
||||||
|
f=$(fixture missing); skill "$f" alpha no
|
||||||
|
expect 1 'a skill with no section fails, naming it' "$f" 'alpha'
|
||||||
|
|
||||||
|
f=$(fixture pending); skill "$f" alpha no; printf 'alpha\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 0 'a listed skill is allowed through the ratchet' "$f"
|
||||||
|
|
||||||
|
f=$(fixture done); skill "$f" alpha yes; printf 'alpha\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 1 'a listed skill that now HAS a section fails — remove it from the list' "$f" 'escalation-pending'
|
||||||
|
|
||||||
|
f=$(fixture stale); skill "$f" alpha yes; printf 'ghost\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 1 'a listed skill that does not exist fails' "$f" 'ghost'
|
||||||
|
|
||||||
|
f=$(fixture comments); skill "$f" alpha no
|
||||||
|
printf '# a comment, and a blank line follow\n\nalpha\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 0 'comments and blank lines in the list are ignored' "$f"
|
||||||
|
|
||||||
|
# --- the ratchet must actually ratchet ---------------------------------------
|
||||||
|
# Five places in this repo claimed the pending list "only shrinks" and nothing
|
||||||
|
# enforced it. A traceability probe falsified that in one run: add an unsectioned
|
||||||
|
# skill, append it to the list, gate returns 0. A convention stated five times is
|
||||||
|
# still a convention.
|
||||||
|
#
|
||||||
|
# Enforcement compares against the list as committed at HEAD, so the fixtures
|
||||||
|
# below need a real git repo.
|
||||||
|
|
||||||
|
git_fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/skills" "$_f/bin"
|
||||||
|
( cd "$_f" && git init -q . && git config user.email t@example.com && git config user.name test )
|
||||||
|
skill "$_f" alpha no
|
||||||
|
printf 'alpha\n' > "$_f/bin/escalation-pending.txt"
|
||||||
|
( cd "$_f" && git add -A && git commit -qm baseline )
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(git_fixture ratchet_same)
|
||||||
|
expect 0 'an unchanged list passes' "$f"
|
||||||
|
|
||||||
|
f=$(git_fixture ratchet_shrink)
|
||||||
|
skill "$f" alpha yes
|
||||||
|
: > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 0 'removing an entry (and writing its section) passes' "$f"
|
||||||
|
|
||||||
|
f=$(git_fixture ratchet_grow)
|
||||||
|
skill "$f" beta no
|
||||||
|
printf 'alpha\nbeta\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
expect 1 'adding a NEW entry fails — the list only shrinks' "$f" 'beta'
|
||||||
|
|
||||||
|
# Outside a git repo the comparison is impossible; that must not be silent.
|
||||||
|
f=$(fixture ratchet_nogit); skill "$f" alpha no
|
||||||
|
printf 'alpha\n' > "$f/bin/escalation-pending.txt"
|
||||||
|
_out=$(sh "$SUT" "$f" 2>&1)
|
||||||
|
if printf '%s' "$_out" | grep -qi 'cannot verify'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok an unverifiable ratchet says so rather than passing silently\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL an unverifiable ratchet says so rather than passing silently\n%s\n' "$_out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\ncheck_escalation: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Tests for bin/check-index.
|
|
||||||
#
|
|
||||||
# The regression these exist for: SKILLS_INDEX.md is maintained by hand, and
|
|
||||||
# two skills (`dream`, `experiment-spec`) shipped without a row — the index
|
|
||||||
# rows lived on feature branches that were squash-merged after a directory
|
|
||||||
# reorg, so the SKILL.md landed and the row did not (#15, #22). Nothing
|
|
||||||
# errored; the index just quietly under-reported the library.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
||||||
SUT="$ROOT/bin/check-index"
|
|
||||||
|
|
||||||
pass=0
|
|
||||||
fail=0
|
|
||||||
WORK=${TMPDIR:-/tmp}/check_index_test.$$
|
|
||||||
|
|
||||||
if [ ! -f "$SUT" ]; then
|
|
||||||
printf 'FAIL: %s does not exist\n' "$SUT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cleanup() { rm -rf "$WORK"; }
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
# new_fixture <name> — a checkout with skills/alpha and skills/beta.
|
|
||||||
new_fixture() {
|
|
||||||
_f="$WORK/$1"
|
|
||||||
mkdir -p "$_f/skills/alpha" "$_f/skills/beta"
|
|
||||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
|
|
||||||
printf -- '---\nname: beta\ndescription: b\n---\n' > "$_f/skills/beta/SKILL.md"
|
|
||||||
printf '%s' "$_f"
|
|
||||||
}
|
|
||||||
|
|
||||||
index() {
|
|
||||||
_dir=$1; shift
|
|
||||||
printf '# Engineering Skills Index\n\n| Skill | Description | Use when |\n|---|---|---|\n' > "$_dir/SKILLS_INDEX.md"
|
|
||||||
for _n in "$@"; do
|
|
||||||
printf '| `%s` | desc | when |\n' "$_n" >> "$_dir/SKILLS_INDEX.md"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# expect <0|1> <label> <fixture> [grep-for]
|
|
||||||
expect() {
|
|
||||||
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
|
|
||||||
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
|
|
||||||
if [ "$_rc" -ne "$_want" ]; then
|
|
||||||
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
|
|
||||||
fi
|
|
||||||
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
|
|
||||||
fail=$((fail + 1)); printf ' FAIL %s (rc ok, but report never names %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
|
||||||
fi
|
|
||||||
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
|
||||||
}
|
|
||||||
|
|
||||||
fx=$(new_fixture complete); index "$fx" alpha beta
|
|
||||||
expect 0 'every skill has a row' "$fx"
|
|
||||||
|
|
||||||
fx=$(new_fixture missing); index "$fx" alpha
|
|
||||||
expect 1 'a skill with no row is reported' "$fx" beta
|
|
||||||
|
|
||||||
fx=$(new_fixture stale); index "$fx" alpha beta gamma
|
|
||||||
expect 1 'a row for a skill that does not exist is reported' "$fx" gamma
|
|
||||||
|
|
||||||
fx=$(new_fixture noindex)
|
|
||||||
expect 1 'a missing index is reported' "$fx" SKILLS_INDEX.md
|
|
||||||
|
|
||||||
printf '\ncheck_index: %d passed, %d failed\n' "$pass" "$fail"
|
|
||||||
[ "$fail" -eq 0 ]
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/check-session-log.
|
||||||
|
#
|
||||||
|
# The regression: nine skills documented a session_log payload and not one of
|
||||||
|
# them named `session_id` — the tool's ONLY required field. Every documented
|
||||||
|
# call was incomplete, so an agent following any of them either failed the call
|
||||||
|
# or invented an id, and invented ids do not group. Found by a pressure test
|
||||||
|
# against `debug`, then found to be fleet-wide (#36).
|
||||||
|
#
|
||||||
|
# Also gated: `duration_ms: <wall-clock>`, which an agent cannot measure. A
|
||||||
|
# documented field nobody can honestly populate gets fabricated or silently
|
||||||
|
# dropped; both are worse than omitting it.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/bin/check-session-log"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/check_session_log_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# fixture <name> <body> — a checkout with one skill whose SKILL.md is <body>.
|
||||||
|
fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/skills/alpha"
|
||||||
|
printf -- '---\nname: alpha\ndescription: a\n---\n\n%s\n' "$2" > "$_f/skills/alpha/SKILL.md"
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect <rc> <label> <fixture> [needle]
|
||||||
|
expect() {
|
||||||
|
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
|
||||||
|
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
|
||||||
|
if [ "$_rc" -ne "$_want" ]; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
|
||||||
|
fi
|
||||||
|
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (rc ok, report never says %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
||||||
|
fi
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect 0 'a skill that never mentions session_log is fine' \
|
||||||
|
"$(fixture quiet 'Just a skill. No logging here.')"
|
||||||
|
|
||||||
|
expect 0 'a documented call naming session_id passes' \
|
||||||
|
"$(fixture good 'Call `session_log` with `{session_id: "<id>", skill: "alpha", phase: "red", final_status: "pass"}`')"
|
||||||
|
|
||||||
|
expect 1 'a documented call omitting session_id fails' \
|
||||||
|
"$(fixture bad 'Call `session_log` with `{skill: "alpha", phase: "red", final_status: "pass"}`')" \
|
||||||
|
'session_id'
|
||||||
|
|
||||||
|
expect 1 'documenting duration_ms as wall-clock fails' \
|
||||||
|
"$(fixture clock 'Call `session_log` with `{session_id: "<id>", skill: "alpha", duration_ms: <wall-clock>}`')" \
|
||||||
|
'duration_ms'
|
||||||
|
|
||||||
|
# --- the gate must read the PAYLOAD, not the file ---------------------------
|
||||||
|
# A traceability probe falsified the original: it grepped the whole file for the
|
||||||
|
# string "session_id", so stripping the field from every documented payload and
|
||||||
|
# leaving the word once in prose passed cleanly. The exact regression the gate
|
||||||
|
# exists to prevent was back in the file and the gate said nothing.
|
||||||
|
|
||||||
|
expect 1 'a payload without session_id fails even when prose mentions the word' \
|
||||||
|
"$(fixture prose 'The tool has a required session_id field.
|
||||||
|
|
||||||
|
Call `session_log` with `{skill: "alpha", phase: "red", final_status: "pass"}`')" \
|
||||||
|
'session_id'
|
||||||
|
|
||||||
|
expect 1 'one bad payload among several fails' \
|
||||||
|
"$(fixture mixed 'Call `session_log` with `{session_id: "<id>", skill: "alpha", phase: "red"}`
|
||||||
|
|
||||||
|
Then `session_log` with `{skill: "alpha", phase: "green", final_status: "pass"}`')" \
|
||||||
|
'session_id'
|
||||||
|
|
||||||
|
# duration_ms was banned by matching exactly one spelling of it — the historical
|
||||||
|
# one. Both of these passed the original gate.
|
||||||
|
expect 1 'duration_ms with a JSON-quoted key fails' \
|
||||||
|
"$(fixture jsonkey 'Call `session_log` with `{session_id: "<id>", "duration_ms": <wall-clock>}`')" \
|
||||||
|
'duration_ms'
|
||||||
|
|
||||||
|
expect 1 'duration_ms described in prose rather than angle brackets fails' \
|
||||||
|
"$(fixture prosems 'Call `session_log` with `{session_id: "<id>", duration_ms: elapsed wall-clock ms}`')" \
|
||||||
|
'duration_ms'
|
||||||
|
|
||||||
|
# A real measurement is allowed: the objection is to a placeholder no agent can
|
||||||
|
# fill, not to the field.
|
||||||
|
expect 0 'a duration_ms carrying a real value is allowed' \
|
||||||
|
"$(fixture realms 'Call `session_log` with `{session_id: "<id>", duration_ms: 1430}`')"
|
||||||
|
|
||||||
|
printf '\ncheck_session_log: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/check-surfaces.
|
||||||
|
#
|
||||||
|
# The repo ships to three surfaces — the Claude Code plugin, the symlinked
|
||||||
|
# library (Crush/Pi/OpenCode) and claude.ai, which has no fetch path at all —
|
||||||
|
# and used to treat every skill as belonging to all of them. gitea-ci,
|
||||||
|
# playwright and web-shot cannot fire in a chat surface; a flat mirror installs
|
||||||
|
# skills that can never trigger and degrades selection for the ones that can
|
||||||
|
# (#29).
|
||||||
|
#
|
||||||
|
# The risk this gate exists for: a wrong `surfaces:` value silently removes a
|
||||||
|
# skill from a harness rather than erroring. So presence is not enough — values
|
||||||
|
# are validated against the allowed set.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/bin/check-surfaces"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/check_surfaces_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# fixture <name> <surfaces-line-or-empty> — a checkout holding one skill.
|
||||||
|
fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/skills/alpha"
|
||||||
|
if [ -n "$2" ]; then
|
||||||
|
printf -- '---\nname: alpha\n%s\ndescription: a\n---\n' "$2" > "$_f/skills/alpha/SKILL.md"
|
||||||
|
else
|
||||||
|
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$_f/skills/alpha/SKILL.md"
|
||||||
|
fi
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# expect <rc> <label> <fixture> [needle]
|
||||||
|
expect() {
|
||||||
|
_want=$1; _label=$2; _fx=$3; _needle=${4:-}
|
||||||
|
_out=$(sh "$SUT" "$_fx" 2>&1); _rc=$?
|
||||||
|
if [ "$_rc" -ne "$_want" ]; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (want rc=%s got %s)\n%s\n' "$_label" "$_want" "$_rc" "$_out"; return
|
||||||
|
fi
|
||||||
|
if [ -n "$_needle" ] && ! printf '%s' "$_out" | grep -q "$_needle"; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (rc ok, report never says %s)\n%s\n' "$_label" "$_needle" "$_out"; return
|
||||||
|
fi
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$_label"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect 0 'the default pair is accepted' "$(fixture ok 'surfaces: [claude-code, library]')"
|
||||||
|
expect 0 'all three surfaces are accepted' "$(fixture three 'surfaces: [claude-code, library, chat]')"
|
||||||
|
expect 0 'a single surface is accepted' "$(fixture one 'surfaces: [chat]')"
|
||||||
|
expect 1 'a missing surfaces field fails' "$(fixture missing '')" 'alpha'
|
||||||
|
expect 1 'an unknown surface value fails' "$(fixture bogus 'surfaces: [claude-code, desktop]')" 'desktop'
|
||||||
|
expect 1 'an empty surfaces list fails' "$(fixture empty 'surfaces: []')" 'alpha'
|
||||||
|
|
||||||
|
printf '\ncheck_surfaces: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# `task check` must survive a check that reports "could not verify".
|
||||||
|
#
|
||||||
|
# The regression these exist for: bin/check-mcp-env exits 2 to mean "I could
|
||||||
|
# not run — no jq, or no env-file", which the gate deliberately treats as a
|
||||||
|
# reported NOTE rather than a failure. The Taskfile said exactly that:
|
||||||
|
#
|
||||||
|
# "$DIR/check-mcp-env" "$DIR/.mcp.json" "$HOME/.op-env"
|
||||||
|
# case $? in 0) ;; 2) echo "NOTE: ..." ;; *) fail=1 ;; esac
|
||||||
|
#
|
||||||
|
# and it did not work. Task runs each cmds block under `sh -e`, so a bare
|
||||||
|
# command exiting non-zero aborts the shell *before* `case $?` is ever
|
||||||
|
# reached. The whole gate died with `exit status 2`. Locally the check found
|
||||||
|
# jq and exited 0, so it went green; CI has neither jq nor ~/.op-env, so main
|
||||||
|
# went red on the very commit that added it (skills run #57, 2026-09-07).
|
||||||
|
#
|
||||||
|
# A tolerated exit code that is only tolerated on the host where it never
|
||||||
|
# fires is not tolerated at all. This pins the tolerance itself.
|
||||||
|
#
|
||||||
|
# No mocking: pointing HOME at an empty directory removes ~/.op-env, which is
|
||||||
|
# exactly the condition CI runs under, so check-mcp-env genuinely exits 2.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||||
|
no() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; }
|
||||||
|
|
||||||
|
if ! command -v task >/dev/null 2>&1; then
|
||||||
|
printf ' SKIP: task not installed -- cannot exercise the gate\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
# Sanity: the precondition must actually hold, or the test proves nothing.
|
||||||
|
# An empty HOME means no ~/.op-env, so check-mcp-env cannot verify anything.
|
||||||
|
"$ROOT/bin/check-mcp-env" "$ROOT/.mcp.json" "$TMP/.op-env" >/dev/null 2>&1
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -eq 2 ]; then
|
||||||
|
ok 'precondition: check-mcp-env exits 2 when the env-file is absent'
|
||||||
|
else
|
||||||
|
no "precondition: expected check-mcp-env to exit 2, got $rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The assertion. `task check` must still pass, and must say why it could not
|
||||||
|
# verify -- silence would mean the check was skipped rather than tolerated.
|
||||||
|
out=$(HOME="$TMP" task --dir "$ROOT" check 2>&1)
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
ok 'task check passes when MCP env wiring cannot be verified'
|
||||||
|
else
|
||||||
|
no "task check exited $rc when check-mcp-env reported unverifiable"
|
||||||
|
printf '%s\n' "$out" | sed 's/^/ | /' | tail -8
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s' "$out" | grep -q 'unverified'; then
|
||||||
|
ok 'the unverifiable check is reported, not silently skipped'
|
||||||
|
else
|
||||||
|
no 'no NOTE reported -- an unverifiable check must never look like a pass'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf ' %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
+180
-22
@@ -37,11 +37,21 @@ new_fixture() {
|
|||||||
printf '%s' "$_f"
|
printf '%s' "$_f"
|
||||||
}
|
}
|
||||||
|
|
||||||
# wire_clean <fixture> — the correct wiring: one canonical link, harnesses
|
# wire_clean <fixture> [extra-source-dir] — the correct wiring since #27: the
|
||||||
# pointing at it.
|
# canonical location is a REAL directory of per-skill links, fed by one or more
|
||||||
|
# sources, with each harness keeping its single directory link to it.
|
||||||
wire_clean() {
|
wire_clean() {
|
||||||
mkdir -p "$1/home/.agents" "$1/home/.config/crush" "$1/home/.config/opencode"
|
mkdir -p "$1/home/.agents/skills" "$1/home/.config/crush" "$1/home/.config/opencode"
|
||||||
ln -sfn "$1/ck/skills" "$1/home/.agents/skills"
|
for _d in "$1"/ck/skills/*/; do
|
||||||
|
ln -sfn "${_d%/}" "$1/home/.agents/skills/$(basename "$_d")"
|
||||||
|
done
|
||||||
|
printf '%s\n' "$1/ck/skills" > "$1/home/.agents/skills-sources"
|
||||||
|
if [ -n "${2:-}" ]; then
|
||||||
|
for _d in "$2"/*/; do
|
||||||
|
ln -sfn "${_d%/}" "$1/home/.agents/skills/$(basename "$_d")"
|
||||||
|
done
|
||||||
|
printf '%s\n' "$2" >> "$1/home/.agents/skills-sources"
|
||||||
|
fi
|
||||||
ln -sfn "$1/home/.agents/skills" "$1/home/.config/crush/skills"
|
ln -sfn "$1/home/.agents/skills" "$1/home/.config/crush/skills"
|
||||||
ln -sfn "$1/home/.agents/skills" "$1/home/.config/opencode/skills"
|
ln -sfn "$1/home/.agents/skills" "$1/home/.config/opencode/skills"
|
||||||
}
|
}
|
||||||
@@ -79,27 +89,50 @@ assert_clean "$f" 'clean fixture exits 0'
|
|||||||
|
|
||||||
# --- 2. dangling link (the 243-link case, gitignored and invisible) ----------
|
# --- 2. dangling link (the 243-link case, gitignored and invisible) ----------
|
||||||
f=$(new_fixture dangling); wire_clean "$f"
|
f=$(new_fixture dangling); wire_clean "$f"
|
||||||
ln -sfn "$f/ck/skills-that-moved" "$f/home/.agents/skills"
|
ln -sfn "$f/ck/skills/vanished" "$f/home/.agents/skills/alpha"
|
||||||
assert_fails "$f" 'dangling' 'dangling canonical link is reported'
|
assert_fails "$f" 'dangling' 'dangling per-skill link is reported'
|
||||||
|
|
||||||
# --- 3. resolves, but to a DIFFERENT checkout (the silent-stale case) --------
|
# --- 3. resolves, but into a source nobody configured (silent-stale case) ----
|
||||||
# Worse than dangling: it works, so nothing complains, while serving content
|
# Worse than dangling: it works, so nothing complains, while serving content
|
||||||
# from a checkout that is months behind.
|
# from a checkout that is months behind. Since #27 a link may legitimately
|
||||||
|
# point outside the checkout — but only into a source the installer recorded.
|
||||||
f=$(new_fixture stale)
|
f=$(new_fixture stale)
|
||||||
mkdir -p "$f/other/skills/alpha" "$f/other/skills/beta"
|
mkdir -p "$f/other/alpha"
|
||||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/skills/alpha/SKILL.md"
|
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/other/alpha/SKILL.md"
|
||||||
printf -- '---\nname: beta\ndescription: b\n---\n' > "$f/other/skills/beta/SKILL.md"
|
|
||||||
wire_clean "$f"
|
wire_clean "$f"
|
||||||
ln -sfn "$f/other/skills" "$f/home/.agents/skills"
|
ln -sfn "$f/other/alpha" "$f/home/.agents/skills/alpha"
|
||||||
assert_fails "$f" 'outside' 'link into a foreign checkout is reported'
|
assert_fails "$f" 'unknown source' 'link into an unconfigured source is reported'
|
||||||
|
|
||||||
# --- 4. count mismatch (a harness seeing fewer skills than the checkout) -----
|
# --- 3b. a link into a CONFIGURED extra source is healthy -------------------
|
||||||
f=$(new_fixture count)
|
# The whole point of #27: Anthropic's official set alongside this library.
|
||||||
mkdir -p "$f/home/.config/crush/skills/alpha"
|
f=$(new_fixture multisource)
|
||||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
|
mkdir -p "$f/other/xlsx"
|
||||||
mkdir -p "$f/home/.agents"
|
printf -- '---\nname: xlsx\ndescription: x\n---\n' > "$f/other/xlsx/SKILL.md"
|
||||||
|
wire_clean "$f" "$f/other"
|
||||||
|
assert_clean "$f" 'a link into a configured extra source is healthy'
|
||||||
|
|
||||||
|
# --- 3c. the pre-#27 single-link wiring is named, not mistaken for damage ----
|
||||||
|
f=$(new_fixture legacy)
|
||||||
|
mkdir -p "$f/home/.agents" "$f/home/.config/crush" "$f/home/.config/opencode"
|
||||||
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
|
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
|
||||||
assert_fails "$f" 'count' 'skill-count mismatch is reported'
|
ln -sfn "$f/home/.agents/skills" "$f/home/.config/crush/skills"
|
||||||
|
ln -sfn "$f/home/.agents/skills" "$f/home/.config/opencode/skills"
|
||||||
|
assert_fails "$f" 'single-link' 'legacy single-link wiring is reported as such'
|
||||||
|
|
||||||
|
# --- 4. a skill in the checkout with no link anywhere ------------------------
|
||||||
|
# The multi-source replacement for the old count check: with per-skill links a
|
||||||
|
# skill can be missing individually, which a total count would hide if an extra
|
||||||
|
# source happened to supply the same number.
|
||||||
|
f=$(new_fixture missing); wire_clean "$f"
|
||||||
|
rm -f "$f/home/.agents/skills/beta"
|
||||||
|
assert_fails "$f" 'beta' 'a checkout skill with no link is reported'
|
||||||
|
|
||||||
|
# ...but only for skills that ship to the library. A chat-only skill has no
|
||||||
|
# business in a Crush directory, so its absence is correct, not drift (#29).
|
||||||
|
f=$(new_fixture chatonly); wire_clean "$f"
|
||||||
|
mkdir -p "$f/ck/skills/gamma"
|
||||||
|
printf -- '---\nname: gamma\nsurfaces: [chat]\ndescription: g\n---\n' > "$f/ck/skills/gamma/SKILL.md"
|
||||||
|
assert_clean "$f" 'a chat-only skill needs no library link'
|
||||||
|
|
||||||
# --- 5. stray entry (the loose secret-hygiene.md that loaded nowhere) --------
|
# --- 5. stray entry (the loose secret-hygiene.md that loaded nowhere) --------
|
||||||
f=$(new_fixture stray); wire_clean "$f"
|
f=$(new_fixture stray); wire_clean "$f"
|
||||||
@@ -119,9 +152,9 @@ assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
|
|||||||
# resolution target, which is true but useless. It must say the dir is not a
|
# resolution target, which is true but useless. It must say the dir is not a
|
||||||
# link at all, because the fix (move it aside, re-run install.sh) is different
|
# link at all, because the fix (move it aside, re-run install.sh) is different
|
||||||
# from the fix for a link into a foreign checkout.
|
# from the fix for a link into a foreign checkout.
|
||||||
f=$(new_fixture realdir)
|
f=$(new_fixture realdir); wire_clean "$f"
|
||||||
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha"
|
rm -f "$f/home/.config/crush/skills"
|
||||||
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
|
mkdir -p "$f/home/.config/crush/skills/alpha"
|
||||||
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
|
printf -- '---\nname: alpha\ndescription: a\n---\n' > "$f/home/.config/crush/skills/alpha/SKILL.md"
|
||||||
assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"'
|
assert_fails "$f" 'not a link' 'real directory is reported as unmanaged, not as "outside"'
|
||||||
|
|
||||||
@@ -133,5 +166,130 @@ mkdir -p "$f/home/.claude/skills"
|
|||||||
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
|
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
|
||||||
assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported'
|
assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported'
|
||||||
|
|
||||||
|
# --- 8. Claude Code plugin pinned to an older version than is available -----
|
||||||
|
# The plugin is the ONLY delivery channel for Claude Code, and Claude Code
|
||||||
|
# gates updates on plugin.json's version field. A host that never picked the
|
||||||
|
# new one up loads every skill successfully — just the wrong generation. Found
|
||||||
|
# live on koala 2026-08-29: installed 0.6.0, marketplace 0.7.0, repo 0.8.5,
|
||||||
|
# and doctor said "clean" (#30).
|
||||||
|
|
||||||
|
# installed_state <fixture> <version> — the Claude Code install record.
|
||||||
|
installed_state() {
|
||||||
|
mkdir -p "$1/home/.claude/plugins"
|
||||||
|
cat > "$1/home/.claude/plugins/installed_plugins.json" <<JSON
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"plugins": {
|
||||||
|
"skills@mathias": [
|
||||||
|
{
|
||||||
|
"scope": "user",
|
||||||
|
"installPath": "/somewhere/$2",
|
||||||
|
"version": "$2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
# manifest <dir> <version> — a .claude-plugin/plugin.json under <dir>.
|
||||||
|
manifest() {
|
||||||
|
mkdir -p "$1/.claude-plugin"
|
||||||
|
printf '{\n "name": "skills",\n "version": "%s"\n}\n' "$2" > "$1/.claude-plugin/plugin.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(new_fixture pluginmatch); wire_clean "$f"
|
||||||
|
installed_state "$f" 0.8.0
|
||||||
|
manifest "$f/ck" 0.8.0
|
||||||
|
assert_clean "$f" 'plugin on the current version stays silent'
|
||||||
|
|
||||||
|
f=$(new_fixture pluginstale); wire_clean "$f"
|
||||||
|
installed_state "$f" 0.7.0
|
||||||
|
manifest "$f/ck" 0.8.0
|
||||||
|
assert_fails "$f" '0.7.0' 'stale plugin version is reported'
|
||||||
|
assert_fails "$f" '0.8.0' 'stale plugin report names the available version'
|
||||||
|
|
||||||
|
# A host that only uses the symlink path has no install record at all. That is
|
||||||
|
# a valid configuration and must not fail.
|
||||||
|
f=$(new_fixture pluginabsent); wire_clean "$f"
|
||||||
|
manifest "$f/ck" 0.8.0
|
||||||
|
assert_clean "$f" 'no Claude Code install record is not a fault'
|
||||||
|
|
||||||
|
# The marketplace clone is what Claude Code compares against, so a newer
|
||||||
|
# version there means an update is waiting even when the local checkout agrees
|
||||||
|
# with what is installed.
|
||||||
|
f=$(new_fixture pluginmarket); wire_clean "$f"
|
||||||
|
installed_state "$f" 0.7.0
|
||||||
|
manifest "$f/ck" 0.7.0
|
||||||
|
manifest "$f/home/.claude/plugins/marketplaces/mathias" 0.9.0
|
||||||
|
assert_fails "$f" '0.9.0' 'a newer marketplace clone is reported'
|
||||||
|
|
||||||
|
# --- 9. --online: the release this host has never fetched ------------------
|
||||||
|
# Everything above compares copies already on the host, so a host where the
|
||||||
|
# checkout, the marketplace clone and the installed plugin all agree reads as
|
||||||
|
# clean while being a release behind origin. Offline by design (#30); this is
|
||||||
|
# the opt-in that closes it (#38).
|
||||||
|
#
|
||||||
|
# The remote lookup is stubbed through $DOCTOR_LATEST_TAG_CMD so no test needs
|
||||||
|
# a network or a real remote.
|
||||||
|
|
||||||
|
online_fixture() {
|
||||||
|
_f=$(new_fixture "$1"); wire_clean "$_f"
|
||||||
|
installed_state "$_f" "$2"
|
||||||
|
manifest "$_f/ck" "$2"
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# assert_online <fixture> <stub-tag> <want-rc> <needle> <label>
|
||||||
|
assert_online() {
|
||||||
|
_out=$(HOME="$1/home" DOCTOR_CHECKOUT="$1/ck" \
|
||||||
|
DOCTOR_LATEST_TAG_CMD="printf %s\\n $2" sh "$DOCTOR" --online 2>&1); _rc=$?
|
||||||
|
if [ "$3" -eq 0 ] && [ "$_rc" -ne 0 ]; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s\n exited %s on a current host\n got: %s\n' "$5" "$_rc" "$_out"; return
|
||||||
|
fi
|
||||||
|
if [ "$3" -ne 0 ] && [ "$_rc" -eq 0 ]; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s\n exited 0; staleness went unreported\n' "$5"; return
|
||||||
|
fi
|
||||||
|
if [ -n "$4" ] && ! printf '%s' "$_out" | grep -q -- "$4"; then
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s\n report never mentions "%s"\n got: %s\n' "$5" "$4" "$_out"; return
|
||||||
|
fi
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$5"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(online_fixture behind 0.8.0)
|
||||||
|
assert_online "$f" v0.9.0 1 '0.9.0' '--online reports a release the host has not fetched'
|
||||||
|
|
||||||
|
f=$(online_fixture current 0.9.0)
|
||||||
|
assert_online "$f" v0.9.0 0 '' '--online stays quiet when the host is current'
|
||||||
|
|
||||||
|
# A host AHEAD of the newest tag is the normal state between a release commit
|
||||||
|
# and its tag; that is not drift (same reasoning as check's ordering rule).
|
||||||
|
f=$(online_fixture ahead 0.9.1)
|
||||||
|
assert_online "$f" v0.9.0 0 '' '--online does not complain about being ahead'
|
||||||
|
|
||||||
|
# Without the flag, no lookup happens at all — the stub would report a newer
|
||||||
|
# tag and must be ignored.
|
||||||
|
f=$(online_fixture offline 0.8.0)
|
||||||
|
_out=$(HOME="$f/home" DOCTOR_CHECKOUT="$f/ck" \
|
||||||
|
DOCTOR_LATEST_TAG_CMD="printf v9.9.9\\n" sh "$DOCTOR" 2>&1); _rc=$?
|
||||||
|
if [ "$_rc" -eq 0 ]; then
|
||||||
|
pass=$((pass + 1)); printf ' ok the default run performs no remote lookup\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL the default run performs no remote lookup\n got: %s\n' "$_out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A remote that cannot be reached is not drift and must not be reported as it.
|
||||||
|
# An unreachable remote produces no output at all, so it gets its own run
|
||||||
|
# rather than an empty stub argument — "prints nothing" and "prints an empty
|
||||||
|
# string" are different things and only the first is the case under test.
|
||||||
|
f=$(online_fixture unreachable 0.9.0)
|
||||||
|
_out=$(HOME="$f/home" DOCTOR_CHECKOUT="$f/ck" \
|
||||||
|
DOCTOR_LATEST_TAG_CMD="true" sh "$DOCTOR" --online 2>&1); _rc=$?
|
||||||
|
if [ "$_rc" -ne 0 ] && printf '%s' "$_out" | grep -q 'could not'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok --online reports an unreachable remote as its own fault\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL --online reports an unreachable remote as its own fault\n rc=%s got: %s\n' "$_rc" "$_out"
|
||||||
|
fi
|
||||||
|
|
||||||
printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail"
|
printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
[ "$fail" -eq 0 ]
|
[ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/generate-index.
|
||||||
|
#
|
||||||
|
# SKILLS_INDEX.md was maintained by hand and drifted: two skills shipped with
|
||||||
|
# no row at all (#15), because their rows lived on branches squash-merged after
|
||||||
|
# a directory reorg. A gate caught drift after the fact; generating the table
|
||||||
|
# from frontmatter removes the class entirely (#29) — the curated summary and
|
||||||
|
# trigger now live next to the skill they describe, which is the same argument
|
||||||
|
# that made a hand-maintained index the wrong shape.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/bin/generate-index"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/generate_index_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||||
|
no() { fail=$((fail + 1)); printf ' FAIL %s\n%s\n' "$1" "${2:-}"; }
|
||||||
|
|
||||||
|
# skill <root> <name> <surfaces> [summary] [use_when]
|
||||||
|
skill() {
|
||||||
|
mkdir -p "$1/skills/$2"
|
||||||
|
{
|
||||||
|
printf -- '---\nname: %s\nsurfaces: %s\n' "$2" "$3"
|
||||||
|
[ $# -lt 4 ] || printf 'summary: "%s"\n' "$4"
|
||||||
|
[ $# -lt 5 ] || printf 'use_when: "%s"\n' "$5"
|
||||||
|
printf -- 'description: d\n---\n'
|
||||||
|
} > "$1/skills/$2/SKILL.md"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fixture <name> — a root with an index carrying the generated-table markers.
|
||||||
|
fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f"
|
||||||
|
cat > "$_f/SKILLS_INDEX.md" <<'IDX'
|
||||||
|
# Engineering Skills Index
|
||||||
|
|
||||||
|
<!-- BEGIN skills-table -->
|
||||||
|
<!-- END skills-table -->
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
prose that must survive regeneration
|
||||||
|
IDX
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(fixture basic)
|
||||||
|
skill "$f" beta '[claude-code, library]' 'B summary' 'when B'
|
||||||
|
skill "$f" alpha '[chat]' 'A summary' 'when A'
|
||||||
|
out=$(sh "$SUT" "$f" 2>&1); rc=$?
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
no 'generates a table' "$out"
|
||||||
|
elif ! grep -q '^| `alpha` | A summary | when A |' "$f/SKILLS_INDEX.md"; then
|
||||||
|
no 'generates a table' "$(cat "$f/SKILLS_INDEX.md")"
|
||||||
|
elif ! grep -q 'prose that must survive regeneration' "$f/SKILLS_INDEX.md"; then
|
||||||
|
no 'content outside the markers survives' "$(cat "$f/SKILLS_INDEX.md")"
|
||||||
|
elif [ "$(grep -n '`alpha`' "$f/SKILLS_INDEX.md" | cut -d: -f1)" -gt "$(grep -n '`beta`' "$f/SKILLS_INDEX.md" | cut -d: -f1)" ]; then
|
||||||
|
no 'rows are sorted' "$(cat "$f/SKILLS_INDEX.md")"
|
||||||
|
else
|
||||||
|
ok 'generates a sorted table and leaves the rest of the file alone'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The surfaces a skill ships to are the point of #29, so they are visible.
|
||||||
|
grep -q 'chat' "$f/SKILLS_INDEX.md" \
|
||||||
|
&& ok 'the row names the surfaces' \
|
||||||
|
|| no 'the row names the surfaces' "$(cat "$f/SKILLS_INDEX.md")"
|
||||||
|
|
||||||
|
# Idempotent: a second run changes nothing.
|
||||||
|
before=$(cat "$f/SKILLS_INDEX.md")
|
||||||
|
sh "$SUT" "$f" >/dev/null 2>&1
|
||||||
|
[ "$before" = "$(cat "$f/SKILLS_INDEX.md")" ] \
|
||||||
|
&& ok 'regeneration is idempotent' \
|
||||||
|
|| no 'regeneration is idempotent'
|
||||||
|
|
||||||
|
# --check is the CI gate: clean tree passes.
|
||||||
|
sh "$SUT" --check "$f" >/dev/null 2>&1 \
|
||||||
|
&& ok '--check passes on a generated index' \
|
||||||
|
|| no '--check passes on a generated index'
|
||||||
|
|
||||||
|
# ...and fails once a skill appears without regenerating.
|
||||||
|
skill "$f" gamma '[library]' 'G summary' 'when G'
|
||||||
|
if sh "$SUT" --check "$f" >/dev/null 2>&1; then
|
||||||
|
no '--check fails on a stale index'
|
||||||
|
else
|
||||||
|
ok '--check fails on a stale index'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A skill with no curated summary is an error, not an empty cell: the table
|
||||||
|
# cell is what a reader scans, and a blank one is worse than a missing row.
|
||||||
|
f=$(fixture nosummary)
|
||||||
|
skill "$f" alpha '[library]'
|
||||||
|
out=$(sh "$SUT" "$f" 2>&1); rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
no 'a missing summary is an error' "$out"
|
||||||
|
elif ! printf '%s' "$out" | grep -q alpha; then
|
||||||
|
no 'a missing summary is an error' "$out"
|
||||||
|
else
|
||||||
|
ok 'a missing summary is an error naming the skill'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\ngenerate_index: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for install.sh's wiring model.
|
||||||
|
#
|
||||||
|
# The regression these exist for: install.sh wired each harness with ONE
|
||||||
|
# directory symlink to this checkout, which quietly encoded "this checkout is
|
||||||
|
# the only source of skills on the host". On flamingo the Crush directory held
|
||||||
|
# Anthropic's official skill set, and the only way to install this library was
|
||||||
|
# to displace it — a real capability loss dressed up as a tidy-up (#27).
|
||||||
|
#
|
||||||
|
# Fixtures drive install.sh entirely through env seams, so no test touches the
|
||||||
|
# real host wiring.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/install.sh"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
skipped=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/install_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||||
|
no() { fail=$((fail + 1)); printf ' FAIL %s\n%s\n' "$1" "${2:-}"; }
|
||||||
|
|
||||||
|
# skill <dir> <name> — a minimal valid skill.
|
||||||
|
skill() {
|
||||||
|
mkdir -p "$1/$2"
|
||||||
|
printf -- '---\nname: %s\ndescription: d\n---\n' "$2" > "$1/$2/SKILL.md"
|
||||||
|
}
|
||||||
|
|
||||||
|
# new_fixture <name> [extra-source-names...] — checkout with alpha+beta.
|
||||||
|
new_fixture() {
|
||||||
|
_f="$WORK/$1"
|
||||||
|
mkdir -p "$_f/ck/skills" "$_f/home"
|
||||||
|
skill "$_f/ck/skills" alpha
|
||||||
|
skill "$_f/ck/skills" beta
|
||||||
|
printf '%s' "$_f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_install <fixture> [extra-sources] — echoes output, sets $rc.
|
||||||
|
run_install() {
|
||||||
|
_f=$1
|
||||||
|
_extra=${2:-}
|
||||||
|
_out=$(HOME="$_f/home" \
|
||||||
|
SKILLS_SKIP_FETCH=1 \
|
||||||
|
SKILLS_CHECKOUT_DIR="$_f/ck" \
|
||||||
|
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
|
||||||
|
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
|
||||||
|
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
|
||||||
|
ANTIGRAVITY_SKILLS_JSON="$_f/home/.gemini/config/skills.json" \
|
||||||
|
SKILLS_EXTRA_SOURCES="$_extra" \
|
||||||
|
bash "$SUT" 2>&1)
|
||||||
|
rc=$?
|
||||||
|
printf '%s' "$_out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. the new shape: a real directory of per-skill links -------------------
|
||||||
|
f=$(new_fixture fresh)
|
||||||
|
out=$(run_install "$f")
|
||||||
|
A="$f/home/.agents/skills"
|
||||||
|
if [ ! -d "$A" ] || [ -L "$A" ]; then
|
||||||
|
no 'agents dir is a real directory' "$out"
|
||||||
|
elif [ ! -L "$A/alpha" ] || [ ! -L "$A/beta" ]; then
|
||||||
|
no 'each skill is its own link' "$out"
|
||||||
|
elif [ ! -L "$f/home/.config/crush/skills" ] || [ ! -L "$f/home/.config/opencode/skills" ]; then
|
||||||
|
no 'harnesses keep one directory link each' "$out"
|
||||||
|
else
|
||||||
|
ok 'fresh install builds per-skill links under a real agents dir'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2. a second source is served WITHOUT displacing the first --------------
|
||||||
|
f=$(new_fixture twosource)
|
||||||
|
mkdir -p "$f/other"
|
||||||
|
skill "$f/other" xlsx
|
||||||
|
out=$(run_install "$f" "$f/other")
|
||||||
|
if [ -L "$f/home/.agents/skills/xlsx" ] && [ -L "$f/home/.agents/skills/alpha" ]; then
|
||||||
|
ok 'a second source is linked alongside the checkout'
|
||||||
|
else
|
||||||
|
no 'a second source is linked alongside the checkout' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3. name collisions are REPORTED, never silently resolved ---------------
|
||||||
|
# Two sources offering `code-review` is a real possibility, and picking one
|
||||||
|
# quietly is how stale content wins.
|
||||||
|
f=$(new_fixture collide)
|
||||||
|
mkdir -p "$f/other"
|
||||||
|
skill "$f/other" alpha
|
||||||
|
out=$(run_install "$f" "$f/other")
|
||||||
|
if printf '%s' "$out" | grep -qi 'collision' \
|
||||||
|
&& printf '%s' "$out" | grep -q "$f/other/alpha"; then
|
||||||
|
target=$(readlink "$f/home/.agents/skills/alpha")
|
||||||
|
if [ "$target" = "$f/ck/skills/alpha" ]; then
|
||||||
|
ok 'a name collision is reported and the checkout wins'
|
||||||
|
else
|
||||||
|
no 'a name collision is reported and the checkout wins' "resolved to $target"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
no 'a name collision is reported and the checkout wins' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. removing a skill from a source removes its link ---------------------
|
||||||
|
# Without this the previous generation's problem returns in a new shape: links
|
||||||
|
# that resolve to nothing, invisible to git.
|
||||||
|
f=$(new_fixture prune)
|
||||||
|
out=$(run_install "$f")
|
||||||
|
rm -rf "$f/ck/skills/beta"
|
||||||
|
out=$(run_install "$f")
|
||||||
|
if [ -e "$f/home/.agents/skills/beta" ] || [ -L "$f/home/.agents/skills/beta" ]; then
|
||||||
|
no 'a removed skill loses its link' "$out"
|
||||||
|
else
|
||||||
|
ok 'a removed skill loses its link'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 5. migration from the old single-link shape ----------------------------
|
||||||
|
f=$(new_fixture migrate)
|
||||||
|
mkdir -p "$f/home/.agents"
|
||||||
|
ln -s "$f/ck/skills" "$f/home/.agents/skills"
|
||||||
|
out=$(run_install "$f")
|
||||||
|
if [ -L "$f/home/.agents/skills" ]; then
|
||||||
|
no 'the old single-link wiring is migrated in place' "$out"
|
||||||
|
elif [ -L "$f/home/.agents/skills/alpha" ]; then
|
||||||
|
ok 'the old single-link wiring is migrated in place'
|
||||||
|
else
|
||||||
|
no 'the old single-link wiring is migrated in place' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 6. unmanaged real content in the agents dir is never deleted -----------
|
||||||
|
f=$(new_fixture protect)
|
||||||
|
mkdir -p "$f/home/.agents/skills"
|
||||||
|
skill "$f/home/.agents/skills" handwritten
|
||||||
|
out=$(run_install "$f")
|
||||||
|
if [ -f "$f/home/.agents/skills/handwritten/SKILL.md" ] \
|
||||||
|
&& printf '%s' "$out" | grep -qi 'unmanaged'; then
|
||||||
|
ok 'unmanaged content is reported and left in place'
|
||||||
|
else
|
||||||
|
no 'unmanaged content is reported and left in place' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 7. the source list is recorded for doctor ------------------------------
|
||||||
|
f=$(new_fixture manifest)
|
||||||
|
mkdir -p "$f/other"
|
||||||
|
skill "$f/other" xlsx
|
||||||
|
out=$(run_install "$f" "$f/other")
|
||||||
|
if [ -f "$f/home/.agents/skills-sources" ] \
|
||||||
|
&& grep -q "$f/other" "$f/home/.agents/skills-sources" \
|
||||||
|
&& grep -q "$f/ck/skills" "$f/home/.agents/skills-sources"; then
|
||||||
|
ok 'configured sources are recorded for doctor'
|
||||||
|
else
|
||||||
|
no 'configured sources are recorded for doctor' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Antigravity: jq is optional, so BOTH branches must be tested -----------
|
||||||
|
# These three cases inherited whatever jq the host happened to have. koala has
|
||||||
|
# it, the CI runner does not, so they passed locally and failed in CI — the
|
||||||
|
# classic works-on-my-machine gate. install.sh treats jq as optional by design,
|
||||||
|
# which means the skip is a contract and needs a test of its own.
|
||||||
|
|
||||||
|
# run_install_nojq <fixture> — same as run_install, with the jq lookup pointed
|
||||||
|
# at a binary that does not exist. Deterministic regardless of the host.
|
||||||
|
run_install_nojq() {
|
||||||
|
_f=$1
|
||||||
|
_out=$(HOME="$_f/home" \
|
||||||
|
SKILLS_SKIP_FETCH=1 \
|
||||||
|
SKILLS_CHECKOUT_DIR="$_f/ck" \
|
||||||
|
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
|
||||||
|
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
|
||||||
|
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
|
||||||
|
SKILLS_JQ="definitely-not-jq-$$" \
|
||||||
|
bash "$SUT" 2>&1)
|
||||||
|
rc=$?
|
||||||
|
printf '%s' "$_out"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(new_fixture antigravity_nojq)
|
||||||
|
out=$(run_install_nojq "$f")
|
||||||
|
if [ -e "$f/home/.gemini/config/skills.json" ]; then
|
||||||
|
no 'without jq, no skills.json is written' "$out"
|
||||||
|
elif ! printf '%s' "$out" | grep -qi 'jq not found'; then
|
||||||
|
no 'without jq, the skip is reported' "$out"
|
||||||
|
elif [ ! -L "$f/home/.agents/skills/alpha" ]; then
|
||||||
|
no 'without jq, the other harnesses are still wired' "$out"
|
||||||
|
else
|
||||||
|
ok 'without jq, Antigravity is skipped and the rest still wires'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 8. surfaces: the library link set is not "every skill" -----------------
|
||||||
|
# claude.ai has no shell, no cluster and no checkout, so a chat-only skill
|
||||||
|
# linked into Crush is a skill that can never fire, competing for selection
|
||||||
|
# with ones that can (#29).
|
||||||
|
f=$(new_fixture surfaces)
|
||||||
|
mkdir -p "$f/ck/skills/chatonly"
|
||||||
|
printf -- '---\nname: chatonly\nsurfaces: [chat]\ndescription: c\n---\n' > "$f/ck/skills/chatonly/SKILL.md"
|
||||||
|
printf -- '---\nname: alpha\nsurfaces: [claude-code, library]\ndescription: a\n---\n' > "$f/ck/skills/alpha/SKILL.md"
|
||||||
|
out=$(run_install "$f")
|
||||||
|
if [ -L "$f/home/.agents/skills/alpha" ] && [ ! -e "$f/home/.agents/skills/chatonly" ]; then
|
||||||
|
ok 'a skill that does not ship to the library is not linked'
|
||||||
|
else
|
||||||
|
no 'a skill that does not ship to the library is not linked' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A foreign source has no reason to know this convention, so no declaration
|
||||||
|
# means "link it" — otherwise adding Anthropic's set would link nothing.
|
||||||
|
f=$(new_fixture foreign)
|
||||||
|
mkdir -p "$f/other/xlsx"
|
||||||
|
printf -- '---\nname: xlsx\ndescription: x\n---\n' > "$f/other/xlsx/SKILL.md"
|
||||||
|
out=$(run_install "$f" "$f/other")
|
||||||
|
if [ -L "$f/home/.agents/skills/xlsx" ]; then
|
||||||
|
ok 'a source with no surfaces convention is linked as-is'
|
||||||
|
else
|
||||||
|
no 'a source with no surfaces convention is linked as-is' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 9-11. Antigravity's merge behaviour, only where jq exists ---------------
|
||||||
|
# These three assert what jq DOES. install.sh treats jq as optional, so on a
|
||||||
|
# host without it there is nothing to assert — and failing there is what turned
|
||||||
|
# main red: they passed on koala (jq present) and failed on the runner (absent).
|
||||||
|
# A skipped case is honest; a case that fails because the host lacks an optional
|
||||||
|
# dependency is a false red, and false reds are how a suite stops being read.
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
skipped=$((skipped + 3))
|
||||||
|
printf ' SKIP jq absent — Antigravity merge behaviour not covered on this host (3 cases)\n'
|
||||||
|
else
|
||||||
|
# skills.json entries must point at $AGENTS_DIR — that carries the
|
||||||
|
# surfaces-filtering, collision handling and multi-source merge. Pointing at
|
||||||
|
# the checkout directly would give Antigravity a different skill set than
|
||||||
|
# every other harness.
|
||||||
|
f=$(new_fixture antigravity)
|
||||||
|
out=$(run_install "$f")
|
||||||
|
AG="$f/home/.gemini/config/skills.json"
|
||||||
|
if [ ! -f "$AG" ]; then
|
||||||
|
no 'Antigravity skills.json is created' "$out"
|
||||||
|
else
|
||||||
|
registered=$(jq -r --arg p "$f/home/.agents/skills" '[.entries[]? | select(.path == $p)] | length' "$AG")
|
||||||
|
if [ "$registered" = "1" ]; then
|
||||||
|
ok 'Antigravity registers the managed agents dir, not the raw checkout'
|
||||||
|
else
|
||||||
|
no 'Antigravity registers the managed agents dir, not the raw checkout' "$(cat "$AG")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
f=$(new_fixture antigravity_idempotent)
|
||||||
|
run_install "$f" >/dev/null
|
||||||
|
out=$(run_install "$f")
|
||||||
|
AG="$f/home/.gemini/config/skills.json"
|
||||||
|
count=$(jq '.entries | length' "$AG" 2>/dev/null || echo "?")
|
||||||
|
if [ "$count" = "1" ]; then
|
||||||
|
ok 'Antigravity wiring is idempotent (no duplicate entries)'
|
||||||
|
else
|
||||||
|
no 'Antigravity wiring is idempotent (no duplicate entries)' "entries=$count"$'\n'"$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A user may hand-edit skills.json to point at their own personal skills dir
|
||||||
|
# (json_configs.md documents this). Our merge must not clobber it.
|
||||||
|
f=$(new_fixture antigravity_preserve)
|
||||||
|
mkdir -p "$f/home/.gemini/config"
|
||||||
|
printf '{"entries":[{"path":"/Users/mathias/personal-skills"}]}' > "$f/home/.gemini/config/skills.json"
|
||||||
|
out=$(run_install "$f")
|
||||||
|
AG="$f/home/.gemini/config/skills.json"
|
||||||
|
personal=$(jq -r '[.entries[]? | select(.path == "/Users/mathias/personal-skills")] | length' "$AG" 2>/dev/null || echo 0)
|
||||||
|
if [ "$personal" = "1" ]; then
|
||||||
|
ok 'Antigravity wiring preserves pre-existing hand-authored entries'
|
||||||
|
else
|
||||||
|
no 'Antigravity wiring preserves pre-existing hand-authored entries' "$(cat "$AG")"$'\n'"$out"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\ninstall: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skipped"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for skills/lineage/lineage.sh — the decision-archaeology helper.
|
||||||
|
#
|
||||||
|
# The techniques this automates are the ones people skip because they are
|
||||||
|
# fiddly to type: `git log -S` to find when a thing was introduced AND removed,
|
||||||
|
# --diff-filter=D to find files that once existed, and grepping decision records
|
||||||
|
# for the term. A skill that only *describes* those is a skill that gets read and
|
||||||
|
# not run (#11, #19's "small robust tools over prose").
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/skills/lineage/lineage.sh"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/lineage_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# A repo where a thing was built, written about, and then removed.
|
||||||
|
mkdir -p "$WORK/repo/docs/adr"
|
||||||
|
cd "$WORK/repo" || exit 1
|
||||||
|
git init -q .
|
||||||
|
git config user.email t@example.com
|
||||||
|
git config user.name test
|
||||||
|
|
||||||
|
printf 'func WidgetCache() {}\n' > cache.go
|
||||||
|
git add -A && git commit -qm "feat: add WidgetCache for lookup speed"
|
||||||
|
|
||||||
|
printf -- '---\nadr: 0001\ntitle: use a WidgetCache\n---\nWe chose WidgetCache over a database index.\n' > docs/adr/0001-widgetcache.md
|
||||||
|
git add -A && git commit -qm "docs: record the WidgetCache decision"
|
||||||
|
|
||||||
|
git rm -q cache.go
|
||||||
|
git commit -qm "revert: drop WidgetCache, it went stale under concurrent writes"
|
||||||
|
|
||||||
|
cd - >/dev/null || exit 1
|
||||||
|
out=$(sh "$SUT" WidgetCache "$WORK/repo" 2>&1)
|
||||||
|
|
||||||
|
check() {
|
||||||
|
if printf '%s' "$out" | grep -q "$1"; then
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$2"
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s\n output had no %s\n%s\n' "$2" "$1" "$out"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check 'add WidgetCache' 'finds the commit that introduced it'
|
||||||
|
check 'drop WidgetCache' 'finds the commit that removed it — the revival question'
|
||||||
|
check '0001-widgetcache.md' 'finds the decision record naming it'
|
||||||
|
check 'cache.go' 'names the file that no longer exists'
|
||||||
|
|
||||||
|
# A no-match must NOT be reported as evidence the idea is new. The first version
|
||||||
|
# of this tool said "it means this is new here" and a pressure test caught it
|
||||||
|
# asserting exactly that about the most-discussed removed feature in this repo —
|
||||||
|
# the search term simply had a space in it. That is the inference the skill's own
|
||||||
|
# anti-pattern table forbids, printed in the tool's voice.
|
||||||
|
out=$(sh "$SUT" NeverExisted "$WORK/repo" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q 'NOT evidence the idea is new' \
|
||||||
|
&& printf '%s' "$out" | grep -qi 'identifier'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok a no-match refuses to imply novelty and suggests other terms\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL a no-match refuses to imply novelty and suggests other terms\n%s\n' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The repo actually searched is always stated — a confident null against the
|
||||||
|
# wrong tree is this tool's worst failure mode, and the default is the cwd.
|
||||||
|
out=$(sh "$SUT" WidgetCache "$WORK/repo" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q "searching: "; then
|
||||||
|
pass=$((pass + 1)); printf ' ok names the repo it searched\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL names the repo it searched\n%s\n' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The sources it cannot reach are named every run, so their absence never reads
|
||||||
|
# as a null result.
|
||||||
|
if printf '%s' "$out" | grep -q 'brain_query' && printf '%s' "$out" | grep -qi 'closed issues'; then
|
||||||
|
pass=$((pass + 1)); printf ' ok names the sources it cannot search\n'
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL names the sources it cannot search\n%s\n' "$out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nlineage: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Executable
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/check-mcp-env.
|
||||||
|
#
|
||||||
|
# The fault this gate exists for is silent, which is why it needs a gate at
|
||||||
|
# all. An `.mcp.json` server whose Authorization header interpolates a
|
||||||
|
# variable nothing in the session environment defines does not error: the
|
||||||
|
# header goes out empty, the server answers 401, and Claude Code reports
|
||||||
|
# "requires authentication". That is indistinguishable from a revoked or
|
||||||
|
# expired credential, and on 2026-09-07 it cost a full investigation on koala
|
||||||
|
# to establish that the credential was fine and the variable was simply never
|
||||||
|
# set.
|
||||||
|
#
|
||||||
|
# So the property is: every ${VAR} a LIVE server interpolates must be a
|
||||||
|
# variable the session env-file actually defines.
|
||||||
|
#
|
||||||
|
# The `_parked` block is deliberately exempt. Parked servers reference
|
||||||
|
# variables nothing provides on purpose -- that is what parked means -- and a
|
||||||
|
# gate that flagged them would be reporting the intended state as a fault.
|
||||||
|
#
|
||||||
|
# Fixtures drive the checker through argv, so no test touches the real host.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
CHECK="$ROOT/bin/check-mcp-env"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/mcp_config_test.$$
|
||||||
|
|
||||||
|
if [ ! -f "$CHECK" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$CHECK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
printf 'SKIP: jq not installed -- bin/check-mcp-env cannot be exercised\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ok <name> / bad <name> <detail>
|
||||||
|
ok() { pass=$((pass+1)); printf ' ok %s\n' "$1"; }
|
||||||
|
bad() { fail=$((fail+1)); printf ' FAIL %s %s\n' "$1" "${2:-}"; }
|
||||||
|
|
||||||
|
# run <mcp-json> <env-file> -> sets $rc and $out
|
||||||
|
run() {
|
||||||
|
out=$("$CHECK" "$1" "$2" 2>&1); rc=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
ENVFILE="$WORK/op-env"
|
||||||
|
cat > "$ENVFILE" <<'ENVEOF'
|
||||||
|
BRAIN_MCP_TOKEN=op://HomeLab/brain/token
|
||||||
|
DMABE_GITEA_API_TOKEN=op://HomeLab/gitea/token
|
||||||
|
ENVEOF
|
||||||
|
|
||||||
|
# --- a live server whose variable IS defined ---------------------------------
|
||||||
|
cat > "$WORK/good.json" <<'JSONEOF'
|
||||||
|
{"mcpServers":{"brain":{"type":"http","url":"https://x/mcp",
|
||||||
|
"headers":{"Authorization":"Bearer ${BRAIN_MCP_TOKEN}"}}}}
|
||||||
|
JSONEOF
|
||||||
|
run "$WORK/good.json" "$ENVFILE"
|
||||||
|
[ "$rc" -eq 0 ] && ok "defined variable passes" || bad "defined variable passes" "rc=$rc $out"
|
||||||
|
|
||||||
|
# --- a live server whose variable is NOT defined ------------------------------
|
||||||
|
# The exact shape that broke: the server looks correct and fails at runtime.
|
||||||
|
cat > "$WORK/missing.json" <<'JSONEOF'
|
||||||
|
{"mcpServers":{"gitea":{"type":"http","url":"https://x/mcp",
|
||||||
|
"headers":{"Authorization":"Bearer ${GITEA_MCP_STATIC_TOKEN}"}}}}
|
||||||
|
JSONEOF
|
||||||
|
run "$WORK/missing.json" "$ENVFILE"
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
case "$out" in
|
||||||
|
*gitea*GITEA_MCP_STATIC_TOKEN*) ok "undefined variable is flagged, naming server and variable" ;;
|
||||||
|
*) bad "undefined variable is flagged, naming server and variable" "report did not name both: $out" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
bad "undefined variable is flagged, naming server and variable" "rc=0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- a server with no headers at all (the `infra` shape) ----------------------
|
||||||
|
# Unauthenticated by design. Nothing to resolve, so nothing to flag.
|
||||||
|
cat > "$WORK/noauth.json" <<'JSONEOF'
|
||||||
|
{"mcpServers":{"infra":{"type":"http","url":"https://x/mcp"}}}
|
||||||
|
JSONEOF
|
||||||
|
run "$WORK/noauth.json" "$ENVFILE"
|
||||||
|
[ "$rc" -eq 0 ] && ok "header-less server is not flagged" || bad "header-less server is not flagged" "rc=$rc $out"
|
||||||
|
|
||||||
|
# --- parked servers are exempt ------------------------------------------------
|
||||||
|
cat > "$WORK/parked.json" <<'JSONEOF'
|
||||||
|
{"mcpServers":{"brain":{"type":"http","url":"https://x/mcp",
|
||||||
|
"headers":{"Authorization":"Bearer ${BRAIN_MCP_TOKEN}"}}},
|
||||||
|
"_parked":{"_reason":"no identity provides these yet",
|
||||||
|
"cad":{"type":"http","url":"https://x/mcp",
|
||||||
|
"headers":{"Authorization":"Bearer ${CAD_MCP_TOKEN}"}}}}
|
||||||
|
JSONEOF
|
||||||
|
run "$WORK/parked.json" "$ENVFILE"
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
ok "parked server is exempt"
|
||||||
|
else
|
||||||
|
bad "parked server is exempt" "rc=$rc $out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- malformed JSON must not pass silently ------------------------------------
|
||||||
|
# A config the checker cannot read is an unverified config, not a clean one.
|
||||||
|
printf '{"mcpServers":{' > "$WORK/broken.json"
|
||||||
|
run "$WORK/broken.json" "$ENVFILE"
|
||||||
|
[ "$rc" -ne 0 ] && ok "unparseable config does not report success" || bad "unparseable config does not report success" "rc=0"
|
||||||
|
|
||||||
|
# --- a missing env-file is unverifiable, not clean ----------------------------
|
||||||
|
run "$WORK/good.json" "$WORK/no-such-env"
|
||||||
|
[ "$rc" -ne 0 ] && ok "missing env-file does not report success" || bad "missing env-file does not report success" "rc=0"
|
||||||
|
|
||||||
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for bin/newer-version.
|
||||||
|
#
|
||||||
|
# Extracted from bin/doctor (#30) once `task check` needed the same comparison
|
||||||
|
# (#34): check asserted plugin.json == latest tag, which is red on any checkout
|
||||||
|
# that has not fetched the tag the CI tagger just pushed. The assertion it
|
||||||
|
# actually wants is "not behind", and ordering needs a real comparison.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/bin/newer-version"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# expect <winner> <a> <b> <label>
|
||||||
|
expect() {
|
||||||
|
_got=$(sh "$SUT" "$2" "$3")
|
||||||
|
if [ "$_got" = "$1" ]; then
|
||||||
|
pass=$((pass + 1)); printf ' ok %s\n' "$4"
|
||||||
|
else
|
||||||
|
fail=$((fail + 1)); printf ' FAIL %s (newer-version %s %s -> %s, want %s)\n' "$4" "$2" "$3" "$_got" "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect 0.9.0 0.9.0 0.8.5 'higher minor wins, left'
|
||||||
|
expect 0.9.0 0.8.5 0.9.0 'higher minor wins, right'
|
||||||
|
expect 0.8.10 0.8.10 0.8.9 'numeric compare, not lexical — 10 beats 9'
|
||||||
|
expect 1.0.0 1.0.0 0.99.99 'major dominates'
|
||||||
|
expect 0.8.5 0.8.5 0.8.5 'equal returns the value'
|
||||||
|
expect 0.9.1 0.9.1 0.9 'a missing component counts as zero'
|
||||||
|
expect 0.9 0.9 0.9.0 'equal with a missing component returns the left, per contract'
|
||||||
|
|
||||||
|
printf '\nnewer_version: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user