- L1: install.sh post-install banners '~14 generated agents' → '12 generated agents' (both the activated-path and the manual-merge-path copies). - L2: skills/compose-solution/SKILL.md handoff reference '14 kit agents' → '12 kit agents'. - L3: README /new-agent section rephrases 'via option-pickers' to call out that the 6 questions are grouped into multiple option-picker batches (two AskUserQuestion calls) rather than six separate prompts.
418 lines
16 KiB
Bash
Executable file
418 lines
16 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# KeiSeiKit — Constructor-Pattern Agent Kit installer
|
|
# Idempotent: safe to re-run. Never overwrites settings.json or existing user manifests.
|
|
#
|
|
# Usage:
|
|
# ./install.sh # install agents + hooks + skills + bridges/
|
|
# ./install.sh --with-bridges # also render cross-tool bridges into $PWD
|
|
# (AGENTS.md, .cursorrules, .cursor/rules/main.mdc,
|
|
# .github/copilot-instructions.md, Windsurf, Junie,
|
|
# Continue, Gemini, Aider, Replit — 11 files total)
|
|
# Skipped if $PWD is the KeiSeiKit repo itself.
|
|
|
|
set -euo pipefail
|
|
|
|
KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
HOME_DIR="${HOME:?HOME not set}"
|
|
AGENTS_DIR="$HOME_DIR/.claude/agents"
|
|
HOOKS_DIR="$HOME_DIR/.claude/hooks"
|
|
SKILLS_DIR="$HOME_DIR/.claude/skills"
|
|
|
|
# --- flag parsing ----------------------------------------------------------
|
|
ACTIVATE_HOOKS=0
|
|
WITH_BRIDGES=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--activate-hooks) ACTIVATE_HOOKS=1 ;;
|
|
--with-bridges) WITH_BRIDGES=1 ;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
Usage: ./install.sh [--activate-hooks] [--with-bridges]
|
|
|
|
--activate-hooks jq-merge settings-snippet.json into ~/.claude/settings.json
|
|
non-interactively. Without this flag, a TTY prompt asks
|
|
at the end; non-TTY runs print manual instructions.
|
|
|
|
--with-bridges After install, render the 11 cross-tool bridge files
|
|
(Cursor / Copilot / Codex / Windsurf / Junie / Continue /
|
|
Aider / Replit / Antigravity / Warp / Zed) into \$PWD.
|
|
Skipped if invoked inside the KeiSeiKit repo itself.
|
|
EOF
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ANSI on iff stdout is a TTY and NO_COLOR is unset (respect no-color.org).
|
|
if [ -t 1 ] && [ "${NO_COLOR:-}" = "" ]; then
|
|
COLOR=1
|
|
else
|
|
COLOR=0
|
|
fi
|
|
if [ "$COLOR" = "1" ]; then
|
|
say() { printf '\033[1;36m[install]\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
|
|
err() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; }
|
|
else
|
|
say() { printf '[install] %s\n' "$*"; }
|
|
warn() { printf '[warn] %s\n' "$*"; }
|
|
err() { printf '[error] %s\n' "$*" >&2; }
|
|
fi
|
|
|
|
# --- rollback bookkeeping ---------------------------------------------------
|
|
# Every successful backup_dir / per-file backup appends a "ORIGINAL|BACKUP"
|
|
# pair to BACKUP_PAIRS. On ERR the trap walks the list in reverse and atomically
|
|
# swaps BACKUP back onto ORIGINAL. A boolean guard makes rollback idempotent.
|
|
BACKUP_PAIRS=()
|
|
ROLLED_BACK=0
|
|
|
|
rollback() {
|
|
[ "$ROLLED_BACK" = "1" ] && return 0
|
|
ROLLED_BACK=1
|
|
if [ "${#BACKUP_PAIRS[@]}" -eq 0 ]; then
|
|
err "install failed at line ${BASH_LINENO[0]:-?}; no backups to restore"
|
|
return 0
|
|
fi
|
|
warn "install failed — rolling back ${#BACKUP_PAIRS[@]} backup(s)"
|
|
local i pair orig bak
|
|
for (( i=${#BACKUP_PAIRS[@]}-1; i>=0; i-- )); do
|
|
pair="${BACKUP_PAIRS[$i]}"
|
|
orig="${pair%%|*}"
|
|
bak="${pair#*|}"
|
|
if [ -e "$bak" ]; then
|
|
# Guard rm -rf: only remove $orig if it actually exists as a file or
|
|
# directory. Harmless either way, but explicit is safer than brittle.
|
|
if [ -d "$orig" ] || [ -f "$orig" ]; then
|
|
rm -rf "$orig"
|
|
fi
|
|
mv "$bak" "$orig"
|
|
say " restored $orig from $bak"
|
|
fi
|
|
done
|
|
err "install failed at line ${BASH_LINENO[0]:-?}; rolled back"
|
|
}
|
|
trap rollback ERR
|
|
|
|
# Backup a populated target directory to a timestamped sibling before clobber.
|
|
# No-op if target is absent or contains no regular files (recursively). This
|
|
# means freshly-mkdir'd scaffolds are NOT backed up — only real user content.
|
|
# Only called on $AGENTS_DIR/_blocks, _templates, _assembler, $SKILLS_DIR —
|
|
# never on $KIT_DIR source. (hooks are now per-file; see backup_file.)
|
|
backup_dir() {
|
|
local target="$1"
|
|
[ -d "$target" ] || return 0
|
|
# No regular files anywhere under target → nothing worth preserving
|
|
if [ -z "$(find "$target" -type f -print -quit 2>/dev/null)" ]; then
|
|
return 0
|
|
fi
|
|
local backup="${target}.bak-$(date +%s)"
|
|
cp -a "$target" "$backup"
|
|
BACKUP_PAIRS+=("$target|$backup")
|
|
say "backed up existing $target to $backup"
|
|
}
|
|
|
|
# Per-file backup for shared directories like $HOOKS_DIR, where other kits
|
|
# may drop sibling files we must not touch. Only the specific file is moved
|
|
# aside to <file>.bak-TIMESTAMP.
|
|
backup_file() {
|
|
local target="$1"
|
|
[ -f "$target" ] || return 0
|
|
local backup="${target}.bak-$(date +%s)"
|
|
mv "$target" "$backup"
|
|
BACKUP_PAIRS+=("$target|$backup")
|
|
say "backed up existing $target to $backup"
|
|
}
|
|
|
|
# Activate KeiSeiKit hooks by merging settings-snippet.json into the user's
|
|
# settings.json. Idempotent:
|
|
# - If settings.json is absent, copy snippet verbatim (minus _comment key).
|
|
# - If present, concatenate the snippet's PostToolUse / PreToolUse entries
|
|
# onto existing arrays, then de-dupe by the nested hooks[].command field
|
|
# so re-runs do not stack duplicate entries.
|
|
# - .hooks itself (the root object key) is merged with `*` — snippet wins on
|
|
# scalar keys, arrays are unioned then de-duped.
|
|
# Requires jq (already checked earlier in prerequisites). Writes atomically
|
|
# via a tmpfile in the same dir.
|
|
activate_hooks() {
|
|
local snippet="$KIT_DIR/settings-snippet.json"
|
|
local target="$HOME_DIR/.claude/settings.json"
|
|
local tmp
|
|
[ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; }
|
|
if [ ! -f "$target" ]; then
|
|
# Strip _comment, keep the rest. Create atomically.
|
|
tmp="$(mktemp "$target.XXXXXX")"
|
|
jq 'del(._comment)' "$snippet" > "$tmp"
|
|
mv "$tmp" "$target"
|
|
say "created $target from snippet (no prior settings.json)"
|
|
return 0
|
|
fi
|
|
# Merge path: back up the pre-merge settings.json so rollback can restore
|
|
# it if a later step ERR-traps. The "create new" path above exits before
|
|
# reaching here, so backup_file is only invoked when $target exists.
|
|
backup_file "$target"
|
|
# Merge: walk each matcher-group in PostToolUse / PreToolUse, append hooks,
|
|
# unique_by command. jq filter is written for readability, not golf.
|
|
tmp="$(mktemp "$target.XXXXXX")"
|
|
jq --slurpfile snip "$snippet" '
|
|
. as $orig
|
|
| ($snip[0] | del(._comment)) as $add
|
|
| reduce ($add.hooks | keys[]) as $phase ($orig;
|
|
.hooks[$phase] = (
|
|
((.hooks[$phase] // []) + ($add.hooks[$phase] // []))
|
|
| group_by(.matcher)
|
|
| map({
|
|
matcher: .[0].matcher,
|
|
hooks: (map(.hooks // []) | add | unique_by(.command))
|
|
})
|
|
)
|
|
)
|
|
' "$target" > "$tmp"
|
|
# Only replace if jq produced non-empty valid JSON
|
|
if [ -s "$tmp" ] && jq -e . "$tmp" >/dev/null 2>&1; then
|
|
mv "$tmp" "$target"
|
|
say "merged hooks into $target (idempotent)"
|
|
else
|
|
rm -f "$tmp"
|
|
err "jq-merge produced invalid output; $target unchanged"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# --- prerequisites ----------------------------------------------------------
|
|
say "checking prerequisites"
|
|
if ! command -v cargo >/dev/null 2>&1; then
|
|
err "cargo not found. Install Rust: https://rustup.rs/"
|
|
exit 1
|
|
fi
|
|
# Verify cargo actually runs (catches "rustup has no default toolchain")
|
|
if ! cargo --version >/dev/null 2>&1; then
|
|
err "cargo is installed but not functional. Run: rustup default stable"
|
|
exit 1
|
|
fi
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
err "jq not found. jq is REQUIRED on any machine that will activate the"
|
|
err "KeiSeiKit hooks — without it the hooks become dead weight and would"
|
|
err "otherwise abort Claude Code's Edit/Write/Bash tool calls. Install it:"
|
|
err " brew install jq (macOS)"
|
|
err " apt install jq (Debian/Ubuntu)"
|
|
exit 1
|
|
fi
|
|
# Soft-warn on pandoc — the tomd primitive works without it for CSV / code /
|
|
# JSON / images, but fails on .docx / .pptx / .html. Opt-in use, so not
|
|
# promoted to a hard-fail.
|
|
if ! command -v pandoc >/dev/null 2>&1; then
|
|
warn "pandoc not found — tomd primitive will fail on .docx/.pptx. Install: brew install pandoc"
|
|
fi
|
|
|
|
# --- create target dirs -----------------------------------------------------
|
|
say "creating directories"
|
|
mkdir -p \
|
|
"$AGENTS_DIR/_blocks" \
|
|
"$AGENTS_DIR/_manifests" \
|
|
"$AGENTS_DIR/_primitives" \
|
|
"$AGENTS_DIR/_templates" \
|
|
"$AGENTS_DIR/_assembler/src" \
|
|
"$AGENTS_DIR/_generated" \
|
|
"$HOOKS_DIR" \
|
|
"$SKILLS_DIR/new-agent" \
|
|
"$HOME_DIR/.claude/memory"
|
|
|
|
# --- scaffold MEMORY.md placeholder (user-respecting) ----------------------
|
|
# _blocks/memory-protocol.md references ~/.claude/memory/MEMORY.md; without
|
|
# this file the first agent following the protocol fails on read.
|
|
MEMORY_INDEX="$HOME_DIR/.claude/memory/MEMORY.md"
|
|
if [[ ! -f "$MEMORY_INDEX" ]]; then
|
|
cat > "$MEMORY_INDEX" <<'EOF'
|
|
# Auto Memory — Index
|
|
|
|
> File-based memory index. Add entries as you save memory files under this directory.
|
|
> See `_blocks/memory-protocol.md` for format.
|
|
EOF
|
|
say "scaffolded $MEMORY_INDEX"
|
|
fi
|
|
|
|
# --- copy blocks (overwrite ours; blocks are SSoT from kit) ----------------
|
|
say "copying shared blocks -> $AGENTS_DIR/_blocks/"
|
|
backup_dir "$AGENTS_DIR/_blocks"
|
|
cp -f "$KIT_DIR/_blocks/"*.md "$AGENTS_DIR/_blocks/"
|
|
|
|
# --- copy primitives (overwrite; primitives are SSoT from kit) -------------
|
|
if [[ -d "$KIT_DIR/_primitives" ]]; then
|
|
say "copying primitives -> $AGENTS_DIR/_primitives/"
|
|
backup_dir "$AGENTS_DIR/_primitives"
|
|
cp -f "$KIT_DIR/_primitives/"*.sh "$AGENTS_DIR/_primitives/" 2>/dev/null || true
|
|
cp -f "$KIT_DIR/_primitives/README.md" "$AGENTS_DIR/_primitives/" 2>/dev/null || true
|
|
chmod +x "$AGENTS_DIR/_primitives/"*.sh 2>/dev/null || true
|
|
fi
|
|
|
|
# --- copy bridges (overwrite; templates are SSoT from kit) -----------------
|
|
if [[ -d "$KIT_DIR/_bridges" ]]; then
|
|
say "copying bridge templates -> $AGENTS_DIR/_bridges/"
|
|
mkdir -p "$AGENTS_DIR/_bridges"
|
|
backup_dir "$AGENTS_DIR/_bridges"
|
|
cp -f "$KIT_DIR/_bridges/"*.tmpl "$AGENTS_DIR/_bridges/"
|
|
cp -f "$KIT_DIR/_bridges/README.md" "$AGENTS_DIR/_bridges/"
|
|
cp -f "$KIT_DIR/_bridges/emit.sh" "$AGENTS_DIR/_bridges/emit.sh"
|
|
chmod +x "$AGENTS_DIR/_bridges/emit.sh"
|
|
fi
|
|
|
|
# --- copy generic manifests, DO NOT overwrite user's existing manifests -----
|
|
say "copying generic manifests -> $AGENTS_DIR/_manifests/ (skip if exists)"
|
|
copied=0; skipped=0
|
|
for f in "$KIT_DIR/_manifests/"*.toml; do
|
|
name="$(basename "$f")"
|
|
if [[ -f "$AGENTS_DIR/_manifests/$name" ]]; then
|
|
skipped=$((skipped+1))
|
|
else
|
|
cp "$f" "$AGENTS_DIR/_manifests/$name"
|
|
copied=$((copied+1))
|
|
fi
|
|
done
|
|
say " copied $copied, skipped $skipped (already present)"
|
|
|
|
# --- copy template ---------------------------------------------------------
|
|
# bash-3.2-portable glob detection: iterate, break on first hit.
|
|
has_templates=0
|
|
for t in "$KIT_DIR/_templates/"*.template; do
|
|
[ -f "$t" ] && { has_templates=1; break; }
|
|
done
|
|
if [ "$has_templates" = "1" ]; then
|
|
say "copying specialist template"
|
|
backup_dir "$AGENTS_DIR/_templates"
|
|
cp -f "$KIT_DIR/_templates/"*.template "$AGENTS_DIR/_templates/"
|
|
fi
|
|
|
|
# --- copy assembler source (always refresh) --------------------------------
|
|
say "copying assembler source"
|
|
backup_dir "$AGENTS_DIR/_assembler"
|
|
cp -f "$KIT_DIR/_assembler/Cargo.toml" "$AGENTS_DIR/_assembler/"
|
|
cp -f "$KIT_DIR/_assembler/src/"*.rs "$AGENTS_DIR/_assembler/src/"
|
|
if [[ -f "$KIT_DIR/_assembler/.gitignore" ]]; then
|
|
cp -f "$KIT_DIR/_assembler/.gitignore" "$AGENTS_DIR/_assembler/"
|
|
fi
|
|
|
|
# --- copy hooks (refresh; hooks are logic, not config) ---------------------
|
|
# $HOOKS_DIR is shared with other kits — back up each KeiSeiKit-owned hook
|
|
# individually rather than the whole directory, so foreign hooks are not
|
|
# dragged into .bak-TIMESTAMP snapshots on every re-run.
|
|
say "copying hooks -> $HOOKS_DIR/"
|
|
for h in assemble-agents.sh assemble-validate.sh no-hand-edit-agents.sh tomd-preread.sh; do
|
|
[ -f "$KIT_DIR/hooks/$h" ] || continue
|
|
backup_file "$HOOKS_DIR/$h"
|
|
cp -f "$KIT_DIR/hooks/$h" "$HOOKS_DIR/$h"
|
|
chmod +x "$HOOKS_DIR/$h"
|
|
done
|
|
|
|
# --- copy skills -----------------------------------------------------------
|
|
if [[ -d "$KIT_DIR/skills" ]]; then
|
|
say "copying skills"
|
|
backup_dir "$SKILLS_DIR"
|
|
for skill_dir in "$KIT_DIR/skills/"*/; do
|
|
[ -d "$skill_dir" ] || continue
|
|
skill_name="$(basename "$skill_dir")"
|
|
mkdir -p "$SKILLS_DIR/$skill_name"
|
|
cp -rf "$skill_dir"* "$SKILLS_DIR/$skill_name/" 2>/dev/null || true
|
|
say " -> $skill_name"
|
|
done
|
|
fi
|
|
|
|
# --- build assembler -------------------------------------------------------
|
|
# Prefer offline build (fresh-clone on a no-network machine should still work
|
|
# if the registry cache is warm). Fall back to online fetch on failure.
|
|
say "building Rust assembler (cargo build --release, offline first)"
|
|
if ! ( cd "$AGENTS_DIR/_assembler" && cargo build --release --offline ) 2>/tmp/keiseikit-cargo-offline.log; then
|
|
say "offline build failed — fetching deps from crates.io"
|
|
( cd "$AGENTS_DIR/_assembler" && cargo build --release )
|
|
fi
|
|
if [[ ! -x "$AGENTS_DIR/_assembler/target/release/assemble" ]]; then
|
|
err "build succeeded but binary not found at $AGENTS_DIR/_assembler/target/release/assemble"
|
|
exit 2
|
|
fi
|
|
|
|
# --- generate .md agents in-place ------------------------------------------
|
|
say "generating agent .md files (--in-place)"
|
|
AGENT_ROOT="$AGENTS_DIR" "$AGENTS_DIR/_assembler/target/release/assemble" --in-place
|
|
|
|
# --- activate hooks (flag, or interactive prompt on TTY) -------------------
|
|
SETTINGS_FILE="$HOME_DIR/.claude/settings.json"
|
|
DID_ACTIVATE=0
|
|
if [ "$ACTIVATE_HOOKS" = "1" ]; then
|
|
say "activating hooks (--activate-hooks)"
|
|
activate_hooks && DID_ACTIVATE=1
|
|
elif [ ! -f "$SETTINGS_FILE" ]; then
|
|
# No existing settings — merge is trivial, do it unconditionally.
|
|
say "no existing settings.json; installing snippet"
|
|
activate_hooks && DID_ACTIVATE=1
|
|
elif [ -t 0 ] && [ -t 1 ]; then
|
|
if [ "$COLOR" = "1" ]; then
|
|
printf '\033[1;36m[install]\033[0m activate hooks now? [y/N] '
|
|
else
|
|
printf '[install] activate hooks now? [y/N] '
|
|
fi
|
|
read -r reply
|
|
case "$reply" in
|
|
y|Y|yes|YES) activate_hooks && DID_ACTIVATE=1 ;;
|
|
*) say "skipping hook activation" ;;
|
|
esac
|
|
fi
|
|
|
|
# --- optional: render cross-tool bridges into $PWD -------------------------
|
|
# If a prior step ERR-trapped into rollback(), we MUST NOT keep writing into
|
|
# $PWD — the install is now aborted, and bridges should not land as
|
|
# collateral on a failed run. rollback() sets ROLLED_BACK=1 before returning.
|
|
if [ "${ROLLED_BACK:-0}" = "1" ]; then
|
|
exit 2
|
|
fi
|
|
if [[ "$WITH_BRIDGES" == "1" ]]; then
|
|
if [[ -f "./install.sh" && -d "./_bridges" ]]; then
|
|
warn "not generating bridges — you are in the KeiSeiKit repo, not a project directory"
|
|
else
|
|
say "rendering cross-tool bridges into $PWD"
|
|
"$KIT_DIR/_bridges/emit.sh" "$PWD"
|
|
fi
|
|
fi
|
|
|
|
# --- done -----------------------------------------------------------------
|
|
echo
|
|
say "install complete"
|
|
echo
|
|
if [ "$DID_ACTIVATE" = "1" ]; then
|
|
cat <<EOF
|
|
==========================================================================
|
|
Hooks activated. Settings merged into $SETTINGS_FILE
|
|
==========================================================================
|
|
|
|
To verify install:
|
|
ls $AGENTS_DIR/*.md # should show 12 generated agents
|
|
$AGENTS_DIR/_assembler/target/release/assemble --validate
|
|
|
|
To create a new project-specialist agent:
|
|
/new-agent
|
|
|
|
==========================================================================
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
==========================================================================
|
|
NEXT STEP: merge settings-snippet.json into ~/.claude/settings.json
|
|
==========================================================================
|
|
|
|
KeiSeiKit ships 4 hooks (assemble-agents, assemble-validate, no-hand-edit, tomd-preread).
|
|
To activate them, merge entries from:
|
|
$KIT_DIR/settings-snippet.json
|
|
into your:
|
|
$SETTINGS_FILE
|
|
|
|
Or re-run with automatic activation:
|
|
./install.sh --activate-hooks
|
|
|
|
To verify install:
|
|
ls $AGENTS_DIR/*.md # should show 12 generated agents
|
|
$AGENTS_DIR/_assembler/target/release/assemble --validate
|
|
|
|
To create a new project-specialist agent:
|
|
/new-agent
|
|
|
|
==========================================================================
|
|
EOF
|
|
fi
|