From 5c12f8bf438e59b089bde40567bbd21b6f116d40 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 13 May 2026 15:45:44 +0000 Subject: [PATCH] added tdd --- .gitignore | 3 + scripts/generate-lock.sh | 135 ++++++++++++++++++++++++++++++++ skills-lock.json | 7 +- skills.config.json | 31 ++++++++ skills/engineering/tdd/SKILL.md | 2 +- 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100755 scripts/generate-lock.sh create mode 100644 skills.config.json diff --git a/.gitignore b/.gitignore index 368543d..576b7fc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ dist .env.local coverage .vitest-cache +# skills CLI side-effects when running `skills add` locally +.agents/ +.claude/ diff --git a/scripts/generate-lock.sh b/scripts/generate-lock.sh new file mode 100755 index 0000000..74ab053 --- /dev/null +++ b/scripts/generate-lock.sh @@ -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 --skill ... --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" diff --git a/skills-lock.json b/skills-lock.json index e5f9a7b..ceb79a6 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -34,7 +34,6 @@ "ralph-loop": { "source": "Hestia-Homes/agentic-toolkit", "sourceType": "github", - "skillPath": "skills/engineering/ralph-loop/SKILL.md", "computedHash": "6d6c256a5145008038d128089fa17ed78ac9350d98efd977624da21d9255988b" }, "setup-matt-pocock-skills": { @@ -43,6 +42,11 @@ "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", @@ -58,7 +62,6 @@ "to-project": { "source": "Hestia-Homes/agentic-toolkit", "sourceType": "github", - "skillPath": "skills/engineering/to-project/SKILL.md", "computedHash": "59daf039ac699a44a9416f8ec403b83d4166e05489959e127746231ff8be4e12" }, "triage": { diff --git a/skills.config.json b/skills.config.json new file mode 100644 index 0000000..27e1f84 --- /dev/null +++ b/skills.config.json @@ -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" + ] + } + ] +} diff --git a/skills/engineering/tdd/SKILL.md b/skills/engineering/tdd/SKILL.md index 675af60..6ecd2c9 100644 --- a/skills/engineering/tdd/SKILL.md +++ b/skills/engineering/tdd/SKILL.md @@ -1,6 +1,6 @@ --- name: tdd -description: Test-driven development using vertical 3A slices (Arrange-Act-Assert, one test → one implementation at a time). Tests verify behavior through public interfaces, not implementation details. Use when adding behavior to a codebase, fixing a bug that lacks a regression test, or any work where "make a failing test pass" is the right unit of progress. +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)