Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2afb35d1d | ||
|
|
ad17a7c6c0 | ||
|
|
21dbb35526 | ||
|
|
8ad2079f86 | ||
|
|
b188965161 | ||
|
|
ae66840a03 | ||
|
|
e7e514390b | ||
|
|
087685f2e3 | ||
|
|
83b069afba | ||
|
|
a699ad8cbd | ||
|
|
a42ec18f40 | ||
|
|
46ff1acdfe | ||
|
|
3112b4ce92 | ||
|
|
51a8c25b4d | ||
|
|
d40169f2b7 | ||
|
|
d4f0416dc5 | ||
|
|
abd81f939a |
@@ -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.6.2",
|
"version": "0.8.5",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/mathias"
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
|||||||
@@ -1,23 +1,52 @@
|
|||||||
name: release
|
name: CI
|
||||||
|
|
||||||
# Auto-tag every push to main with a semver bump derived from the latest
|
# Two jobs, in order:
|
||||||
# commit footer:
|
|
||||||
# Bump-Type: major → vX.0.0
|
|
||||||
# Bump-Type: minor → v0.Y.0
|
|
||||||
# (default) → v0.0.Z (patch)
|
|
||||||
#
|
#
|
||||||
# Skips when the latest commit is itself the auto-tagger (commit message
|
# gate — `task check` (structure, frontmatter, manifest/tag parity) plus
|
||||||
# contains "[skip-release]") to prevent recursion. Tag is pushed back
|
# `task test` (hooks, doctor, release-skip). Runs on every push and
|
||||||
# to the repo using the GITEA_TOKEN provided by act_runner.
|
# every PR. Before this existed the suite ran nowhere: CI reported
|
||||||
|
# green having tested nothing (#22).
|
||||||
|
#
|
||||||
|
# tag — auto-tags a push to main with a semver bump derived from the latest
|
||||||
|
# commit footer:
|
||||||
|
# Bump-Type: major → vX.0.0
|
||||||
|
# Bump-Type: minor → v0.Y.0
|
||||||
|
# (default) → v0.0.Z (patch)
|
||||||
|
# Skips when the commit carries the `Skip-Release: true` trailer (the
|
||||||
|
# tagger's own manifest bump) to prevent recursion. Gated on `gate`,
|
||||||
|
# so a red check cannot ship a tag.
|
||||||
|
|
||||||
on:
|
"on":
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ["**"]
|
||||||
- main
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag:
|
gate:
|
||||||
|
name: Check / Test
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# `task check` compares plugin.json's version to `git describe`;
|
||||||
|
# a shallow checkout has no tags and would skip that assertion.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Verify toolchain
|
||||||
|
run: task --version
|
||||||
|
|
||||||
|
- name: Structure gate
|
||||||
|
run: task check
|
||||||
|
|
||||||
|
- name: Shell test suite
|
||||||
|
run: task test
|
||||||
|
|
||||||
|
tag:
|
||||||
|
name: Tag release
|
||||||
|
needs: gate
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -72,16 +101,19 @@ jobs:
|
|||||||
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)
|
current=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -1)
|
||||||
|
git tag -a "$TAG" -m "release $TAG"
|
||||||
|
|
||||||
if [ "$current" != "$version" ]; then
|
if [ "$current" != "$version" ]; then
|
||||||
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"
|
||||||
git push origin HEAD:main
|
# Commit and tag go up in ONE push: the gate job runs `task check`
|
||||||
|
# on the bump commit, and that asserts manifest == latest tag. Two
|
||||||
|
# separate pushes leave a window where it legitimately fails.
|
||||||
|
git push origin HEAD:main "$TAG"
|
||||||
echo "bumped $manifest to $version"
|
echo "bumped $manifest to $version"
|
||||||
|
else
|
||||||
|
git push origin "$TAG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git tag -a "$TAG" -m "release $TAG"
|
|
||||||
# gitea-actions checkout uses an x-access-token URL; reuse it.
|
|
||||||
git push origin "$TAG"
|
|
||||||
echo "pushed $TAG"
|
echo "pushed $TAG"
|
||||||
+13
-9
@@ -19,9 +19,11 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
|
|||||||
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
|
| `gitea-ci` | Gitea CI/CD pipeline with act_runner, buildah, k3s, GitHub mirror | Setting up or debugging CI on this infra |
|
||||||
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
|
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious |
|
||||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project |
|
||||||
|
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature |
|
||||||
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
| `playwright` | Playwright E2E testing — setup, HTMX patterns, SSE streams, CI wiring | Adding browser tests to any project with a web UI |
|
||||||
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
| `session-retrospective` | Surface non-obvious learnings from a session log | After a coding session ends, before context is lost |
|
||||||
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
| `trainer` | Two-phase brain curation (score candidates, write past quality gate) | Periodic brain pruning and curation |
|
||||||
|
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index |
|
||||||
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
|
| `grill-me` | Structured plan interrogation — Quick Poke, Full Grill, Pre-mortem | Stress-testing a plan before committing; end of Diamond 1; before promoting to pre-prod |
|
||||||
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
|
| `own-the-task` | Delegation ownership — Intake → Brief → Plan → Execute | A task is handed over open-ended; "take this off my plate"; before a plan exists |
|
||||||
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
| `telos-load` | Load TELOS intention substrate at session start | Starting any koala session; before architectural decisions; CAD pipeline entry |
|
||||||
@@ -30,13 +32,13 @@ This index lists all available engineering skills. Load the full SKILL.md on dem
|
|||||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
|
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing |
|
||||||
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
|
| `stage-gate-review` | Stage-gate product governance (go/no-go council) | Preparing a Product Council ask; building a go/no-go checklist; gate review |
|
||||||
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists |
|
| `web-shot` | Screenshot a running web UI via a koala k3s Playwright job | Visual verification of a running UI when no local browser exists |
|
||||||
|
| `cad-babysit` | Focused human-in-the-loop session working CAD's stuck queue (read-only) | Picking up whatever `cad stuck` flags, with a human present |
|
||||||
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
|
| `superpowers-brainstorming` | Structured divergent brainstorming | Generating or expanding a set of ideas |
|
||||||
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
|
| `superpowers-writing-plans` | Writing implementation plans | Planning substantial work before coding |
|
||||||
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion |
|
| `superpowers-executing-plans` | Executing a written plan step by step | Running an approved plan to completion |
|
||||||
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents |
|
| `superpowers-dispatching-parallel-agents` | Fan work out across parallel subagents | Decomposing work for concurrent agents |
|
||||||
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents |
|
| `superpowers-subagent-driven-development` | Subagent-driven development workflow | Building via orchestrated subagents |
|
||||||
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically |
|
| `superpowers-systematic-debugging` | Hypothesis-first systematic debugging | Diagnosing a failure methodically |
|
||||||
| `superpowers-test-driven-development` | TDD discipline (superpowers variant) | Red → green → refactor on a change |
|
|
||||||
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete |
|
| `superpowers-verification-before-completion` | Verify behavior before claiming done | Before marking any task complete |
|
||||||
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review |
|
| `superpowers-requesting-code-review` | Requesting a code review well | Asking another agent/human to review |
|
||||||
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments |
|
| `superpowers-receiving-code-review` | Acting on received review feedback | Responding to review comments |
|
||||||
@@ -106,14 +108,16 @@ problem-analysis
|
|||||||
│
|
│
|
||||||
├── spec-driven-dev (PM-kickoff lifecycle)
|
├── spec-driven-dev (PM-kickoff lifecycle)
|
||||||
│ │
|
│ │
|
||||||
│ └── feature-spec (per-feature, mid-implementation)
|
│ ├── feature-spec (per-feature, mid-implementation)
|
||||||
│ │
|
│ │ │
|
||||||
│ └── planning
|
│ │ └── planning
|
||||||
│ │
|
│ │ │
|
||||||
│ └── atdd ──→ tdd (foundation)
|
│ │ └── atdd ──→ tdd (foundation)
|
||||||
│ ├──→ debug (when tests fail)
|
│ │ ├──→ debug (when tests fail)
|
||||||
│ ├──→ clean-code (REFACTOR phase)
|
│ │ ├──→ clean-code (REFACTOR phase)
|
||||||
│ └──→ test-design (review)
|
│ │ └──→ test-design (review)
|
||||||
|
│ │
|
||||||
|
│ └── experiment-spec (research/ML phase, instead of feature-spec)
|
||||||
│
|
│
|
||||||
└── planning
|
└── planning
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -94,7 +94,12 @@ tasks:
|
|||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
# 3. Plugin manifest version must match the latest tag, or installers
|
# 3. SKILLS_INDEX.md lists exactly the skills that exist. Delegated
|
||||||
|
# to bin/check-index so the rule is covered by
|
||||||
|
# tests/check_index_test.sh rather than living untested here.
|
||||||
|
sh '{{.TASKFILE_DIR}}/bin/check-index' '{{.TASKFILE_DIR}}' || fail=1
|
||||||
|
|
||||||
|
# 4. 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)
|
||||||
|
|||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/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
|
||||||
+64
-2
@@ -109,6 +109,15 @@ do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A real directory where a link belongs holds content nothing manages: it
|
||||||
|
# cannot receive new skills and silently shadows the checkout. Distinguished
|
||||||
|
# from the foreign-checkout case below because the fix differs — move it
|
||||||
|
# aside and re-run install.sh, rather than re-point a link.
|
||||||
|
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"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
# Resolve to a physical path and confirm it lands inside THIS checkout.
|
# 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
|
# A link that resolves to a different checkout is the dangerous case: it
|
||||||
# works, so nothing complains, while serving stale content.
|
# works, so nothing complains, while serving stale content.
|
||||||
@@ -171,10 +180,63 @@ 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.
|
||||||
|
newer_version() {
|
||||||
|
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]
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
installed_v=''
|
||||||
|
if [ -f "$INSTALLED" ]; then
|
||||||
|
installed_v=$(sed -n '/"skills@mathias"/,/\]/p' "$INSTALLED" \
|
||||||
|
| sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
available_v=''
|
||||||
|
for manifest in "$CHECKOUT/.claude-plugin/plugin.json" \
|
||||||
|
"$HOME"/.claude/plugins/marketplaces/*/.claude-plugin/plugin.json
|
||||||
|
do
|
||||||
|
v=$(manifest_version "$manifest") || continue
|
||||||
|
[ -n "$v" ] || continue
|
||||||
|
if [ -z "$available_v" ]; then
|
||||||
|
available_v=$v
|
||||||
|
else
|
||||||
|
available_v=$(newer_version "$available_v" "$v")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$installed_v" ] && [ -n "$available_v" ] && [ "$installed_v" != "$available_v" ]; then
|
||||||
|
add FAIL plugin-version "Claude Code has skills@mathias $installed_v installed but $available_v is available on this host — /plugin update skills@mathias (a host stale enough to matter is running the old doctor, so the first detection may need this run by hand)"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- output -----------------------------------------------------------------
|
# --- output -----------------------------------------------------------------
|
||||||
if [ "$JSON" -eq 1 ]; then
|
if [ "$JSON" -eq 1 ]; then
|
||||||
printf '{"checkout":"%s","expected":%s,"findings":[%s]}\n' \
|
printf '{"checkout":"%s","expected":%s,"plugin":{"installed":"%s","available":"%s"},"findings":[%s]}\n' \
|
||||||
"$CHECKOUT" "$EXPECTED" "${report%,}"
|
"$CHECKOUT" "$EXPECTED" "$installed_v" "$available_v" "${report%,}"
|
||||||
elif [ "$findings" -gt 0 ]; then
|
elif [ "$findings" -gt 0 ]; then
|
||||||
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
|
printf 'skills doctor — %s finding(s), checkout %s\n\n%s\n' \
|
||||||
"$findings" "$CHECKOUT" "$report"
|
"$findings" "$CHECKOUT" "$report"
|
||||||
|
|||||||
+12
-5
@@ -50,11 +50,18 @@ if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)op\s+document\s+get\b'; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Raw env/secret-file dumps (infra#228/#229 root cause).
|
# Raw env/secret-file dumps (infra#228/#229 root cause).
|
||||||
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)env(\s|$).*\|.*grep'; then
|
#
|
||||||
deny "Blocked: 'env | grep' prints matching values in full. Use an existence-only check: [ -n \"\$VAR\" ] && echo set"
|
# `env` is a dump only when nothing but options and VAR=value assignments
|
||||||
fi
|
# follow it. `env -u VAR cmd` and `env FOO=bar cmd` EXEC a command with a
|
||||||
if printf '%s' "$CMD" | grep -Eq '(^|[;&|])[[:space:]]*env[[:space:]]*($|[;&|])'; then
|
# modified environment and print nothing themselves — piping those into grep
|
||||||
deny "Blocked: bare 'env' prints every variable's value, including secrets. Use an existence-only check: [ -n \"\$VAR\" ] && echo set"
|
# filters the command's own output, not the environment (#25). The earlier
|
||||||
|
# pattern matched any `env` token followed anywhere later by a pipe into
|
||||||
|
# grep, which blocked the documented 1Password write path
|
||||||
|
# (`env -u OP_SERVICE_ACCOUNT_TOKEN op item create`) and cost a workaround.
|
||||||
|
# Denying only the "no command word follows" shape also catches `env FOO=bar`
|
||||||
|
# with no command, which the two previous rules let through.
|
||||||
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|[[:space:]])env([[:space:]]+-[iu][[:space:]]*[A-Za-z_][A-Za-z0-9_]*|[[:space:]]+-[iu]|[[:space:]]+[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*)*[[:space:]]*($|[|;&])'; then
|
||||||
|
deny "Blocked: this 'env' prints every variable's value, including secrets (no command word follows it, so it dumps rather than execs). Use an existence-only check: [ -n \"\$VAR\" ] && echo set. Note 'env -u VAR cmd' and 'env FOO=bar cmd' are allowed — they run a command."
|
||||||
fi
|
fi
|
||||||
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
|
if printf '%s' "$CMD" | grep -Eqi '(^|[;&|]|\s)printenv\b'; then
|
||||||
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
|
deny "Blocked: printenv dumps all env values. Use: [ -n \"\$VAR\" ] && echo set"
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
name: cad-babysit
|
||||||
|
description: Focused human-in-the-loop session working CAD's stuck queue (cad-stuck output). Read-only — diagnoses, never dispatches, arms a label, or closes an issue. Use when picking up whatever CAD has run into trouble on, with a human present.
|
||||||
|
---
|
||||||
|
|
||||||
|
# CAD Babysit
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A deliberately narrow session: pick up whatever CAD has run into trouble on, work it with a
|
||||||
|
human present, and stop. This is not an autonomous supervisor — it is a babysitter with a
|
||||||
|
person in the room, and that is the point, not a limitation to route around later.
|
||||||
|
|
||||||
|
**Core principle:** an unattended supervisor reproduces the exact failure it exists to catch.
|
||||||
|
`dispatch` ran dead for eight weeks because it exited 0 and nobody noticed (`dispatch#33`); a
|
||||||
|
long-running watcher process that dies quietly has the same property one level up, and
|
||||||
|
manufactures false confidence while doing it. With a human present, **the human is the
|
||||||
|
liveness check** — that removes the objection entirely and needs no new machinery.
|
||||||
|
|
||||||
|
This skill is the ritual around `cad stuck` (`mathias/cad#14`), not a replacement for it.
|
||||||
|
Without that query it would be a checklist with no input; the query without this skill is
|
||||||
|
data nobody reads carefully. Neither closes the loop alone.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Picking up CAD's stuck queue with a human present and time set aside to work it
|
||||||
|
- After `dispatch` (or any executor) reports a failure and it is not immediately obvious
|
||||||
|
whether it is an infrastructure fault or a real task outcome
|
||||||
|
- Reviewing the "awaiting human" queue (issues CI has verified but nobody has closed)
|
||||||
|
|
||||||
|
**Do not use for:**
|
||||||
|
- Unattended/scheduled runs. This skill assumes a human is present for its entire duration —
|
||||||
|
if nobody is watching, its central safety property (human-as-liveness-check) does not hold.
|
||||||
|
- Dispatching, arming a redispatch label, or closing an issue. This skill only ever diagnoses;
|
||||||
|
see Iron Laws.
|
||||||
|
|
||||||
|
## Iron Laws
|
||||||
|
|
||||||
|
1. **Read-only, always.** No dispatching, no arming labels (including `status:redispatch`), no
|
||||||
|
closing or reopening an issue. A reviewer that can act on its own findings has no
|
||||||
|
independent check — control stays where it already is.
|
||||||
|
2. **Every finding cites a locator.** A run id, commit sha, comment permalink, or a specific
|
||||||
|
log line. A finding without one is an unfalsifiable opinion about a machine — do not emit
|
||||||
|
it, and do not soften this into "probably" language instead of dropping it.
|
||||||
|
3. **Re-derive, never recall.** State lives in Gitea and Actions, not in this session. Start
|
||||||
|
cold every time: run `cad stuck` fresh, do not carry forward a prior session's worklist.
|
||||||
|
Sessions get compacted and lose exactly the cross-run history that makes a stale worklist
|
||||||
|
dangerous to trust.
|
||||||
|
4. **Distinguish "blocked" from "awaiting human".** `cad stuck` already makes this split
|
||||||
|
(`stuck` vs `awaiting_human`); preserve it in every summary. The `awaiting_human` class is
|
||||||
|
the human gate working, not a bottleneck — do not report it as one.
|
||||||
|
5. **Anything that is the operator's call surfaces as `DECISION NEEDED`, never gets decided
|
||||||
|
for them.** See Output Format.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### Phase 1 — Query (re-derive the worklist)
|
||||||
|
|
||||||
|
Run `cad stuck` against the estate (the `dispatch-repos.txt` scope, or whatever repo set the
|
||||||
|
operator names). This is the only input. Do not substitute a memory of a prior run, a brain
|
||||||
|
note, or this issue's own worked examples — all three can be stale within the same day (a
|
||||||
|
taxonomy row's cited live example can stop reproducing between when it was written and when
|
||||||
|
you run the query, because the underlying state genuinely changed; that is the design working,
|
||||||
|
not a bug to paper over).
|
||||||
|
|
||||||
|
Group the output by class exactly as `cad stuck` returns it: `stuck` first, `awaiting_human`
|
||||||
|
second. Pick ONE row to work per pass — this skill is deliberately narrow, not a full-estate
|
||||||
|
sweep.
|
||||||
|
|
||||||
|
### Phase 2 — Read the issue's own comments FIRST
|
||||||
|
|
||||||
|
Before anything else, before touching Actions or pod logs: read every comment on the issue,
|
||||||
|
oldest first. This step exists because it was skipped once and it cost a session — a confident
|
||||||
|
wrong root cause was produced from pod logs while the correct diagnosis sat in the issue's own
|
||||||
|
comment thread from nine days earlier (`cad-atlas#10`). The rule "read issue comments before
|
||||||
|
working" was already written down elsewhere and was not followed; it is written down again
|
||||||
|
here because that is exactly the kind of already-known thing a rushed session skips.
|
||||||
|
|
||||||
|
What to look for: the bot's own report comments (routing decision, ground-truth verdict,
|
||||||
|
attempt count), and any human comment that already contains the diagnosis.
|
||||||
|
|
||||||
|
### Phase 3 — THEN the Actions job logs
|
||||||
|
|
||||||
|
Only after Phase 2 turns up nothing conclusive, read the CI job logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /repos/{owner}/{repo}/actions/runs # find the run, match on .run_number
|
||||||
|
GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs # returns 200, full text, secrets masked
|
||||||
|
```
|
||||||
|
|
||||||
|
This endpoint works. A prior note claiming it 404s was wrong and cost a diagnosis the day this
|
||||||
|
skill was written — do not rediscover that. Get the run id from `/actions/runs`, not from the
|
||||||
|
`run_number` shown in a comment (they are different fields; match on `.run_number` to find the
|
||||||
|
internal `id` the jobs endpoint needs).
|
||||||
|
|
||||||
|
This is where the two most valuable findings of the day this skill is drawn from lived: an
|
||||||
|
agent issuing the identical refused command five times and stopping (never trying the tools it
|
||||||
|
was actually permitted to use), and a different model, same denial, writing a Go program to
|
||||||
|
route around it. Neither is visible from the issue comments alone — no metric produces either
|
||||||
|
of them. They need a reader of the raw transcript.
|
||||||
|
|
||||||
|
### Phase 4 — Separate infrastructure fault from task outcome
|
||||||
|
|
||||||
|
A run that died before turn 1 says nothing about the issue itself. Before concluding anything
|
||||||
|
about the WORK, check whether the run reached the model at all:
|
||||||
|
|
||||||
|
- Zero turns, a 2-3 record transcript, a "could not start" comment, or a claim label with no
|
||||||
|
attempt comment behind it → infrastructure fault. This is not evidence the issue is hard, ill
|
||||||
|
specified, or that the assigned model is unsuitable — it is evidence something upstream
|
||||||
|
(credit, a key, MCP connectivity, the executor binary) is broken.
|
||||||
|
- A run that reached the model, drove tools, and stopped on a ceiling, a denial loop, or a
|
||||||
|
genuine task failure → a real task outcome, worth diagnosing on its own terms.
|
||||||
|
|
||||||
|
Four claim labels were burned this exact way in one estate sweep and the tracker read them as
|
||||||
|
four handled issues (`dispatch#33`). Get this distinction wrong and everything downstream is
|
||||||
|
about the wrong thing.
|
||||||
|
|
||||||
|
### Phase 5 — Decide, or say `DECISION NEEDED`
|
||||||
|
|
||||||
|
For each row worked, produce one of:
|
||||||
|
|
||||||
|
- **A closed diagnosis** — cause identified, cited to a locator, no operator judgment call
|
||||||
|
required (e.g. "infrastructure fault, credit exhausted, see comment #N — retry is automatic
|
||||||
|
on the next cycle, no action needed").
|
||||||
|
- **A `DECISION NEEDED`** — anything that is genuinely the operator's call: whether to close a
|
||||||
|
verified-but-open issue, whether to raise a ceiling, whether to abandon an issue that keeps
|
||||||
|
hitting the same denial. Never make this call on the operator's behalf.
|
||||||
|
|
||||||
|
This skill's OWN last act, every time, is to stop — not to dispatch, not to redispatch, not to
|
||||||
|
close. Ending a pass is itself the enforcement of Iron Law 1.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
For a closed diagnosis:
|
||||||
|
|
||||||
|
```
|
||||||
|
DIAGNOSIS: <repo>#<issue> — <category from cad stuck, e.g. ceiling_hit>
|
||||||
|
CAUSE: <one sentence, cites the mechanism>
|
||||||
|
LOCATOR: <url or run id / log line>
|
||||||
|
ACTION: <what happens automatically, or "none — informational">
|
||||||
|
```
|
||||||
|
|
||||||
|
For anything requiring a human call:
|
||||||
|
|
||||||
|
```
|
||||||
|
DECISION NEEDED: <repo>#<issue>
|
||||||
|
QUESTION: <the exact thing only the operator can decide>
|
||||||
|
OPTIONS:
|
||||||
|
1. <option> — <consequence>
|
||||||
|
2. <option> — <consequence>
|
||||||
|
RECOMMENDATION: <your pick, with reasoning — but the operator decides>
|
||||||
|
BLOCKS: <what stays stuck until this is answered, if anything>
|
||||||
|
```
|
||||||
|
|
||||||
|
End every pass with a one-line summary: `N diagnosed, M decisions needed, stopping.`
|
||||||
|
|
||||||
|
## Worked Example
|
||||||
|
|
||||||
|
`cad stuck` returns `mathias/cad-atlas#10 [near_attempt_cap] 4 dispatch attempts recorded, at
|
||||||
|
or above the near-cap threshold (4 of MaxAttempts=5).`
|
||||||
|
|
||||||
|
Phase 2 (issue comments): the issue's own thread already explains the pattern — repeated
|
||||||
|
`status:redispatch` re-arming without the label being cleared after a completed attempt.
|
||||||
|
|
||||||
|
Phase 3: not needed — Phase 2 was conclusive.
|
||||||
|
|
||||||
|
Phase 4: each of the 4 attempts reached the model and ran (not an infrastructure fault) — a
|
||||||
|
real re-fire pattern, not a claim-label artifact.
|
||||||
|
|
||||||
|
Phase 5 output:
|
||||||
|
|
||||||
|
```
|
||||||
|
DECISION NEEDED: mathias/cad-atlas#10
|
||||||
|
QUESTION: one more automatic attempt remains before MaxAttempts=5 gives up permanently. Let
|
||||||
|
it fire, or intervene now (fix the underlying cause, or manually close/redirect the issue)?
|
||||||
|
OPTIONS:
|
||||||
|
1. Let it fire — costs one more headless run; if it fails the same way, dispatch gives up
|
||||||
|
automatically and posts a terminal comment. No action needed until then.
|
||||||
|
2. Intervene now — diagnose why redispatch keeps re-arming (likely: the watcher isn't
|
||||||
|
clearing RedispatchLabel after a serviced run) and fix that before the cap is spent on
|
||||||
|
a symptom rather than the cause.
|
||||||
|
RECOMMENDATION: option 2. The pattern (redispatch not self-clearing after a completed run) is
|
||||||
|
already documented as a known operational hazard (dispatch#54) — spending the last attempt
|
||||||
|
on it again teaches nothing new.
|
||||||
|
BLOCKS: nothing else; this issue's own progress.
|
||||||
|
```
|
||||||
|
|
||||||
|
`1 diagnosed, 1 decision needed, stopping.`
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
| Anti-Pattern | Why It Fails |
|
||||||
|
|---|---|
|
||||||
|
| Diagnosing from pod/Actions logs before reading issue comments | The exact mistake that cost a session — the answer was already written down nine days earlier. |
|
||||||
|
| Reporting `awaiting_human` rows as a backlog or bottleneck | It is the human gate working. Misreporting it manufactures urgency that isn't there and trains the operator to distrust the queue. |
|
||||||
|
| Treating a zero-turn, could-not-start run as evidence about the issue | It is an infrastructure fault. Four claim labels were burned exactly this way and read as four handled issues. |
|
||||||
|
| Deciding a `DECISION NEEDED` case on the operator's behalf "to save time" | Removes the one property that makes this skill safe to run unattended-adjacent: the human decides, this skill only prepares the decision. |
|
||||||
|
| Applying `status:redispatch`, closing an issue, or adding any label from this skill | Control stays where it is. This skill has no write path and should never be given one. |
|
||||||
|
| Carrying forward a worklist from an earlier session instead of re-running `cad stuck` | State changes between sessions (see the worked note on `brain-gardener#190` in `cad#14`'s own acceptance run — its trigger label was gone hours later). A stale worklist gives false confidence, which is the specific failure mode this whole skill exists to avoid. |
|
||||||
|
|
||||||
|
## Honest Limitation
|
||||||
|
|
||||||
|
This skill encodes the *repeatable* diagnostic sequence: read comments, then logs, separate
|
||||||
|
fault from outcome, verify by exit code, re-derive rather than recall. It reliably catches the
|
||||||
|
taxonomy `cad stuck` already knows about. It will not, by itself, produce a genuinely novel
|
||||||
|
finding the way an unhurried human-plus-agent reading session did the day this skill was
|
||||||
|
written — that came from noticing things (an agent editing the file that authorizes it, an
|
||||||
|
issue body recommending a command the policy forbids) that no checklist anticipates. Expect
|
||||||
|
this skill to be reliable on known failure modes and to miss the next new one. That is an
|
||||||
|
acceptable trade as long as nobody mistakes a clean pass through this skill for "nothing is
|
||||||
|
wrong" — it means "nothing on the known list is wrong."
|
||||||
|
|
||||||
|
## Brain MCP Integration
|
||||||
|
|
||||||
|
**At session start:**
|
||||||
|
- Run `brain_query` for the domain (`cad`, `dispatch`, the specific repo) to check whether a
|
||||||
|
finding this session is about to produce has already been diagnosed and written up. Do not
|
||||||
|
re-diagnose a known pattern from scratch every session.
|
||||||
|
|
||||||
|
**After a genuinely novel finding (not already in the taxonomy or the brain):**
|
||||||
|
- `brain_write` the pattern, generalized beyond the specific issue — the failure mode, not the
|
||||||
|
issue number. Do not write routine, already-known-taxonomy findings; that is what `cad
|
||||||
|
stuck`'s own output already records, durably, in Gitea.
|
||||||
|
|
||||||
|
**Never:**
|
||||||
|
- Write a `DECISION NEEDED` outcome to the brain before the operator has actually decided —
|
||||||
|
that would record a guess as a resolution.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Call `session_log` once at the end of every phase to record the outcome.
|
||||||
|
|
||||||
|
**At end of each phase:**
|
||||||
|
- `session_log` with `{skill: "cad-babysit", phase: "<phase-name>", final_status: "pass" |
|
||||||
|
"fail" | "skip", message: "<one-line summary>", duration_ms: <wall-clock>, project_root:
|
||||||
|
"<absolute path>"}`
|
||||||
|
|
||||||
|
**Phases for this skill:** query, read-comments, read-actions-logs, classify-fault-vs-outcome,
|
||||||
|
decide
|
||||||
|
|
||||||
|
**Status semantics:**
|
||||||
|
- `pass` — the phase's intended outcome was reached (worklist re-derived; comments read;
|
||||||
|
fault/outcome separated; a diagnosis or `DECISION NEEDED` was produced).
|
||||||
|
- `fail` — the phase could not be completed (e.g. `cad stuck` errored, Gitea was unreachable).
|
||||||
|
- `skip` — phase intentionally skipped (e.g. Phase 3 skipped because Phase 2 was conclusive).
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Depends on `mathias/cad#14`'s `cad stuck` query — this skill is the ritual around it, not a
|
||||||
|
replacement.
|
||||||
|
- Load `debug` for the hypothesis-generation discipline if Phase 4's fault/outcome split is
|
||||||
|
itself ambiguous.
|
||||||
|
- Load `secret-hygiene` before reading raw Actions job logs if the repo is client-tagged —
|
||||||
|
logs are secret-masked by Gitea but a pasted excerpt into a summary is not.
|
||||||
|
- `mathias/cad-atlas#11` renders the same `cad stuck` output as a pipeline overlay — read that
|
||||||
|
instead of this skill's text output when a visual "where is everything stuck" view is wanted
|
||||||
|
over working one row in depth.
|
||||||
@@ -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.
|
|
||||||
@@ -22,18 +22,28 @@ Screenshot a locally-running web UI without installing a browser on the agent's
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
`shot.sh` ships beside this SKILL.md, so resolve it from wherever the harness
|
||||||
|
installed the skill rather than from a checkout path — an absolute `~/dev/...`
|
||||||
|
went stale the first time the repo moved and cost a caller three hand-rolled
|
||||||
|
Job attempts before they found the working script (#26).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Claude Code installs the plugin; Crush/Pi/OpenCode get ~/.agents/skills.
|
||||||
|
SHOT=$(ls ~/.claude/plugins/marketplaces/*/skills/web-shot/shot.sh \
|
||||||
|
~/.agents/skills/web-shot/shot.sh 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$SHOT" ] || { echo "web-shot not installed on this host: run install.sh or the plugin install" >&2; exit 1; }
|
||||||
|
|
||||||
# auto-detect Tailscale hostname, screenshot port 8099 root
|
# auto-detect Tailscale hostname, screenshot port 8099 root
|
||||||
~/dev/.skills/web-shot/shot.sh --port 8099
|
"$SHOT" --port 8099
|
||||||
|
|
||||||
# explicit URL
|
# explicit URL
|
||||||
~/dev/.skills/web-shot/shot.sh --url http://flamingo:5173
|
"$SHOT" --url http://flamingo:5173
|
||||||
|
|
||||||
# capture a search state too (types into input[name=q])
|
# capture a search state too (types into input[name=q])
|
||||||
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad
|
"$SHOT" --port 8181 --search agentsquad
|
||||||
|
|
||||||
# different output dir
|
# different output dir
|
||||||
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots
|
"$SHOT" --port 3000 --out /tmp/shots
|
||||||
```
|
```
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/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,57 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for the CI gate workflow.
|
||||||
|
#
|
||||||
|
# The regression these exist for: this repo had a release workflow that
|
||||||
|
# auto-tagged every push to main, and a `task check` / `task test` suite that
|
||||||
|
# nothing ever ran. CI reported green having tested nothing — a gate that
|
||||||
|
# fails *open*. These assertions pin the wiring so it cannot silently regress.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
SUT="$ROOT/.gitea/workflows/ci.yml"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
||||||
|
no() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; }
|
||||||
|
|
||||||
|
if [ ! -f "$SUT" ]; then
|
||||||
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok 'ci.yml exists'
|
||||||
|
|
||||||
|
# A pull_request trigger is what gives tbd_ship a CI run to read on the head
|
||||||
|
# commit of a branch; without one it fails closed and never merges (#22).
|
||||||
|
grep -qE '^ pull_request:' "$SUT" \
|
||||||
|
&& ok 'runs on pull_request' \
|
||||||
|
|| no 'runs on pull_request'
|
||||||
|
|
||||||
|
grep -qE '^ +push:' "$SUT" \
|
||||||
|
&& ok 'runs on push' \
|
||||||
|
|| no 'runs on push'
|
||||||
|
|
||||||
|
grep -qE '^ +- *(run: *)?task check$|task check' "$SUT" \
|
||||||
|
&& ok 'gate runs task check' \
|
||||||
|
|| no 'gate runs task check'
|
||||||
|
|
||||||
|
grep -qE 'task test' "$SUT" \
|
||||||
|
&& ok 'gate runs task test' \
|
||||||
|
|| no 'gate runs task test'
|
||||||
|
|
||||||
|
# The point of the gate: tagging must not happen on a red check.
|
||||||
|
tag_job=$(awk '/^ tag:/{f=1} f&&/^ [a-z][a-z_-]*:/&&!/^ tag:/{f=0} f' "$SUT")
|
||||||
|
printf '%s' "$tag_job" | grep -qE '^ +needs: *gate' \
|
||||||
|
&& ok 'tag job needs the gate' \
|
||||||
|
|| no 'tag job needs the gate'
|
||||||
|
|
||||||
|
# The old workflow file must be gone, or two workflows race on the same push
|
||||||
|
# and the ungated one still tags.
|
||||||
|
[ ! -f "$ROOT/.gitea/workflows/release.yml" ] \
|
||||||
|
&& ok 'ungated release.yml removed' \
|
||||||
|
|| no 'ungated release.yml removed'
|
||||||
|
|
||||||
|
printf '\nci_gate: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -112,6 +112,19 @@ mkdir -p "$f/home/.claude/skills"
|
|||||||
ln -sfn "$f/ck/skills/alpha" "$f/home/.claude/skills/alpha"
|
ln -sfn "$f/ck/skills/alpha" "$f/home/.claude/skills/alpha"
|
||||||
assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
|
assert_fails "$f" 'legacy' 'leftover per-skill Claude symlinks are reported'
|
||||||
|
|
||||||
|
# --- 7b. harness dir is a REAL directory, not a link into the checkout -------
|
||||||
|
# Found on flamingo 2026-08-26: ~/.config/crush/skills held real directories
|
||||||
|
# from a pre-plugin install. The old message read "resolves to
|
||||||
|
# <path>, outside the active checkout" — naming the directory as its own
|
||||||
|
# resolution target, which is true but useless. It must say the dir is not a
|
||||||
|
# link at all, because the fix (move it aside, re-run install.sh) is different
|
||||||
|
# from the fix for a link into a foreign checkout.
|
||||||
|
f=$(new_fixture realdir)
|
||||||
|
mkdir -p "$f/home/.agents" "$f/home/.config/crush/skills/alpha"
|
||||||
|
ln -sfn "$f/ck/skills" "$f/home/.agents/skills"
|
||||||
|
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"'
|
||||||
|
|
||||||
# --- 7. dangling link inside a harness dir (not the canonical link itself) --
|
# --- 7. dangling link inside a harness dir (not the canonical link itself) --
|
||||||
# Exercises the per-entry scan rather than the canonical-link check: this is
|
# Exercises the per-entry scan rather than the canonical-link check: this is
|
||||||
# the shape the 243 dead links actually had.
|
# the shape the 243 dead links actually had.
|
||||||
@@ -120,5 +133,63 @@ mkdir -p "$f/home/.claude/skills"
|
|||||||
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
|
ln -sfn "$f/ck/skills/vanished" "$f/home/.claude/skills/vanished"
|
||||||
assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported'
|
assert_fails "$f" 'dangling' 'dangling link inside a harness dir is reported'
|
||||||
|
|
||||||
|
# --- 8. Claude Code plugin pinned to an older version than is available -----
|
||||||
|
# The plugin is the ONLY delivery channel for Claude Code, and Claude Code
|
||||||
|
# gates updates on plugin.json's version field. A host that never picked the
|
||||||
|
# new one up loads every skill successfully — just the wrong generation. Found
|
||||||
|
# live on koala 2026-08-29: installed 0.6.0, marketplace 0.7.0, repo 0.8.5,
|
||||||
|
# and doctor said "clean" (#30).
|
||||||
|
|
||||||
|
# installed_state <fixture> <version> — the Claude Code install record.
|
||||||
|
installed_state() {
|
||||||
|
mkdir -p "$1/home/.claude/plugins"
|
||||||
|
cat > "$1/home/.claude/plugins/installed_plugins.json" <<JSON
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"plugins": {
|
||||||
|
"skills@mathias": [
|
||||||
|
{
|
||||||
|
"scope": "user",
|
||||||
|
"installPath": "/somewhere/$2",
|
||||||
|
"version": "$2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
# manifest <dir> <version> — a .claude-plugin/plugin.json under <dir>.
|
||||||
|
manifest() {
|
||||||
|
mkdir -p "$1/.claude-plugin"
|
||||||
|
printf '{\n "name": "skills",\n "version": "%s"\n}\n' "$2" > "$1/.claude-plugin/plugin.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
f=$(new_fixture pluginmatch); wire_clean "$f"
|
||||||
|
installed_state "$f" 0.8.0
|
||||||
|
manifest "$f/ck" 0.8.0
|
||||||
|
assert_clean "$f" 'plugin on the current version stays silent'
|
||||||
|
|
||||||
|
f=$(new_fixture pluginstale); wire_clean "$f"
|
||||||
|
installed_state "$f" 0.7.0
|
||||||
|
manifest "$f/ck" 0.8.0
|
||||||
|
assert_fails "$f" '0.7.0' 'stale plugin version is reported'
|
||||||
|
assert_fails "$f" '0.8.0' 'stale plugin report names the available version'
|
||||||
|
|
||||||
|
# A host that only uses the symlink path has no install record at all. That is
|
||||||
|
# a valid configuration and must not fail.
|
||||||
|
f=$(new_fixture pluginabsent); wire_clean "$f"
|
||||||
|
manifest "$f/ck" 0.8.0
|
||||||
|
assert_clean "$f" 'no Claude Code install record is not a fault'
|
||||||
|
|
||||||
|
# The marketplace clone is what Claude Code compares against, so a newer
|
||||||
|
# version there means an update is waiting even when the local checkout agrees
|
||||||
|
# with what is installed.
|
||||||
|
f=$(new_fixture pluginmarket); wire_clean "$f"
|
||||||
|
installed_state "$f" 0.7.0
|
||||||
|
manifest "$f/ck" 0.7.0
|
||||||
|
manifest "$f/home/.claude/plugins/marketplaces/mathias" 0.9.0
|
||||||
|
assert_fails "$f" '0.9.0' 'a newer marketplace clone is reported'
|
||||||
|
|
||||||
printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail"
|
printf '\ndoctor: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
[ "$fail" -eq 0 ]
|
[ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ expect deny 'op read "op://vault/item/field"' 'op read'
|
|||||||
expect deny 'env | grep -i gitea' 'env piped to grep'
|
expect deny 'env | grep -i gitea' 'env piped to grep'
|
||||||
expect deny 'env' 'bare env'
|
expect deny 'env' 'bare env'
|
||||||
expect deny 'printenv' 'printenv'
|
expect deny 'printenv' 'printenv'
|
||||||
|
expect deny 'env -i | grep -i token' 'env -i with no command is still a dump'
|
||||||
|
expect deny 'env FOO=bar' 'env with only assignments and no command is a dump'
|
||||||
|
|
||||||
|
# --- allow: `env VAR=x cmd` / `env -u VAR cmd` RUN a command (#25) ---
|
||||||
|
# These print nothing themselves; a pipe into grep filters the command's own
|
||||||
|
# output, not the environment. Blocking them cost a documented workaround
|
||||||
|
# ((unset VAR; cmd)) on the 1Password write path, which is the shape below.
|
||||||
|
expect allow 'env -u ANTHROPIC_API_KEY claude --debug -p "ok" 2>&1 | grep -i plugin' 'env -u VAR runs a command'
|
||||||
|
expect allow 'env FOO=bar ./script.sh | grep x' 'env VAR=x runs a command'
|
||||||
|
expect allow 'env -u OP_SERVICE_ACCOUNT_TOKEN op item create - < item.json' 'env -u on the 1P write path'
|
||||||
|
expect deny 'env -u FOO ./run.sh; env | grep -i secret' 'a real dump later in the same line still denies'
|
||||||
|
|
||||||
# --- deny: kubernetes secret material ---
|
# --- deny: kubernetes secret material ---
|
||||||
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'
|
expect deny 'kubectl get secret brain-app -o yaml' 'kubectl get secret -o yaml'
|
||||||
|
|||||||
Reference in New Issue
Block a user