scripts/setup-agent.sh
#!/usr/bin/env bash
# setup-agent.sh — one-command agent automation setup for signaltty.
# Works from any checkout on any machine with only bash + git:
# scripts/setup-agent.sh all
# See docs/15-agent-skills.md for the full routing reference.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Pinned upstream revs (refresh: `setup-agent.sh pins`, then edit + commit).
PIN_mattpocock="c55ee46073ed923f86ce59a5eb3b6d895095d1b7"
PIN_anthropics="8a1541c4a3ffa5a20a5a91de0dcf3f0bab1d1ef4"
PIN_emilkowalski="d16ebe60d09a5ba2afcb7054ede9d0a10c9f6128"
PIN_karpathy="2c606141936f1eeef17fa3043a72095b4765b9c2"
PIN_pstack="adf3218ca2f5b9971eedc07a76bef22df7701539"
PIN_speckit="8d3f64cdccc877b6a297bc8167131103bb5b8ca0" # vendored under .specify/, see SOURCES.md
URL_mattpocock="https://github.com/mattpocock/skills"
URL_anthropics="https://github.com/anthropics/skills"
URL_emilkowalski="https://github.com/emilkowalski/skills"
URL_karpathy="https://github.com/multica-ai/andrej-karpathy-skills" # fork of forrestchang/andrej-karpathy-skills
URL_pstack="https://github.com/cursor/plugins"
URL_speckit="https://github.com/github/spec-kit"
SPARSE_mattpocock="skills"
SPARSE_anthropics="skills"
SPARSE_emilkowalski="skills"
SPARSE_karpathy="skills"
SPARSE_pstack="pstack/skills"
CACHE_DEFAULT="${XDG_CACHE_HOME:-$HOME/.cache}/signaltty/upstream"
# Curated skill table: repo|source path|installed name|one-liner.
# Installed names ending in "-tdd/-teach/prototype" are renamed on collision.
read -r -d '' SKILLS <<'EOF' || true
mattpocock|skills/engineering/ask-matt|ask-matt|Router: which skill or flow fits the situation.
mattpocock|skills/engineering/code-review|code-review|Review changes since a fixed point on Standards plus Spec axes.
mattpocock|skills/engineering/codebase-design|codebase-design|Shared vocabulary for designing deep modules.
mattpocock|skills/engineering/diagnosing-bugs|diagnosing-bugs|Diagnosis loop for hard bugs and perf regressions.
mattpocock|skills/engineering/domain-modeling|domain-modeling|Build and sharpen the domain model: terminology, CONTEXT, ADRs.
mattpocock|skills/engineering/grill-with-docs|grill-with-docs|Relentless interview to sharpen a plan; writes ADRs plus glossary.
mattpocock|skills/engineering/implement|implement|Implement a piece of work from a spec or ticket set.
mattpocock|skills/engineering/improve-codebase-architecture|improve-codebase-architecture|Scan for deepening opportunities; visual report, then grill.
mattpocock|skills/engineering/prototype|prototype|Throwaway code prototype to answer a design question.
mattpocock|skills/engineering/research|research|Investigate against high-trust primary sources; capture as repo Markdown.
mattpocock|skills/engineering/resolving-merge-conflicts|resolving-merge-conflicts|Resolve an in-progress git merge or rebase conflict.
mattpocock|skills/engineering/tdd|tdd|Test-driven development, red-green-refactor, integration tests.
mattpocock|skills/engineering/to-spec|to-spec|Turn the current conversation into a spec.
mattpocock|skills/engineering/to-tickets|to-tickets|Break a plan or spec into tracer-bullet tickets with blocking edges.
mattpocock|skills/engineering/wayfinder|wayfinder|Plan huge multi-session work as a map of decision tickets.
mattpocock|skills/engineering/wizard|wizard|Interactive bash wizard for human-only steps (infra, secrets).
mattpocock|skills/productivity/grill-me|grill-me|Relentless interview to sharpen a plan or design.
mattpocock|skills/productivity/handoff|handoff|Compact a conversation into a handoff doc for another agent.
mattpocock|skills/productivity/teach|teach|Teach the user a skill or concept within this workspace.
mattpocock|skills/productivity/writing-for-agents|writing-for-agents|Write or edit agent docs: skills, AGENTS.md, CLAUDE.md.
anthropics|skills/frontend-design|frontend-design|Aesthetic direction for distinctive, non-templated UI.
anthropics|skills/skill-creator|skill-creator|Create, edit, and evaluate skills.
emilkowalski|skills/emil-design-eng|emil-design-eng|UI polish, component design, and animation philosophy.
emilkowalski|skills/apple-design|apple-design|Apple interface-design and fluid-motion principles.
emilkowalski|skills/animation-vocabulary|animation-vocabulary|Vague motion description to exact term, reverse-lookup glossary.
emilkowalski|skills/find-animation-opportunities|find-animation-opportunities|Read-only: UI that should animate (exact values) and what should not.
emilkowalski|skills/improve-animations|improve-animations|Read-only codebase motion audit with prioritized plans.
emilkowalski|skills/review-animations|review-animations|Strict review of animation code; approval is earned.
emilkowalski|skills/prototype|emilkowalski-prototype|Genuinely different UI versions behind a visual picker.
karpathy|skills/karpathy-guidelines|karpathy-guidelines|Think before coding, simplicity first, surgical changes.
pstack|pstack/skills/architect|architect|Sketch types, signatures, module structure before code.
pstack|pstack/skills/arena|arena|Spawn N parallel candidates, pick a base, graft the best parts.
pstack|pstack/skills/blast-radius|blast-radius|Find what a change could break beyond the diff; prove safety by running code.
pstack|pstack/skills/bro|bro|Restate the last message in plain human language, no jargon.
pstack|pstack/skills/create-verification-skill|create-verification-skill|Generate a project-local verification skill driving the app like a user.
pstack|pstack/skills/figure-it-out|figure-it-out|Auditable playbook for large migrations; hypothesis loop plus decision log.
pstack|pstack/skills/how|how|Code walkthroughs: subsystem architecture, runtime flow, ownership.
pstack|pstack/skills/interrogate|interrogate|Multi-reviewer adversarial review from independent angles.
pstack|pstack/skills/maintain-verification-skill|maintain-verification-skill|Periodic pass keeping the verification skill and feature map honest.
pstack|pstack/skills/no-comments|no-comments|Spawn Comment Sicko review, fix accepted findings.
pstack|pstack/skills/recall|recall|Reconstruct working context from history and live state into a brief.
pstack|pstack/skills/reflect|reflect|Three parallel transcript reviews into concrete edits on existing skills.
pstack|pstack/skills/show-me-your-work|show-me-your-work|Keep a reviewable decision trail for long or unattended work.
pstack|pstack/skills/swarm|swarm|Fan out N parallel workers, drain, return one report.
pstack|pstack/skills/tdd|pstack-tdd|TDD only when explicitly asked or the bug has a cheap local test target.
pstack|pstack/skills/teach|pstack-teach|Explain a body of work plainly via how plus why.
pstack|pstack/skills/technical-writing|technical-writing|Layered standard: Diataxis plus Google style plus STE.
pstack|pstack/skills/unslop|unslop|Cut AI tells from any writing; always applies.
pstack|pstack/skills/why|why|Design rationale, regressions, postmortems via parallel evidence queries.
pstack|pstack/skills/principle-attack-the-premise|principle-attack-the-premise|After 2 plus failed fixes sharing one premise, question the premise.
pstack|pstack/skills/principle-boundary-discipline|principle-boundary-discipline|Guards at system boundaries; trust internal types.
pstack|pstack/skills/principle-build-the-lever|principle-build-the-lever|Build the rerunnable tool instead of working by hand.
pstack|pstack/skills/principle-encode-lessons-in-structure|principle-encode-lessons-in-structure|Second occurrence of a rule becomes a lint, flag, or check.
pstack|pstack/skills/principle-exhaust-the-design-space|principle-exhaust-the-design-space|Novel decision: 2 to 3 competing prototypes, compare, then commit.
pstack|pstack/skills/principle-experience-first|principle-experience-first|User delight over implementation convenience.
pstack|pstack/skills/principle-fix-root-causes|principle-fix-root-causes|Reproduce, ask why to the root, fix there.
pstack|pstack/skills/principle-foundational-thinking|principle-foundational-thinking|Data structures and scaffolding order right before logic.
pstack|pstack/skills/principle-guard-the-context-window|principle-guard-the-context-window|Bulk to subagents; summaries, not raw payloads, in main thread.
pstack|pstack/skills/principle-laziness-protocol|principle-laziness-protocol|Delete; smallest change that solves the problem.
pstack|pstack/skills/principle-make-operations-idempotent|principle-make-operations-idempotent|Converge to the same end state across retries and restarts.
pstack|pstack/skills/principle-migrate-callers-then-delete-legacy-apis|principle-migrate-callers-then-delete-legacy-apis|Migrate plus delete the old API in one wave, no compat layers.
pstack|pstack/skills/principle-minimize-reader-load|principle-minimize-reader-load|Collapse one-caller wrappers, shrink mutable scope.
pstack|pstack/skills/principle-model-the-domain|principle-model-the-domain|Encode the domain in a structure, not scattered conditionals.
pstack|pstack/skills/principle-never-block-on-the-human|principle-never-block-on-the-human|Proceed on reversible work; confirm only irreversible actions.
pstack|pstack/skills/principle-outcome-oriented-execution|principle-outcome-oriented-execution|Converge on target architecture; no throwaway compat states.
pstack|pstack/skills/principle-prove-it-works|principle-prove-it-works|Verify against the real artifact before declaring done.
pstack|pstack/skills/principle-redesign-from-first-principles|principle-redesign-from-first-principles|Integrate new requirements as if foundational, not bolted on.
pstack|pstack/skills/principle-separate-before-serializing-shared-state|principle-separate-before-serializing-shared-state|Eliminate concurrent-writer sharing first.
pstack|pstack/skills/principle-sequence-verifiable-units|principle-sequence-verifiable-units|Small units each ending in a verifiable state.
pstack|pstack/skills/principle-subtract-before-you-add|principle-subtract-before-you-add|Remove dead or redundant code first, then build.
pstack|pstack/skills/principle-test-behavior-not-implementation|principle-test-behavior-not-implementation|Call code like users do; literal expected values.
pstack|pstack/skills/principle-type-system-discipline|principle-type-system-discipline|Illegal states unrepresentable; parse at boundaries; exhaust variants.
EOF
usage() {
cat <<'USAGE'
Usage: setup-agent.sh <area> <action> [options]
skills install [--target auto|DIR] [--cache DIR] [--dry-run]
skills check [--target auto|DIR]
skills list
commands install --agent auto|all|claude|codex|cursor|opencode|gemini|copilot|muse [--dry-run]
commands check --agent auto|all|claude|codex|cursor|opencode|gemini|copilot|muse
hooks install
pins
verify
all [--target ...] [--agent ...] [--cache DIR] # skills + commands + hooks
USAGE
}
die() { echo "setup-agent.sh: $*" >&2; exit 1; }
need_git() { command -v git >/dev/null || die "git is required"; }
pin_of() { # $1=repo key -> pinned rev
case "$1" in
mattpocock) echo "$PIN_mattpocock" ;;
anthropics) echo "$PIN_anthropics" ;;
emilkowalski) echo "$PIN_emilkowalski" ;;
karpathy) echo "$PIN_karpathy" ;;
pstack) echo "$PIN_pstack" ;;
speckit) echo "$PIN_speckit" ;;
*) die "unknown repo: $1" ;;
esac
}
url_of() {
case "$1" in
mattpocock) echo "$URL_mattpocock" ;;
anthropics) echo "$URL_anthropics" ;;
emilkowalski) echo "$URL_emilkowalski" ;;
karpathy) echo "$URL_karpathy" ;;
pstack) echo "$URL_pstack" ;;
speckit) echo "$URL_speckit" ;;
*) die "unknown repo: $1" ;;
esac
}
sparse_of() {
case "$1" in
mattpocock) echo "$SPARSE_mattpocock" ;;
anthropics) echo "$SPARSE_anthropics" ;;
emilkowalski) echo "$SPARSE_emilkowalski" ;;
karpathy) echo "$SPARSE_karpathy" ;;
pstack) echo "$SPARSE_pstack" ;;
*) die "unknown repo: $1" ;;
esac
}
# Global skills dirs, one per detected harness home. Order is preference order.
detect_skills_targets() {
[ -d "$HOME/.agents" ] && echo "$HOME/.agents/skills"
[ -d "$HOME/.claude" ] && echo "$HOME/.claude/skills"
[ -d "$HOME/.codex" ] && echo "$HOME/.codex/skills"
}
ensure_repo() { # $1=key $2=cache
need_git
local key="$1" cache="$2" dir rev url sparse
dir="$cache/$key"; rev="$(pin_of "$key")"; url="$(url_of "$key")"; sparse="$(sparse_of "$key")"
if [ -d "$dir/.git" ]; then
if [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$rev" ]; then return 0; fi
if ! git -C "$dir" cat-file -e "$rev" 2>/dev/null; then
git -C "$dir" fetch --depth 1 origin "$rev" || die "cannot fetch $rev for $key (offline?)"
fi
else
mkdir -p "$cache"
git clone --depth 1 --filter=blob:none --sparse "$url" "$dir" || die "cannot clone $url"
git -C "$dir" sparse-checkout set "$sparse"
if ! git -C "$dir" cat-file -e "$rev" 2>/dev/null; then
git -C "$dir" fetch --depth 1 origin "$rev" || die "cannot fetch $rev for $key"
fi
fi
git -C "$dir" checkout --detach -q "$rev"
}
skills_install() { # --target --cache --dry-run
local target="auto" cache="$CACHE_DEFAULT" dry_run=0
while [ $# -gt 0 ]; do
case "$1" in
--target) target="$2"; shift 2 ;;
--cache) cache="$2"; shift 2 ;;
--dry-run) dry_run=1; shift ;;
*) die "unknown flag: $1" ;;
esac
done
local targets=()
if [ "$target" = "auto" ]; then
while IFS= read -r t; do [ -n "$t" ] && targets+=("$t"); done < <(detect_skills_targets)
[ "${#targets[@]}" -gt 0 ] || die "no harness home detected (~/.agents ~/.claude ~/.codex); pass --target DIR"
else
targets=("$target")
fi
for key in mattpocock anthropics emilkowalski karpathy pstack; do
[ "$dry_run" -eq 1 ] || ensure_repo "$key" "$cache"
done
local new=0 updated=0 same=0
while IFS='|' read -r repo src dest _desc; do
[ -n "$repo" ] || continue
local from="$cache/$repo/$src"
for t in "${targets[@]}"; do
local to="$t/$dest"
if [ "$dry_run" -eq 1 ]; then
echo "would install $dest -> $to"
continue
fi
[ -f "$from/SKILL.md" ] || die "missing $from/SKILL.md (cache: $cache)"
if [ -d "$to" ] && diff -r -q "$from" "$to" >/dev/null 2>&1; then
same=$((same + 1))
else
if [ -d "$to" ]; then updated=$((updated + 1)); else new=$((new + 1)); fi
rm -rf "$to"
mkdir -p "$t"
cp -r "$from" "$to"
chmod -R u+w "$to"
fi
done
done <<<"$SKILLS"
echo "skills: $new new, $updated updated, $same unchanged (${#targets[@]} target(s))"
}
skills_check() { # --target --cache
local target="auto" cache="$CACHE_DEFAULT"
while [ $# -gt 0 ]; do
case "$1" in
--target) target="$2"; shift 2 ;;
--cache) cache="$2"; shift 2 ;;
*) die "unknown flag: $1" ;;
esac
done
local targets=()
if [ "$target" = "auto" ]; then
while IFS= read -r t; do [ -n "$t" ] && targets+=("$t"); done < <(detect_skills_targets)
[ "${#targets[@]}" -gt 0 ] || die "no harness home detected; pass --target DIR"
else
targets=("$target")
fi
local missing=0 total=0
for t in "${targets[@]}"; do
while IFS='|' read -r _repo _src dest _desc; do
[ -n "$_repo" ] || continue
total=$((total + 1))
if [ ! -f "$t/$dest/SKILL.md" ]; then
echo "missing: $t/$dest"
missing=$((missing + 1))
elif [ ! -d "$cache/$_repo/$_src" ] ||
[ "$(git -C "$cache/$_repo" rev-parse HEAD 2>/dev/null)" != "$(pin_of "$_repo")" ] ||
! diff -r -q "$cache/$_repo/$_src" "$t/$dest" >/dev/null 2>&1; then
echo "unverified revision or modified skill: $t/$dest (run skills install with the same --cache)"
missing=$((missing + 1))
fi
done <<<"$SKILLS"
done
if [ "$missing" -gt 0 ]; then
echo "skills check: $missing/$total missing" >&2
return 1
fi
echo "skills check: $total present in ${#targets[@]} target(s)"
}
skills_list() {
while IFS='|' read -r repo src dest desc; do
[ -n "$repo" ] || continue
printf '%-52s %-12s %s\n' "$dest" "$repo" "$desc"
done <<<"$SKILLS"
echo "---"
echo "renamed on collision: emilkowalski-prototype, pstack-tdd, pstack-teach"
}
# spec-kit slash commands: project-local layout per harness (matches `specify init`).
agent_dest() { # $1=agent -> "layout|reldir"
case "$1" in
claude) echo "skills|.claude/skills" ;;
codex) echo "skills|.agents/skills" ;;
muse) echo "skills|.agents/skills" ;;
cursor) echo "skills|.cursor/skills" ;;
copilot) echo "skills|.github/skills" ;;
opencode) echo "commands|.opencode/commands" ;;
gemini) echo "commands|.gemini/commands" ;;
*) die "unknown agent: $1" ;;
esac
}
detect_agents() {
[ -d "$HOME/.claude" ] && echo "claude"
[ -d "$HOME/.codex" ] && echo "codex"
[ -d "$HOME/.agents" ] && echo "muse"
[ -d "$HOME/.cursor" ] && echo "cursor"
}
resolve_agents() { # $1=auto|all|name -> names, deduped by dest dir
local sel="$1" names=()
if [ "$sel" = "auto" ]; then
while IFS= read -r a; do [ -n "$a" ] && names+=("$a"); done < <(detect_agents)
[ "${#names[@]}" -gt 0 ] || die "no harness detected; pass --agent explicitly"
elif [ "$sel" = "all" ]; then
names=(claude codex cursor copilot opencode gemini muse)
else
agent_dest "$sel" >/dev/null
names=("$sel")
fi
local seen="" a key
for a in "${names[@]}"; do
key="$(agent_dest "$a")"
case " $seen " in
*" $key "*) ;;
*) seen="$seen $key"; echo "$a" ;;
esac
done
}
render_command() { # layout name source
if [ "$1" = "skills" ]; then
awk -v n="$2" 'NR==1 && $0=="---" {print; print "name: " n; next} {print}' "$3"
else
cat "$3"
fi
}
commands_install() { # --agent --dry-run
local agent="auto" dry_run=0
while [ $# -gt 0 ]; do
case "$1" in
--agent) agent="$2"; shift 2 ;;
--dry-run) dry_run=1; shift ;;
*) die "unknown flag: $1" ;;
esac
done
local src_dir="$ROOT/.specify/commands"
[ -d "$src_dir" ] || die "$src_dir missing (spec-kit not vendored?)"
while IFS= read -r a; do
[ -n "$a" ] || continue
local spec dest_dir layout reldir
spec="$(agent_dest "$a")"; layout="${spec%%|*}"; reldir="${spec##*|}"
dest_dir="$ROOT/$reldir"
for src in "$src_dir"/*.md; do
local base name
base="$(basename "$src" .md)"; name="speckit-$base"
if [ "$layout" = "skills" ]; then
local to="$dest_dir/$name/SKILL.md"
if [ "$dry_run" -eq 1 ]; then echo "would install $a: $to"; continue; fi
mkdir -p "$dest_dir/$name"
# Ensure `name:` frontmatter for the skills layout; keep body verbatim.
render_command "$layout" "$name" "$src" >"$to"
else
local to="$dest_dir/$base.md"
if [ "$dry_run" -eq 1 ]; then echo "would install $a: $to"; continue; fi
mkdir -p "$dest_dir"
render_command "$layout" "$name" "$src" >"$to"
fi
done
echo "commands: installed for $a ($reldir)"
done < <(resolve_agents "$agent")
}
commands_check() { # --agent --installed-only
local agent="auto" installed_only=0
while [ $# -gt 0 ]; do
case "$1" in
--agent) agent="$2"; shift 2 ;;
--installed-only) installed_only=1; shift ;;
*) die "unknown flag: $1" ;;
esac
done
local missing=0 total=0
while IFS= read -r a; do
[ -n "$a" ] || continue
local spec layout reldir
spec="$(agent_dest "$a")"; layout="${spec%%|*}"; reldir="${spec##*|}"
if [ "$installed_only" -eq 1 ]; then
local installed=0 src base
for src in "$ROOT/.specify/commands"/*.md; do
base="$(basename "$src" .md)"
if [ "$layout" = "skills" ]; then
[ ! -f "$ROOT/$reldir/speckit-$base/SKILL.md" ] || installed=1
else
[ ! -f "$ROOT/$reldir/$base.md" ] || installed=1
fi
done
[ "$installed" -eq 1 ] || continue
fi
for src in "$ROOT/.specify/commands"/*.md; do
local base name to
base="$(basename "$src" .md)"; name="speckit-$base"
if [ "$layout" = "skills" ]; then to="$ROOT/$reldir/$name/SKILL.md"; else to="$ROOT/$reldir/$base.md"; fi
total=$((total + 1))
if [ ! -f "$to" ]; then
echo "missing: $to"; missing=$((missing + 1))
elif ! cmp -s "$to" <(render_command "$layout" "$name" "$src"); then
echo "changed: $to"; missing=$((missing + 1))
fi
done
done < <(resolve_agents "$agent")
if [ "$missing" -gt 0 ]; then
echo "commands check: $missing/$total missing or changed" >&2
return 1
fi
echo "commands check: $total match source"
}
hooks_install() {
git -C "$ROOT" config core.hooksPath .githooks
echo "hooks: core.hooksPath=.githooks"
}
pins() {
need_git
for key in mattpocock anthropics emilkowalski karpathy pstack speckit; do
local pinned latest
pinned="$(pin_of "$key")"
latest="$(git ls-remote "$(url_of "$key")" HEAD 2>/dev/null | cut -f1)"
[ -z "$latest" ] && latest="(unreachable)"
printf '%-14s pinned %s\n%-14s latest %s\n' "$key" "$pinned" "" "$latest"
done
}
cmd_verify() {
local fail=0
skills_check --target auto || fail=1
commands_check --agent auto || fail=1
if [ "$(git -C "$ROOT" config core.hooksPath 2>/dev/null)" = ".githooks" ]; then
echo "hooks: configured"
else
echo "hooks: NOT configured (run: hooks install)"
fail=1
fi
[ -d "$ROOT/.specify" ] && echo "spec-kit: vendored" || { echo "spec-kit: MISSING .specify/"; fail=1; }
return $fail
}
cmd_all() { # --target --agent --cache
local target="auto" agent="auto" cache="$CACHE_DEFAULT"
while [ $# -gt 0 ]; do
case "$1" in
--target) target="$2"; shift 2 ;;
--agent) agent="$2"; shift 2 ;;
--cache) cache="$2"; shift 2 ;;
*) die "unknown flag: $1" ;;
esac
done
skills_install --target "$target" --cache "$cache"
commands_install --agent "$agent"
hooks_install
}
main() {
[ $# -ge 1 ] || { usage; exit 1; }
case "$1" in
skills)
[ $# -ge 2 ] || { usage; exit 1; }
case "$2" in
install) shift 2; skills_install "$@" ;;
check) shift 2; skills_check "$@" ;;
list) skills_list ;;
*) usage; exit 1 ;;
esac
;;
commands)
[ $# -ge 2 ] || { usage; exit 1; }
case "$2" in
install) shift 2; commands_install "$@" ;;
check) shift 2; commands_check "$@" ;;
*) usage; exit 1 ;;
esac
;;
hooks)
[ "$2" = "install" ] || { usage; exit 1; }
hooks_install
;;
pins) pins ;;
verify) cmd_verify ;;
all) shift; cmd_all "$@" ;;
-h | --help | help) usage ;;
*) usage; exit 1 ;;
esac
}
main "$@"