mirror of
https://github.com/Hestia-Homes/agentic-toolkit.git
synced 2026-06-08 11:37:26 +00:00
Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
677a0add9d | ||
|
|
c2bb301a57 | ||
|
|
b88fb554ec | ||
|
|
5c12f8bf43 | ||
|
|
12a885f43b | ||
|
|
0c32b3fe0f | ||
|
|
9cf30dd365 | ||
|
|
4e09b52b29 | ||
|
|
47f7b1b828 | ||
|
|
0d67119bdb | ||
|
|
4d763066d4 | ||
|
|
3e04fe14ae | ||
|
|
af8ea95a5b | ||
|
|
769689a093 |
6 changed files with 482 additions and 118 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,3 +6,6 @@ dist
|
||||||
.env.local
|
.env.local
|
||||||
coverage
|
coverage
|
||||||
.vitest-cache
|
.vitest-cache
|
||||||
|
# skills CLI side-effects when running `skills add` locally
|
||||||
|
.agents/
|
||||||
|
.claude/
|
||||||
|
|
|
||||||
135
scripts/generate-lock.sh
Executable file
135
scripts/generate-lock.sh
Executable file
|
|
@ -0,0 +1,135 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Regenerate skills-lock.json from skills.config.json.
|
||||||
|
#
|
||||||
|
# How it works:
|
||||||
|
# 1. Read skills.config.json (list of {source, skills[]}).
|
||||||
|
# 2. In a temp dir, run `npx skills add <source> --skill ... --agent <agent> --copy --yes`
|
||||||
|
# once per source. The `skills` CLI auto-writes/merges skills-lock.json there.
|
||||||
|
# 3. Copy the resulting lock back to the repo root.
|
||||||
|
#
|
||||||
|
# Run from the repo root (or anywhere; paths are resolved relative to the script):
|
||||||
|
# bash scripts/generate-lock.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
CONFIG="$REPO_ROOT/skills.config.json"
|
||||||
|
DEST_LOCK="$REPO_ROOT/skills-lock.json"
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG" ]]; then
|
||||||
|
echo "error: $CONFIG not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for bin in node npx; do
|
||||||
|
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||||
|
echo "error: $bin is required (install Node.js >= 20)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d -t skills-lockgen.XXXXXX)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
# Emit "agent\ninstallSource\tcanonicalSource\tskillA,skillB\n..." for the shell.
|
||||||
|
# installSource is what we pass to `skills add` (resolved localPath if set, else
|
||||||
|
# the canonical slug). canonicalSource is what the published lock must record.
|
||||||
|
PLAN_FILE="$WORK_DIR/plan.txt"
|
||||||
|
node --input-type=module -e "
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
||||||
|
const repoRoot = process.argv[3];
|
||||||
|
const agent = cfg.agent || 'claude-code';
|
||||||
|
const lines = [agent];
|
||||||
|
for (const entry of cfg.sources || []) {
|
||||||
|
if (!entry?.source || !Array.isArray(entry.skills) || entry.skills.length === 0) continue;
|
||||||
|
const install = entry.localPath ? path.resolve(repoRoot, entry.localPath) : entry.source;
|
||||||
|
lines.push(install + '\t' + entry.source + '\t' + entry.skills.join(','));
|
||||||
|
}
|
||||||
|
fs.writeFileSync(process.argv[2], lines.join('\n') + '\n');
|
||||||
|
" "$CONFIG" "$PLAN_FILE" "$REPO_ROOT"
|
||||||
|
|
||||||
|
AGENT="$(head -n1 "$PLAN_FILE")"
|
||||||
|
echo "==> Agent target: $AGENT"
|
||||||
|
echo "==> Generating lock in $WORK_DIR"
|
||||||
|
|
||||||
|
# Read plan from fd 3 so the loop body's stdin stays free for npx (the skills
|
||||||
|
# CLI reads stdin even in --yes mode and would otherwise gobble loop lines).
|
||||||
|
while IFS=$'\t' read -r INSTALL_SOURCE CANONICAL_SOURCE SKILLS <&3; do
|
||||||
|
[[ -z "$INSTALL_SOURCE" ]] && continue
|
||||||
|
if [[ "$INSTALL_SOURCE" == "$CANONICAL_SOURCE" ]]; then
|
||||||
|
echo "==> $CANONICAL_SOURCE :: $SKILLS"
|
||||||
|
else
|
||||||
|
echo "==> $CANONICAL_SOURCE (from local $INSTALL_SOURCE) :: $SKILLS"
|
||||||
|
fi
|
||||||
|
SKILL_ARGS=()
|
||||||
|
IFS=',' read -ra _NAMES <<< "$SKILLS"
|
||||||
|
for n in "${_NAMES[@]}"; do
|
||||||
|
[[ -z "$n" ]] && continue
|
||||||
|
SKILL_ARGS+=(--skill "$n")
|
||||||
|
done
|
||||||
|
( cd "$WORK_DIR" && npx --yes skills@latest add "$INSTALL_SOURCE" \
|
||||||
|
"${SKILL_ARGS[@]}" \
|
||||||
|
--agent "$AGENT" \
|
||||||
|
--copy \
|
||||||
|
--yes < /dev/null )
|
||||||
|
done 3< <(tail -n +2 "$PLAN_FILE")
|
||||||
|
|
||||||
|
if [[ ! -f "$WORK_DIR/skills-lock.json" ]]; then
|
||||||
|
echo "error: skills CLI did not produce $WORK_DIR/skills-lock.json." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rewrite any local-path sources in the lock back to their canonical slug, so
|
||||||
|
# consumers of skills-lock.json (e.g. setup.sh in a dev container) fetch from
|
||||||
|
# GitHub instead of a path that only exists on the generator's machine.
|
||||||
|
node --input-type=module -e "
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
||||||
|
const repoRoot = process.argv[2];
|
||||||
|
const lock = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
|
||||||
|
const remap = new Map();
|
||||||
|
for (const entry of cfg.sources || []) {
|
||||||
|
if (!entry?.source || !entry.localPath) continue;
|
||||||
|
remap.set(path.resolve(repoRoot, entry.localPath), entry.source);
|
||||||
|
}
|
||||||
|
for (const skill of Object.values(lock.skills || {})) {
|
||||||
|
if (!skill?.source) continue;
|
||||||
|
const resolved = path.resolve(skill.source);
|
||||||
|
if (remap.has(resolved)) {
|
||||||
|
skill.source = remap.get(resolved);
|
||||||
|
skill.sourceType = 'github';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(process.argv[4], JSON.stringify(lock, null, 2) + '\n');
|
||||||
|
" "$CONFIG" "$REPO_ROOT" "$WORK_DIR/skills-lock.json" "$DEST_LOCK"
|
||||||
|
echo "==> Wrote $DEST_LOCK"
|
||||||
|
|
||||||
|
# Cross-check: every (source, skill) requested in config should be present in the lock.
|
||||||
|
node --input-type=module -e "
|
||||||
|
import fs from 'node:fs';
|
||||||
|
const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
||||||
|
const lock = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||||
|
const got = new Map();
|
||||||
|
for (const [name, entry] of Object.entries(lock.skills || {})) {
|
||||||
|
if (!entry?.source) continue;
|
||||||
|
if (!got.has(entry.source)) got.set(entry.source, new Set());
|
||||||
|
got.get(entry.source).add(name);
|
||||||
|
}
|
||||||
|
const missing = [];
|
||||||
|
for (const entry of cfg.sources || []) {
|
||||||
|
const have = got.get(entry.source) || new Set();
|
||||||
|
for (const s of entry.skills || []) {
|
||||||
|
if (!have.has(s)) missing.push(entry.source + '::' + s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.length) {
|
||||||
|
console.error('error: skills missing from generated lock (likely missing SKILL.md frontmatter upstream):');
|
||||||
|
for (const m of missing) console.error(' - ' + m);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('==> Verified: all ' + Object.keys(lock.skills).length + ' configured skills present in lock.');
|
||||||
|
" "$CONFIG" "$DEST_LOCK"
|
||||||
113
setup.sh
113
setup.sh
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Install Domna's curated skill set into the current repo.
|
# Install Domna's curated skill set globally (~/.claude).
|
||||||
#
|
#
|
||||||
# Run from the root of the target repo:
|
# Run from the root of the target repo:
|
||||||
# curl -fsSL https://raw.githubusercontent.com/Hestia-Homes/agentic-toolkit/main/setup.sh | bash
|
# curl -fsSL https://raw.githubusercontent.com/Hestia-Homes/agentic-toolkit/main/setup.sh | bash
|
||||||
|
|
@ -8,60 +8,119 @@
|
||||||
# bash /path/to/agentic-toolkit/setup.sh
|
# bash /path/to/agentic-toolkit/setup.sh
|
||||||
#
|
#
|
||||||
# What this does:
|
# What this does:
|
||||||
# 1. Drops the pinned skills-lock.json from agentic-toolkit into the target repo.
|
# 1. Copies the pinned skills-lock.json from agentic-toolkit to ~/.claude.
|
||||||
# 2. Runs `skills experimental_install` to restore the exact pinned versions
|
# 2. Parses it and runs `skills add --global` per source so all skills land
|
||||||
# of Matt Pocock's skills + Domna's own skills (Hestia-Homes/agentic-toolkit).
|
# in ~/.claude/skills (user-level, available in every repo).
|
||||||
#
|
#
|
||||||
# To upgrade skills across all Domna repos:
|
# Note: `skills experimental_install` would be the natural fit but it only
|
||||||
# - In agentic-toolkit, re-install the skills you want, then commit the
|
# restores at project scope — confirmed empirically that running it against
|
||||||
# resulting skills-lock.json (this script reads from that file).
|
# a global lock prints "No project skills found" and exits 0, installing
|
||||||
# - Devs re-run setup.sh in their target repos.
|
# nothing. So we drive `skills add --global` from the lock instead.
|
||||||
|
#
|
||||||
|
# Logs verbose output to $HOME/.claude/agentic-toolkit-setup.log so failures
|
||||||
|
# under postCreateCommand or `| bash` are inspectable after the fact.
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- logging -------------------------------------------------------------------
|
||||||
|
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||||
|
mkdir -p "$CLAUDE_DIR"
|
||||||
|
LOG_FILE="$CLAUDE_DIR/agentic-toolkit-setup.log"
|
||||||
|
# Tee everything (stdout + stderr) to the log so silent failures aren't silent.
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
echo "=== agentic-toolkit setup.sh @ $(date -u +%FT%TZ) ==="
|
||||||
|
|
||||||
|
# Trap so a failure under set -e prints the failing line into the log.
|
||||||
|
trap 'rc=$?; echo "ERROR: setup.sh exited $rc at line $LINENO" >&2; exit $rc' ERR
|
||||||
|
|
||||||
# --- config --------------------------------------------------------------------
|
# --- config --------------------------------------------------------------------
|
||||||
LOCK_URL="https://raw.githubusercontent.com/Hestia-Homes/agentic-toolkit/main/skills-lock.json"
|
LOCK_URL="https://raw.githubusercontent.com/Hestia-Homes/agentic-toolkit/main/skills-lock.json"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
|
||||||
LOCAL_LOCK="${SCRIPT_DIR:+$SCRIPT_DIR/skills-lock.json}"
|
LOCAL_LOCK="${SCRIPT_DIR:+$SCRIPT_DIR/skills-lock.json}"
|
||||||
|
AGENT_TARGET="claude-code"
|
||||||
|
DEST_LOCK="$CLAUDE_DIR/skills-lock.json"
|
||||||
|
|
||||||
# --- guards --------------------------------------------------------------------
|
# --- guards --------------------------------------------------------------------
|
||||||
if ! command -v npx >/dev/null 2>&1; then
|
if ! command -v npx >/dev/null 2>&1; then
|
||||||
echo "error: npx is required (install Node.js >= 20)." >&2
|
echo "error: npx is required (install Node.js >= 20)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ ! -d .git ]]; then
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
echo "error: run this script from the root of a git repository." >&2
|
echo "error: node is required to parse skills-lock.json." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- stage skills-lock.json ----------------------------------------------------
|
# --- stage skills-lock.json into ~/.claude ------------------------------------
|
||||||
if [[ -n "$LOCAL_LOCK" && -f "$LOCAL_LOCK" ]]; then
|
if [[ -n "$LOCAL_LOCK" && -f "$LOCAL_LOCK" ]]; then
|
||||||
echo "==> Using local skills-lock.json from $LOCAL_LOCK"
|
echo "==> Copying local skills-lock.json -> $DEST_LOCK"
|
||||||
cp "$LOCAL_LOCK" ./skills-lock.json
|
cp "$LOCAL_LOCK" "$DEST_LOCK"
|
||||||
else
|
else
|
||||||
echo "==> Fetching skills-lock.json from $LOCK_URL"
|
echo "==> Fetching skills-lock.json -> $DEST_LOCK"
|
||||||
if command -v curl >/dev/null 2>&1; then
|
if command -v curl >/dev/null 2>&1; then
|
||||||
curl -fsSL "$LOCK_URL" -o ./skills-lock.json
|
curl -fsSL "$LOCK_URL" -o "$DEST_LOCK"
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
wget -qO ./skills-lock.json "$LOCK_URL"
|
wget -qO "$DEST_LOCK" "$LOCK_URL"
|
||||||
else
|
else
|
||||||
echo "error: need curl or wget to fetch skills-lock.json." >&2
|
echo "error: need curl or wget to fetch skills-lock.json." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- install -------------------------------------------------------------------
|
# --- parse lock: write "source<TAB>skill1,skill2,..." to a tempfile -----------
|
||||||
echo "==> Restoring skills from skills-lock.json"
|
GROUPS_FILE="$(mktemp -t skills-groups.XXXXXX)"
|
||||||
npx --yes skills@latest experimental_install
|
trap 'rm -f "$GROUPS_FILE"' EXIT
|
||||||
|
|
||||||
# --- post-install reminder -----------------------------------------------------
|
node --input-type=module -e "
|
||||||
|
import fs from 'node:fs';
|
||||||
|
const lock = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
||||||
|
const bySource = new Map();
|
||||||
|
for (const [name, entry] of Object.entries(lock.skills || {})) {
|
||||||
|
if (!entry || !entry.source) continue;
|
||||||
|
if (!bySource.has(entry.source)) bySource.set(entry.source, []);
|
||||||
|
bySource.get(entry.source).push(name);
|
||||||
|
}
|
||||||
|
const out = [...bySource.entries()]
|
||||||
|
.map(([src, names]) => src + '\t' + names.join(','))
|
||||||
|
.join('\n');
|
||||||
|
fs.writeFileSync(process.argv[2], out + (out ? '\n' : ''));
|
||||||
|
" "$DEST_LOCK" "$GROUPS_FILE"
|
||||||
|
|
||||||
|
if [[ ! -s "$GROUPS_FILE" ]]; then
|
||||||
|
echo "error: no skills found in $DEST_LOCK." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Skill groups parsed from lock:"
|
||||||
|
cat "$GROUPS_FILE"
|
||||||
|
|
||||||
|
# --- install (globally, grouped by source) -------------------------------------
|
||||||
|
echo "==> Installing skills globally (~/.claude)"
|
||||||
|
# Read plan from fd 3 and redirect npx stdin to /dev/null. The skills CLI reads
|
||||||
|
# stdin even with --yes; without these guards it would consume the remaining
|
||||||
|
# lines of GROUPS_FILE and the loop would only run once (silently dropping
|
||||||
|
# every source after the first).
|
||||||
|
while IFS=$'\t' read -r SOURCE SKILLS <&3; do
|
||||||
|
[[ -z "$SOURCE" ]] && continue
|
||||||
|
echo "==> $SOURCE :: $SKILLS"
|
||||||
|
# The skills CLI expects --skill repeated per name, not a comma-joined string.
|
||||||
|
SKILL_ARGS=()
|
||||||
|
IFS=',' read -ra _NAMES <<< "$SKILLS"
|
||||||
|
for n in "${_NAMES[@]}"; do
|
||||||
|
[[ -z "$n" ]] && continue
|
||||||
|
SKILL_ARGS+=(--skill "$n")
|
||||||
|
done
|
||||||
|
npx --yes skills@latest add "$SOURCE" \
|
||||||
|
"${SKILL_ARGS[@]}" \
|
||||||
|
--agent "$AGENT_TARGET" \
|
||||||
|
--copy \
|
||||||
|
--global \
|
||||||
|
--yes < /dev/null
|
||||||
|
done 3< "$GROUPS_FILE"
|
||||||
|
|
||||||
|
echo "==> Done. Log: $LOG_FILE"
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
|
|
||||||
==> Done. Next steps:
|
Next step:
|
||||||
|
Run /setup-matt-pocock-skills once per repo to record the issue tracker,
|
||||||
1. Run /setup-matt-pocock-skills (one-time per repo) to record the issue
|
triage labels, and domain-doc layout.
|
||||||
tracker, triage labels, and domain-doc layout.
|
|
||||||
|
|
||||||
2. Commit skills-lock.json so the rest of the team gets the same pinned
|
|
||||||
skill versions on their next `skills experimental_install`.
|
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
153
skills-lock.json
153
skills-lock.json
|
|
@ -1,115 +1,86 @@
|
||||||
{
|
{
|
||||||
"version": 3,
|
"version": 1,
|
||||||
"skills": {
|
"skills": {
|
||||||
"diagnose": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/diagnose/SKILL.md",
|
|
||||||
"skillFolderHash": "43d464d0e9d1049b9d525975c0f7367ab7e01a5f",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"grill-with-docs": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/grill-with-docs/SKILL.md",
|
|
||||||
"skillFolderHash": "2969a1224c70fe41b9dd2ddbe32c2ec62f2815bd",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"improve-codebase-architecture": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/improve-codebase-architecture/SKILL.md",
|
|
||||||
"skillFolderHash": "3ad8fa787b3b9b622d1f5a3d0afc27812ac782fa",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"setup-matt-pocock-skills": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/setup-matt-pocock-skills/SKILL.md",
|
|
||||||
"skillFolderHash": "a500c5b9a481e6fac8c9bb87cd4c4e16c3f46e1a",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"tdd": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/tdd/SKILL.md",
|
|
||||||
"skillFolderHash": "75beb3030b4c979205dd771ff85ac600baeb68f4",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"to-issues": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/to-issues/SKILL.md",
|
|
||||||
"skillFolderHash": "2bf2903405724ce27038fc91f1c4304c4a63ae16",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"to-prd": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
|
||||||
"skillFolderHash": "63e890f04a30b69c9931615f36700fc45eb8d1ad",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"triage": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/triage/SKILL.md",
|
|
||||||
"skillFolderHash": "de4f182c30876a2460ca307e2f601b9b892527e5",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"zoom-out": {
|
|
||||||
"source": "mattpocock/skills",
|
|
||||||
"sourceType": "github",
|
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/engineering/zoom-out/SKILL.md",
|
|
||||||
"skillFolderHash": "6ecebabdea814d12888f56a611da7bf182b5fb26",
|
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
|
||||||
"caveman": {
|
"caveman": {
|
||||||
"source": "mattpocock/skills",
|
"source": "mattpocock/skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/productivity/caveman/SKILL.md",
|
"skillPath": "skills/productivity/caveman/SKILL.md",
|
||||||
"skillFolderHash": "17972a1bdbf5909b9dbdc435ad348f0bf45f8664",
|
"computedHash": "934433479903febc585bf6deb5f0cebc63137e3f86b7babe0aab1ecb94d6d7a4"
|
||||||
"pluginName": "mattpocock-skills"
|
},
|
||||||
|
"diagnose": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/diagnose/SKILL.md",
|
||||||
|
"computedHash": "15939a26f86edec2d4862042b8564e5a062cb81d04e047a0cea6305c8830b5f5"
|
||||||
},
|
},
|
||||||
"grill-me": {
|
"grill-me": {
|
||||||
"source": "mattpocock/skills",
|
"source": "mattpocock/skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
|
||||||
"skillPath": "skills/productivity/grill-me/SKILL.md",
|
"skillPath": "skills/productivity/grill-me/SKILL.md",
|
||||||
"skillFolderHash": "2a1ad17028306ebe45f0e49703fa28b9b2e7f499",
|
"computedHash": "784f0dbb7403b0f00324bce9a112f715342777a0daee7bbb7385f9c6f0a170ea"
|
||||||
"pluginName": "mattpocock-skills"
|
|
||||||
},
|
},
|
||||||
"write-a-skill": {
|
"grill-with-docs": {
|
||||||
"source": "mattpocock/skills",
|
"source": "mattpocock/skills",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"sourceUrl": "https://github.com/mattpocock/skills.git",
|
"skillPath": "skills/engineering/grill-with-docs/SKILL.md",
|
||||||
"skillPath": "skills/productivity/write-a-skill/SKILL.md",
|
"computedHash": "1adf321072f53cce3dcaf5357d91b8230d4aa647bb8a51756745337a6ee567b8"
|
||||||
"skillFolderHash": "2f252b35aa238879afc5a230ac30343708dee0b3"
|
},
|
||||||
|
"improve-codebase-architecture": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/improve-codebase-architecture/SKILL.md",
|
||||||
|
"computedHash": "c77b86b4332919499608f9af1880074e1fec65a59b95c70c27a9f39cd137865e"
|
||||||
},
|
},
|
||||||
"ralph-loop": {
|
"ralph-loop": {
|
||||||
"source": "Hestia-Homes/agentic-toolkit",
|
"source": "Hestia-Homes/agentic-toolkit",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"sourceUrl": "https://github.com/Hestia-Homes/agentic-toolkit.git",
|
"computedHash": "6d6c256a5145008038d128089fa17ed78ac9350d98efd977624da21d9255988b"
|
||||||
"skillPath": "skills/engineering/ralph-loop/SKILL.md",
|
},
|
||||||
"skillFolderHash": "d1414d3ccd3cc27f7cc1117b7723efc22b2d1ec7"
|
"setup-matt-pocock-skills": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/setup-matt-pocock-skills/SKILL.md",
|
||||||
|
"computedHash": "0164a8b1ef998abca426056c6ed8a7716a9d4692fc6daa5378f68381a6dafd24"
|
||||||
|
},
|
||||||
|
"tdd": {
|
||||||
|
"source": "Hestia-Homes/agentic-toolkit",
|
||||||
|
"sourceType": "github",
|
||||||
|
"computedHash": "b995b2494d7cd0737c7aa2d6a8affcdd8030ac1b29a52c572a3c5c163842c451"
|
||||||
|
},
|
||||||
|
"to-issues": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/to-issues/SKILL.md",
|
||||||
|
"computedHash": "47f648f3414848ccfc62cb41d2828b7e575fb5e7cbd6c4bdf630c063b5dc5e82"
|
||||||
|
},
|
||||||
|
"to-prd": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
||||||
|
"computedHash": "6d741474efd4bc3db55fabc2722ed78ca9c374cabcb6212936d79d4fd4a30fcb"
|
||||||
},
|
},
|
||||||
"to-project": {
|
"to-project": {
|
||||||
"source": "Hestia-Homes/agentic-toolkit",
|
"source": "Hestia-Homes/agentic-toolkit",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"sourceUrl": "https://github.com/Hestia-Homes/agentic-toolkit.git",
|
"computedHash": "59daf039ac699a44a9416f8ec403b83d4166e05489959e127746231ff8be4e12"
|
||||||
"skillPath": "skills/engineering/to-project/SKILL.md",
|
},
|
||||||
"skillFolderHash": "c28e88e90052bee0dfddc8c525462fe339ff2244"
|
"triage": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/triage/SKILL.md",
|
||||||
|
"computedHash": "2b6efb6da12d92551772fcc04acf331f4e0e6f7bd9d4cb23ce0b301e0b128feb"
|
||||||
|
},
|
||||||
|
"write-a-skill": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/productivity/write-a-skill/SKILL.md",
|
||||||
|
"computedHash": "b44d8aab2ead83c716e01af4c9a24ccc4575ce70ad58ec4f1749fb88c9cc82ba"
|
||||||
|
},
|
||||||
|
"zoom-out": {
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/engineering/zoom-out/SKILL.md",
|
||||||
|
"computedHash": "8357aeaece3b709c442eab67e64b86844e05e2f1ea95b109565eba50b6def36e"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"dismissed": {}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
skills.config.json
Normal file
31
skills.config.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"$schema_comment": "Source of truth for skills-lock.json. Edit this then run scripts/generate-lock.sh.",
|
||||||
|
"agent": "claude-code",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"source": "mattpocock/skills",
|
||||||
|
"skills": [
|
||||||
|
"diagnose",
|
||||||
|
"grill-with-docs",
|
||||||
|
"improve-codebase-architecture",
|
||||||
|
"setup-matt-pocock-skills",
|
||||||
|
"to-issues",
|
||||||
|
"to-prd",
|
||||||
|
"triage",
|
||||||
|
"zoom-out",
|
||||||
|
"caveman",
|
||||||
|
"grill-me",
|
||||||
|
"write-a-skill"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "Hestia-Homes/agentic-toolkit",
|
||||||
|
"localPath": ".",
|
||||||
|
"skills": [
|
||||||
|
"ralph-loop",
|
||||||
|
"to-project",
|
||||||
|
"tdd"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
165
skills/engineering/tdd/SKILL.md
Normal file
165
skills/engineering/tdd/SKILL.md
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
---
|
||||||
|
name: tdd
|
||||||
|
description: Test-driven development using vertical 3A slices. Use when adding behavior, fixing a bug that lacks a regression test, or any work where making a failing test pass is the right unit of progress.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test-Driven Development (3A)
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
|
||||||
|
|
||||||
|
**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
|
||||||
|
|
||||||
|
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
|
||||||
|
|
||||||
|
Tests exercise real code paths through public APIs; mocks are a last resort for I/O boundaries only.
|
||||||
|
|
||||||
|
## Anti-Pattern: Horizontal Slices
|
||||||
|
|
||||||
|
**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
|
||||||
|
|
||||||
|
This produces **crap tests**:
|
||||||
|
|
||||||
|
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior
|
||||||
|
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
|
||||||
|
- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
|
||||||
|
- You outrun your headlights, committing to test structure before understanding the implementation
|
||||||
|
|
||||||
|
**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
|
||||||
|
|
||||||
|
```
|
||||||
|
WRONG (horizontal):
|
||||||
|
RED: test1, test2, test3, test4, test5
|
||||||
|
GREEN: impl1, impl2, impl3, impl4, impl5
|
||||||
|
|
||||||
|
RIGHT (vertical):
|
||||||
|
RED→GREEN: test1→impl1
|
||||||
|
RED→GREEN: test2→impl2
|
||||||
|
RED→GREEN: test3→impl3
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Planning
|
||||||
|
|
||||||
|
When exploring the codebase, use the project's domain glossary so that test names and interface vocabulary match the project's language, and respect ADRs in the area you're touching.
|
||||||
|
|
||||||
|
Before writing any code:
|
||||||
|
|
||||||
|
- [ ] Confirm with user what interface changes are needed
|
||||||
|
- [ ] Confirm with user which behaviors to test (prioritize)
|
||||||
|
- [ ] Identify opportunities for deep modules (small interface, deep implementation)
|
||||||
|
- [ ] Design interfaces for testability
|
||||||
|
- [ ] List the behaviors to test (not implementation steps)
|
||||||
|
- [ ] Get user approval on the plan
|
||||||
|
|
||||||
|
Ask: "What should the public interface look like? Which behaviors are most important to test?"
|
||||||
|
|
||||||
|
**You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
|
||||||
|
|
||||||
|
### 2. Tracer Bullet
|
||||||
|
|
||||||
|
Write ONE test that confirms ONE thing about the system:
|
||||||
|
|
||||||
|
```
|
||||||
|
RED: Write test for first behavior → test fails
|
||||||
|
GREEN: Write minimal code to pass → test passes
|
||||||
|
```
|
||||||
|
|
||||||
|
This is your tracer bullet - proves the path works end-to-end.
|
||||||
|
|
||||||
|
### 3. Incremental Loop
|
||||||
|
|
||||||
|
For each remaining behavior:
|
||||||
|
|
||||||
|
```
|
||||||
|
RED: Write next test → fails
|
||||||
|
GREEN: Minimal code to pass → passes
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- One test at a time
|
||||||
|
- Only enough code to pass current test
|
||||||
|
- Don't anticipate future tests
|
||||||
|
- Keep tests focused on observable behavior
|
||||||
|
|
||||||
|
### 4. Refactor
|
||||||
|
|
||||||
|
After all tests pass, look for refactor candidates:
|
||||||
|
|
||||||
|
- [ ] Extract duplication
|
||||||
|
- [ ] Deepen modules (move complexity behind simple interfaces)
|
||||||
|
- [ ] Apply SOLID principles where natural
|
||||||
|
- [ ] Consider what new code reveals about existing code
|
||||||
|
- [ ] Run tests after each refactor step
|
||||||
|
|
||||||
|
**Never refactor while RED.** Get to GREEN first.
|
||||||
|
|
||||||
|
## Checklist Per Cycle
|
||||||
|
|
||||||
|
```
|
||||||
|
[ ] Test describes behavior, not implementation
|
||||||
|
[ ] Test uses public interface only
|
||||||
|
[ ] Test would survive internal refactor
|
||||||
|
[ ] Code is minimal for this test
|
||||||
|
[ ] No speculative features added
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Structure: Arrange-Act-Assert
|
||||||
|
|
||||||
|
Every test must be structured in three clearly separated phases:
|
||||||
|
|
||||||
|
- **Arrange** — set up inputs, state, and dependencies
|
||||||
|
- **Act** — invoke the unit under test (one call per test)
|
||||||
|
- **Assert** — verify the single observable outcome
|
||||||
|
|
||||||
|
Keep each phase visually distinct with a blank line between phases and inline `// Arrange / // Act / // Assert` comments, where the framework doesn't make the separation obvious.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stub-First Reds
|
||||||
|
|
||||||
|
Before writing a failing test, define stub implementations for all functions, methods, and classes the test will import. Stubs must:
|
||||||
|
|
||||||
|
- Exist at the correct import path
|
||||||
|
- Have the correct signature
|
||||||
|
- Raise / throw a "not implemented" error (e.g. `raise NotImplementedError`, `throw new Error('not implemented')`)
|
||||||
|
|
||||||
|
This ensures RED tests fail because the **behavior is missing**, not because of import or attribute errors. A test that errors on import is not a RED test — it is a broken test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Red-Green-Refactor: Stage Gates
|
||||||
|
|
||||||
|
After completing each stage, **stop and wait** before proceeding to the next. At each gate:
|
||||||
|
|
||||||
|
1. **After RED** — present the suggested commit message, run tests to confirm they fail for the right reason (not import/attribute errors), then wait for the user to commit manually.
|
||||||
|
2. **After GREEN** — present the suggested commit message, confirm all tests pass, then wait for the user to commit manually.
|
||||||
|
3. **After REFACTOR** — present the suggested commit message, confirm tests still pass and no behavior changed, then wait for the user to commit manually.
|
||||||
|
|
||||||
|
Do not continue to the next stage until the user has committed and explicitly given the go-ahead.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
One sentence describing the **behavior being delivered**, followed by a stage emoji. The sentence must describe what the system now does, not the TDD action taken.
|
||||||
|
|
||||||
|
| Stage | Emoji |
|
||||||
|
|----------|-------|
|
||||||
|
| Red | 🟥 |
|
||||||
|
| Green | 🟩 |
|
||||||
|
| Refactor | 🟪 |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Map API response to domain object 🟥
|
||||||
|
Map API response to domain object 🟩
|
||||||
|
Map API response to domain object 🟪
|
||||||
|
```
|
||||||
|
|
||||||
|
Not: "wrote failing mapping tests" or "made tests pass" — those describe the process, not the behavior.
|
||||||
Loading…
Add table
Reference in a new issue