From d75d7829022b7ad733077069c54df0290c43f869 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 23:00:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(primitives):=20MANIFEST.toml=20?= =?UTF-8?q?=E2=80=94=20SSoT=20for=2021=20primitives=20+=206=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _primitives/MANIFEST.toml | 154 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 _primitives/MANIFEST.toml diff --git a/_primitives/MANIFEST.toml b/_primitives/MANIFEST.toml new file mode 100644 index 0000000..cd1e098 --- /dev/null +++ b/_primitives/MANIFEST.toml @@ -0,0 +1,154 @@ +# KeiSeiKit Primitives Manifest +# Declarative SSoT for install.sh profile resolution. +# +# Profiles compose primitive sets; install.sh --profile= resolves the +# member list, copies/builds only those, and records the result in +# ~/.claude/agents/_primitives/.installed. +# +# Individual primitives can be added/removed on top of any profile via +# --add=[,] / --remove=. +# +# Schema (per primitive): +# kind = "shell" | "rust" +# file = ".sh" (shell only — lives at _primitives/) +# crate = "" (rust only — lives at _primitives/_rust/) +# deps = ["", ...] # runtime/host deps, human-readable +# desc = "" + +[profile] +minimal = [] +core = ["tomd"] +frontend = ["mock-render", "visual-diff", "tokens-sync", "design-scrape", "live-preview", "figma-tokens", "frontend-inspect", "screenshot-decode"] +ops = ["kei-ledger", "ssh-check", "firewall-diff", "provision-hetzner", "provision-vultr", "harden-base", "metrics-scrape", "log-ship"] +dev = ["kei-migrate", "kei-changelog", "kei-ci-lint", "kei-docs-scaffold"] +full = ["tomd", "kei-ledger", "kei-migrate", "kei-changelog", "ssh-check", "firewall-diff", "mock-render", "visual-diff", "tokens-sync", "design-scrape", "live-preview", "figma-tokens", "frontend-inspect", "screenshot-decode", "provision-hetzner", "provision-vultr", "harden-base", "metrics-scrape", "log-ship", "kei-ci-lint", "kei-docs-scaffold"] + +# --- shell primitives (13) ------------------------------------------------- + +[primitive.tomd] +kind = "shell" +file = "tomd.sh" +deps = ["jq", "pandoc (optional — needed for .docx/.pptx/.html)"] +desc = "Universal non-native format → markdown (PDF, DOCX, XLSX, PPTX, CSV, images, code)" + +[primitive.design-scrape] +kind = "shell" +file = "design-scrape.sh" +deps = ["jq", "npx (Node)", "playwright (`npx playwright install chromium`)"] +desc = "Live URL → design tokens + screenshots JSON via Playwright" + +[primitive.live-preview] +kind = "shell" +file = "live-preview.sh" +deps = ["npm"] +desc = "start/stop/status wrapper for a project's dev server (.keisei/dev-server.pid)" + +[primitive.figma-tokens] +kind = "shell" +file = "figma-tokens.sh" +deps = ["curl", "jq", "FIGMA_TOKEN env var"] +desc = "Figma API → design tokens JSON (consumed by tokens-sync)" + +[primitive.frontend-inspect] +kind = "shell" +file = "frontend-inspect.sh" +deps = ["jq"] +desc = "Scan project dir → report framework, styling, UI count, lockfile" + +[primitive.screenshot-decode] +kind = "shell" +file = "screenshot-decode.sh" +deps = ["curl", "jq", "base64", "ANTHROPIC_API_KEY env var"] +desc = "Screenshot → structured design description via Claude vision API" + +[primitive.harden-base] +kind = "shell" +file = "harden-base.sh" +deps = ["bash", "apt (runs on target Debian/Ubuntu VPS)"] +desc = "Idempotent Debian/Ubuntu baseline hardening (fail2ban, ufw, unattended-upgrades)" + +[primitive.provision-hetzner] +kind = "shell" +file = "provision-hetzner.sh" +deps = ["hcloud CLI", "HCLOUD_TOKEN env var"] +desc = "Hetzner Cloud server provisioner — create/status/destroy/list" + +[primitive.provision-vultr] +kind = "shell" +file = "provision-vultr.sh" +deps = ["vultr-cli v3", "VULTR_API_KEY env var"] +desc = "Vultr VPS provisioner — create/status/destroy/list" + +[primitive.metrics-scrape] +kind = "shell" +file = "metrics-scrape.sh" +deps = ["curl", "awk", "jq (optional — needed for --format json)"] +desc = "Prometheus /metrics scrape + normalize + diff against baseline" + +[primitive.log-ship] +kind = "shell" +file = "log-ship.sh" +deps = ["curl", "awk", "jq (optional — needed for --validate)"] +desc = "Tail structured logs → forward to Loki / Datadog / HTTP with rate limits" + +[primitive.kei-ci-lint] +kind = "shell" +file = "kei-ci-lint.sh" +deps = ["yq v4+ (mikefarah/yq Go impl)"] +desc = "Validate GitHub/Forgejo Actions workflow YAML (pinning, OIDC, cache, permissions)" + +[primitive.kei-docs-scaffold] +kind = "shell" +file = "kei-docs-scaffold.sh" +deps = [] +desc = "Detect project type → generate missing CLAUDE.md/DECISIONS.md/RUNBOOK.md/README.md" + +# --- rust primitives (8) --------------------------------------------------- + +[primitive.kei-ledger] +kind = "rust" +crate = "kei-ledger" +deps = ["rusqlite bundled (no system sqlite required)"] +desc = "Agent-fork lifecycle SQLite ledger (fork/done/fail) — SSoT for RULE 0.12" + +[primitive.kei-migrate] +kind = "rust" +crate = "kei-migrate" +deps = ["sqlx (postgres/sqlite/mysql)", "tokio", "DATABASE_URL env var"] +desc = "Universal SQL migration runner — Postgres/SQLite/MySQL autodetect" + +[primitive.kei-changelog] +kind = "rust" +crate = "kei-changelog" +deps = ["git2 (vendored libgit2)"] +desc = "Git-cliff-style CHANGELOG.md generator from Conventional Commits" + +[primitive.ssh-check] +kind = "rust" +crate = "ssh-check" +deps = [] +desc = "sshd_config linter — flags weak ciphers, PermitRootLogin yes, password auth" + +[primitive.firewall-diff] +kind = "rust" +crate = "firewall-diff" +deps = ["ufw (target-side; binary parses `ufw status` output)"] +desc = "ufw intended-vs-running diff — catches drift between declared and live rules" + +[primitive.mock-render] +kind = "rust" +crate = "mock-render" +deps = ["Chrome/Chromium (runtime)", "playwright (optional for parity driver)"] +desc = "Playwright wrapper with SHA-locked PNG (WYSIWYD: What You See Is What You Deploy)" + +[primitive.visual-diff] +kind = "rust" +crate = "visual-diff" +deps = [] +desc = "Pixel diff with tolerance — used in /site-create screenshot-regression loop" + +[primitive.tokens-sync] +kind = "rust" +crate = "tokens-sync" +deps = [] +desc = "Design tokens JSON → Tailwind config extend + CSS variables under :root" From 67d6f5a15a7b4469ab6a0f2da63d515bc793fe58 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 23:00:32 +0800 Subject: [PATCH 2/3] feat(install): modular profiles + --add/--remove/--list incremental install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default changed: ./install.sh now installs minimal (no primitives) — ~5s, ~2 MB. Old full behavior available via --profile=full. Profiles: minimal / core / frontend / ops / dev / full. Incremental: --add=name[,name] / --remove=name / --list. Rust workspace scoped per install — only selected crates built. --- install.sh | 643 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 501 insertions(+), 142 deletions(-) diff --git a/install.sh b/install.sh index 252fac2..3264eff 100755 --- a/install.sh +++ b/install.sh @@ -3,12 +3,13 @@ # 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. +# ./install.sh # profile=minimal (agents + hooks + skills + bridges, NO primitives) +# ./install.sh --profile= # minimal|core|frontend|ops|dev|full +# ./install.sh --add=[,] # install one or more primitives on top of current state +# ./install.sh --remove= # remove a single primitive +# ./install.sh --list # list installed primitives (name | kind | desc | path) +# ./install.sh --with-bridges # also render cross-tool bridges into $PWD +# ./install.sh --activate-hooks # jq-merge settings-snippet.json into ~/.claude/settings.json set -euo pipefail @@ -17,26 +18,58 @@ HOME_DIR="${HOME:?HOME not set}" AGENTS_DIR="$HOME_DIR/.claude/agents" HOOKS_DIR="$HOME_DIR/.claude/hooks" SKILLS_DIR="$HOME_DIR/.claude/skills" +MANIFEST="$KIT_DIR/_primitives/MANIFEST.toml" +INSTALLED_FILE="$AGENTS_DIR/_primitives/.installed" # --- flag parsing ---------------------------------------------------------- ACTIVATE_HOOKS=0 WITH_BRIDGES=0 +PROFILE="" +ADD_LIST="" +REMOVE_NAME="" +LIST_MODE=0 + for arg in "$@"; do case "$arg" in --activate-hooks) ACTIVATE_HOOKS=1 ;; --with-bridges) WITH_BRIDGES=1 ;; + --profile=*) PROFILE="${arg#--profile=}" ;; + --add=*) ADD_LIST="${arg#--add=}" ;; + --remove=*) REMOVE_NAME="${arg#--remove=}" ;; + --list) LIST_MODE=1 ;; --help|-h) cat < set installed-primitive set to one of: + minimal (no primitives) + core (tomd) + frontend (8 site tools: mock-render / visual-diff / ...) + ops (8 infra tools: kei-ledger / ssh-check / ...) + dev (4 dev tools: kei-migrate / kei-changelog / ...) + full (all 21 primitives) + + --add=[,,...] add one or more primitives on top of current install. + Name must match [primitive.] in _primitives/MANIFEST.toml. + + --remove= remove a single primitive (shell file or rust crate dir + + scoped workspace Cargo.toml regenerated + rebuilt). + + --list list installed primitives from .installed state file. + + --with-bridges render the 11 cross-tool bridge files into \$PWD + (Cursor / Copilot / Codex / Windsurf / Junie / Continue / + Aider / Replit / Antigravity / Warp / Zed). + Skipped if invoked inside the KeiSeiKit repo itself. + + --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. + + --help, -h this help. EOF exit 0 ;; @@ -80,8 +113,6 @@ rollback() { 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 @@ -93,15 +124,9 @@ rollback() { } 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 @@ -111,9 +136,6 @@ backup_dir() { 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 .bak-TIMESTAMP. backup_file() { local target="$1" [ -f "$target" ] || return 0 @@ -123,35 +145,336 @@ backup_file() { 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. +# --- MANIFEST.toml parsing -------------------------------------------------- +# Tiny awk-based TOML reader. We only need two shapes: +# 1. profile. = ["a", "b", ...] +# 2. [primitive.] ... kind = "..." file = "..." crate = "..." deps = [...] desc = "..." +# If a real TOML parser (python -c "import tomllib" or python -c "import toml") is +# available, prefer it for robustness. Otherwise fall back to awk. + +have_python_toml() { + if command -v python3 >/dev/null 2>&1; then + python3 -c 'import tomllib' >/dev/null 2>&1 && return 0 + python3 -c 'import toml' >/dev/null 2>&1 && return 0 + fi + return 1 +} + +# Echo space-separated primitive names for a given profile. +# Usage: profile_members +profile_members() { + local profile="$1" + [ -f "$MANIFEST" ] || { err "MANIFEST.toml not found at $MANIFEST"; return 1; } + if have_python_toml; then + python3 - "$MANIFEST" "$profile" <<'PY' 2>/dev/null || return 1 +import sys +try: + import tomllib + mode = "rb" +except ImportError: + import toml as tomllib + mode = "r" +path, prof = sys.argv[1], sys.argv[2] +with open(path, mode) as f: + data = tomllib.load(f) if mode == "rb" else tomllib.load(f) +members = data.get("profile", {}).get(prof) +if members is None: + sys.exit(2) +print(" ".join(members)) +PY + else + # awk fallback — only handles `profile. = [...]` on one line + awk -v prof="$profile" ' + /^\[profile\]/ { in_profile=1; next } + /^\[/ && !/^\[profile\]/ { in_profile=0 } + in_profile && $0 ~ "^[[:space:]]*" prof "[[:space:]]*=" { + # extract between [ and ] + line = $0 + sub(/^[^\[]*\[/, "", line) + sub(/\].*$/, "", line) + gsub(/"/, "", line) + gsub(/,/, " ", line) + print line + exit + } + ' "$MANIFEST" + fi +} + +# Echo a field of a primitive. Usage: primitive_field +# field ∈ { kind, file, crate, desc } +primitive_field() { + local name="$1" field="$2" + [ -f "$MANIFEST" ] || return 1 + if have_python_toml; then + python3 - "$MANIFEST" "$name" "$field" <<'PY' 2>/dev/null +import sys +try: + import tomllib + mode = "rb" +except ImportError: + import toml as tomllib + mode = "r" +path, name, field = sys.argv[1], sys.argv[2], sys.argv[3] +with open(path, mode) as f: + data = tomllib.load(f) if mode == "rb" else tomllib.load(f) +p = data.get("primitive", {}).get(name) +if p is None: + sys.exit(2) +v = p.get(field, "") +if isinstance(v, list): + print("; ".join(v)) +else: + print(v) +PY + else + awk -v pname="$name" -v fname="$field" ' + $0 ~ "^\\[primitive\\." pname "\\]" { in_p=1; next } + /^\[/ && in_p { in_p=0 } + in_p && $0 ~ "^[[:space:]]*" fname "[[:space:]]*=" { + line = $0 + sub(/^[^=]*=[[:space:]]*/, "", line) + # strip surrounding quotes + gsub(/^"/, "", line) + gsub(/"$/, "", line) + print line + exit + } + ' "$MANIFEST" + fi +} + +# Echo all primitive names defined in MANIFEST. +all_primitive_names() { + [ -f "$MANIFEST" ] || return 1 + awk ' + /^\[primitive\./ { + name = $0 + sub(/^\[primitive\./, "", name) + sub(/\]$/, "", name) + print name + } + ' "$MANIFEST" +} + +# --- .installed state helpers --------------------------------------------- +read_installed() { + [ -f "$INSTALLED_FILE" ] && cat "$INSTALLED_FILE" || true +} + +write_installed() { + # stdin = newline-separated names; writes sorted-unique to INSTALLED_FILE. + mkdir -p "$(dirname "$INSTALLED_FILE")" + sort -u > "$INSTALLED_FILE" +} + +# --- per-primitive install/remove ------------------------------------------ +copy_shell_primitive() { + local name="$1" + local file + file="$(primitive_field "$name" file)" + [ -n "$file" ] || { err "no 'file' for shell primitive $name"; return 1; } + local src="$KIT_DIR/_primitives/$file" + local dst="$AGENTS_DIR/_primitives/$file" + [ -f "$src" ] || { err "source missing: $src"; return 1; } + mkdir -p "$AGENTS_DIR/_primitives" + cp -f "$src" "$dst" + chmod +x "$dst" + say " + shell: $name ($file)" +} + +remove_shell_primitive() { + local name="$1" + local file + file="$(primitive_field "$name" file)" + [ -n "$file" ] || return 0 + rm -f "$AGENTS_DIR/_primitives/$file" + say " - shell: $name ($file)" +} + +copy_rust_primitive() { + local name="$1" + local crate + crate="$(primitive_field "$name" crate)" + [ -n "$crate" ] || { err "no 'crate' for rust primitive $name"; return 1; } + local src="$KIT_DIR/_primitives/_rust/$crate" + [ -d "$src" ] || { err "source missing: $src"; return 1; } + local dst_root="$AGENTS_DIR/_primitives/_rust" + local dst="$dst_root/$crate" + mkdir -p "$dst/src" + cp -f "$src/Cargo.toml" "$dst/Cargo.toml" + [ -d "$src/src" ] && cp -rf "$src/src/"* "$dst/src/" 2>/dev/null || true + if [ -d "$src/tests" ]; then + mkdir -p "$dst/tests" + cp -rf "$src/tests/"* "$dst/tests/" 2>/dev/null || true + fi + say " + rust: $name (crate $crate)" +} + +remove_rust_primitive() { + local name="$1" + local crate + crate="$(primitive_field "$name" crate)" + [ -n "$crate" ] || return 0 + rm -rf "$AGENTS_DIR/_primitives/_rust/$crate" + say " - rust: $name (crate $crate)" +} + +# Echo the list of rust crates currently installed (by scanning .installed + +# cross-checking MANIFEST kind = "rust" + dir presence). +installed_rust_crates() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + local name kind crate + while IFS= read -r name; do + [ -z "$name" ] && continue + kind="$(primitive_field "$name" kind)" + [ "$kind" = "rust" ] || continue + crate="$(primitive_field "$name" crate)" + [ -n "$crate" ] && [ -d "$dst_root/$crate" ] && echo "$crate" + done <<< "$(read_installed)" +} + +# Write a scoped Cargo.toml listing only the given members (stdin: one per line). +# The workspace.package / workspace.dependencies / profile.release blocks are +# copied verbatim from the kit source so shared deps stay in sync. +write_rust_workspace_manifest() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + local src_wkspc="$KIT_DIR/_primitives/_rust/Cargo.toml" + local tmp="$dst_root/Cargo.toml.tmp" + { + echo '[workspace]' + echo 'resolver = "2"' + echo 'members = [' + local m + while IFS= read -r m; do + [ -n "$m" ] && echo " \"$m\"," + done + echo ']' + awk '/^\[workspace\.package\]/,0' "$src_wkspc" + } > "$tmp" + mv "$tmp" "$dst_root/Cargo.toml" + if [ -f "$KIT_DIR/_primitives/_rust/Cargo.lock" ]; then + cp -f "$KIT_DIR/_primitives/_rust/Cargo.lock" "$dst_root/Cargo.lock" + fi +} + +# Build the scoped rust workspace. Offline-first, online fallback. +build_rust_workspace() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + if ! ( cd "$dst_root" && cargo build --workspace --release --offline ) 2>/tmp/keiseikit-primitives-offline.log; then + say " offline build failed — fetching deps from crates.io" + if ! ( cd "$dst_root" && cargo build --workspace --release ); then + warn "Rust primitive workspace build failed; shell primitives still work" + warn " see log: /tmp/keiseikit-primitives-offline.log" + return 0 + fi + fi +} + +# Orchestrator: installed rust crates -> scoped manifest -> cargo build -> report. +# No-op when no rust primitives are installed. +regenerate_rust_workspace() { + local dst_root="$AGENTS_DIR/_primitives/_rust" + mkdir -p "$dst_root" + local members_nl + members_nl="$(installed_rust_crates)" + if [ -z "$members_nl" ]; then + rm -f "$dst_root/Cargo.toml" "$dst_root/Cargo.lock" + return 0 + fi + local n + n="$(printf '%s\n' "$members_nl" | grep -c .)" + printf '%s\n' "$members_nl" | write_rust_workspace_manifest + say "building Rust primitives ($n crate(s))" + build_rust_workspace + local built=0 m + while IFS= read -r m; do + [ -n "$m" ] && [ -x "$dst_root/target/release/$m" ] && built=$((built+1)) + done <<< "$members_nl" + say " $built / $n Rust primitive binaries available" +} + +# Install primitives from a name list (newline-separated on stdin). +# Updates .installed as a superset. +install_primitives() { + local names existing combined new_file + names="$(cat)" + existing="$(read_installed)" + combined="$(printf '%s\n%s\n' "$existing" "$names" | grep -v '^$' || true)" + local kind + local any_rust=0 + while IFS= read -r p; do + [ -z "$p" ] && continue + kind="$(primitive_field "$p" kind)" + case "$kind" in + shell) copy_shell_primitive "$p" ;; + rust) copy_rust_primitive "$p"; any_rust=1 ;; + *) warn "unknown primitive: $p (skipping)"; continue ;; + esac + done <<< "$names" + printf '%s\n' "$combined" | write_installed + if [ "$any_rust" = "1" ]; then + regenerate_rust_workspace + fi +} + +# Remove a single primitive by name. +remove_primitive() { + local name="$1" kind + kind="$(primitive_field "$name" kind)" + case "$kind" in + shell) remove_shell_primitive "$name" ;; + rust) remove_rust_primitive "$name" ;; + *) err "unknown primitive: $name"; return 1 ;; + esac + local existing + existing="$(read_installed)" + printf '%s\n' "$existing" | grep -vFx "$name" | grep -v '^$' | write_installed || true + # Rust removal => rewrite scoped workspace + if [ "$kind" = "rust" ]; then + regenerate_rust_workspace + fi +} + +# --- --list implementation -------------------------------------------------- +cmd_list() { + echo + printf '%-22s %-6s %-10s %s\n' "NAME" "KIND" "STATUS" "DESCRIPTION" + printf '%-22s %-6s %-10s %s\n' "----" "----" "------" "-----------" + local installed names kind desc status + installed="$(read_installed)" + while IFS= read -r name; do + [ -z "$name" ] && continue + kind="$(primitive_field "$name" kind)" + desc="$(primitive_field "$name" desc)" + if printf '%s\n' "$installed" | grep -qFx "$name"; then + status="INSTALLED" + else + status="-" + fi + printf '%-22s %-6s %-10s %s\n' "$name" "$kind" "$status" "$desc" + done < <(all_primitive_names) + echo + local count + count="$(printf '%s\n' "$installed" | grep -c . || true)" + printf '%s primitives installed (state: %s)\n' "${count:-0}" "$INSTALLED_FILE" + echo +} + +# --- hook activation (unchanged jq-merge) ---------------------------------- 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 @@ -167,7 +490,6 @@ activate_hooks() { ) ) ' "$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)" @@ -178,13 +500,64 @@ activate_hooks() { fi } -# --- prerequisites ---------------------------------------------------------- +# --- --list short-circuit --------------------------------------------------- +if [ "$LIST_MODE" = "1" ]; then + [ -f "$MANIFEST" ] || { err "MANIFEST.toml missing: $MANIFEST"; exit 2; } + cmd_list + exit 0 +fi + +# --- incremental --add / --remove short-circuit --------------------------- +# If either flag is set, skip the full agent/hook/skills sync and just mutate +# the primitive set. Assumes a prior install already wrote _blocks etc. +if [ -n "$ADD_LIST" ] || [ -n "$REMOVE_NAME" ]; then + [ -f "$MANIFEST" ] || { err "MANIFEST.toml missing: $MANIFEST"; exit 2; } + mkdir -p "$AGENTS_DIR/_primitives" + + if [ -n "$REMOVE_NAME" ]; then + say "removing primitive: $REMOVE_NAME" + remove_primitive "$REMOVE_NAME" + fi + + if [ -n "$ADD_LIST" ]; then + # Resolve --add=x,y,z OR --add= (profile expands in-place) + tr ',' '\n' <<< "$ADD_LIST" | grep -v '^$' | while IFS= read -r token; do + # Is token a known profile? + local_members="$(profile_members "$token" 2>/dev/null || true)" + if [ -n "$local_members" ]; then + printf '%s\n' "$local_members" | tr ' ' '\n' + else + printf '%s\n' "$token" + fi + done | grep -v '^$' | sort -u | install_primitives + say "added: $ADD_LIST" + fi + + echo + say "incremental change complete" + cmd_list + exit 0 +fi + +# --- resolve profile ------------------------------------------------------ +# Default profile is minimal. +PROFILE="${PROFILE:-minimal}" +case "$PROFILE" in + minimal|core|frontend|ops|dev|full) ;; + *) + err "unknown profile: $PROFILE. Valid: minimal | core | frontend | ops | dev | full" + exit 1 + ;; +esac +say "profile: $PROFILE" + +# --- prerequisites --------------------------------------------------------- +# HARD: cargo, jq. SOFT: deps based on the primitives that will be installed. 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 @@ -197,24 +570,48 @@ if ! command -v jq >/dev/null 2>&1; then 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 + +# Profile-aware soft-warn: only check deps for primitives actually being installed. +# Build a unique set of substrings to check. +PROFILE_PRIMS="$(profile_members "$PROFILE" 2>/dev/null || true)" +needs_pandoc=0 +needs_playwright=0 +needs_sqlite=0 +needs_hcloud=0 +needs_vultr=0 +needs_yq=0 +for p in $PROFILE_PRIMS; do + case "$p" in + tomd) needs_pandoc=1 ;; + design-scrape|live-preview|mock-render) needs_playwright=1 ;; + kei-ledger|kei-migrate) needs_sqlite=1 ;; + provision-hetzner) needs_hcloud=1 ;; + provision-vultr) needs_vultr=1 ;; + kei-ci-lint) needs_yq=1 ;; + esac +done +if [ "$needs_pandoc" = "1" ] && ! command -v pandoc >/dev/null 2>&1; then warn "pandoc not found — tomd primitive will fail on .docx/.pptx. Install: brew install pandoc" fi -# Soft-warn on playwright — frontend primitives (design-scrape, live-preview, -# mock-render) need the Playwright browser driver. Not used by the core fleet. -if ! command -v playwright >/dev/null 2>&1 && ! command -v npx >/dev/null 2>&1; then - warn "playwright/npx not found — frontend primitives (design-scrape, live-preview, mock-render) will fail. Install: npm i -g playwright && playwright install chromium" +if [ "$needs_playwright" = "1" ] \ + && ! command -v playwright >/dev/null 2>&1 \ + && ! command -v npx >/dev/null 2>&1; then + warn "playwright/npx not found — frontend primitives need them. Install: npm i -g playwright && playwright install chromium" fi -# Soft-warn on sqlite3 CLI — kei-ledger / kei-migrate embed rusqlite, so the -# CLI is optional. Only surfaced so users can manually inspect the ledger DB. -if ! command -v sqlite3 >/dev/null 2>&1; then - warn "sqlite3 CLI not found — kei-ledger/kei-migrate work without it (rusqlite embedded). Install if you want manual DB inspection: brew install sqlite" +if [ "$needs_sqlite" = "1" ] && ! command -v sqlite3 >/dev/null 2>&1; then + warn "sqlite3 CLI not found — kei-ledger/kei-migrate work without it (rusqlite embedded). Install for manual DB inspection: brew install sqlite" +fi +if [ "$needs_hcloud" = "1" ] && ! command -v hcloud >/dev/null 2>&1; then + warn "hcloud CLI not found — provision-hetzner requires it. Install: brew install hcloud" +fi +if [ "$needs_vultr" = "1" ] && ! command -v vultr-cli >/dev/null 2>&1; then + warn "vultr-cli not found — provision-vultr requires it. Install: brew install vultr/vultr-cli/vultr-cli" +fi +if [ "$needs_yq" = "1" ] && ! command -v yq >/dev/null 2>&1; then + warn "yq not found — kei-ci-lint requires yq v4+ (mikefarah/yq). Install: brew install yq" fi -# --- create target dirs ----------------------------------------------------- +# --- create target dirs --------------------------------------------------- say "creating directories" mkdir -p \ "$AGENTS_DIR/_blocks" \ @@ -227,9 +624,7 @@ mkdir -p \ "$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. +# --- scaffold MEMORY.md placeholder -------------------------------------- MEMORY_INDEX="$HOME_DIR/.claude/memory/MEMORY.md" if [[ ! -f "$MEMORY_INDEX" ]]; then cat > "$MEMORY_INDEX" <<'EOF' @@ -241,49 +636,45 @@ EOF say "scaffolded $MEMORY_INDEX" fi -# --- copy blocks (overwrite ours; blocks are SSoT from kit) ---------------- +# --- 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) ------------- -# Shell primitives live at _primitives/*.sh, Rust primitives under -# _primitives/_rust/ (Cargo workspace). The Rust workspace is copied wholesale -# but the compile artefacts (target/) are excluded — we rebuild locally. -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 - if [[ -d "$KIT_DIR/_primitives/_rust" ]]; then - say " copying Rust primitive workspace (excluding target/)" - mkdir -p "$AGENTS_DIR/_primitives/_rust" - # Copy workspace manifest + each crate source, skip target/ - cp -f "$KIT_DIR/_primitives/_rust/Cargo.toml" "$AGENTS_DIR/_primitives/_rust/" - if [[ -f "$KIT_DIR/_primitives/_rust/Cargo.lock" ]]; then - cp -f "$KIT_DIR/_primitives/_rust/Cargo.lock" "$AGENTS_DIR/_primitives/_rust/" - fi - for crate_dir in "$KIT_DIR/_primitives/_rust/"*/; do - [ -d "$crate_dir" ] || continue - crate_name="$(basename "$crate_dir")" - [ "$crate_name" = "target" ] && continue - mkdir -p "$AGENTS_DIR/_primitives/_rust/$crate_name" - # Copy Cargo.toml + src/ + tests/ (if present) - cp -f "$crate_dir/Cargo.toml" "$AGENTS_DIR/_primitives/_rust/$crate_name/" 2>/dev/null || true - if [[ -d "$crate_dir/src" ]]; then - mkdir -p "$AGENTS_DIR/_primitives/_rust/$crate_name/src" - cp -rf "$crate_dir/src/"* "$AGENTS_DIR/_primitives/_rust/$crate_name/src/" 2>/dev/null || true - fi - if [[ -d "$crate_dir/tests" ]]; then - mkdir -p "$AGENTS_DIR/_primitives/_rust/$crate_name/tests" - cp -rf "$crate_dir/tests/"* "$AGENTS_DIR/_primitives/_rust/$crate_name/tests/" 2>/dev/null || true - fi - done - fi +# --- copy primitives (profile-driven) ------------------------------------- +# Always copy MANIFEST.toml + README.md so subsequent --list works. +mkdir -p "$AGENTS_DIR/_primitives" +cp -f "$KIT_DIR/_primitives/MANIFEST.toml" "$AGENTS_DIR/_primitives/MANIFEST.toml" 2>/dev/null || true +cp -f "$KIT_DIR/_primitives/README.md" "$AGENTS_DIR/_primitives/" 2>/dev/null || true + +say "resolving primitives for profile=$PROFILE" +# Clean slate: drop every shell .sh + rust crate dir from the installed set +# FAST (no per-rust rebuild). A single regenerate_rust_workspace at the end +# of the install phase handles the final state. +existing_installed="$(read_installed)" +if [ -n "${existing_installed:-}" ]; then + while IFS= read -r n; do + [ -z "$n" ] && continue + k="$(primitive_field "$n" kind 2>/dev/null || true)" + case "$k" in + shell) f="$(primitive_field "$n" file)"; [ -n "$f" ] && rm -f "$AGENTS_DIR/_primitives/$f" ;; + rust) c="$(primitive_field "$n" crate)"; [ -n "$c" ] && rm -rf "$AGENTS_DIR/_primitives/_rust/$c" ;; + esac + done <<< "$existing_installed" + : > "$INSTALLED_FILE" fi -# --- copy bridges (overwrite; templates are SSoT from kit) ----------------- +# Install fresh per profile. install_primitives rebuilds rust workspace once +# at the end if any rust crate was added; for minimal we still need to scrub +# any stale workspace Cargo.toml. +if [ -n "${PROFILE_PRIMS:-}" ]; then + printf '%s\n' "$PROFILE_PRIMS" | tr ' ' '\n' | grep -v '^$' | install_primitives +else + regenerate_rust_workspace + say " (no primitives — minimal profile)" +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" @@ -294,7 +685,7 @@ if [[ -d "$KIT_DIR/_bridges" ]]; then chmod +x "$AGENTS_DIR/_bridges/emit.sh" fi -# --- copy generic manifests, DO NOT overwrite user's existing manifests ----- +# --- 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 @@ -308,8 +699,7 @@ for f in "$KIT_DIR/_manifests/"*.toml; do done say " copied $copied, skipped $skipped (already present)" -# --- copy template --------------------------------------------------------- -# bash-3.2-portable glob detection: iterate, break on first hit. +# --- copy template -------------------------------------------------------- has_templates=0 for t in "$KIT_DIR/_templates/"*.template; do [ -f "$t" ] && { has_templates=1; break; } @@ -320,7 +710,7 @@ if [ "$has_templates" = "1" ]; then cp -f "$KIT_DIR/_templates/"*.template "$AGENTS_DIR/_templates/" fi -# --- copy assembler source (always refresh) -------------------------------- +# --- copy assembler source (always refresh) ------------------------------- say "copying assembler source" backup_dir "$AGENTS_DIR/_assembler" cp -f "$KIT_DIR/_assembler/Cargo.toml" "$AGENTS_DIR/_assembler/" @@ -329,12 +719,7 @@ 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. -# Discover hooks dynamically from $KIT_DIR/hooks/*.sh so new hooks land -# automatically without editing install.sh. +# --- copy hooks (refresh; hooks are logic, not config) -------------------- say "copying hooks -> $HOOKS_DIR/" hook_count=0 for hook_src in "$KIT_DIR/hooks/"*.sh; do @@ -347,7 +732,7 @@ for hook_src in "$KIT_DIR/hooks/"*.sh; do done say " installed $hook_count hook(s)" -# --- copy skills ----------------------------------------------------------- +# --- copy skills ---------------------------------------------------------- if [[ -d "$KIT_DIR/skills" ]]; then say "copying skills" backup_dir "$SKILLS_DIR" @@ -360,9 +745,7 @@ if [[ -d "$KIT_DIR/skills" ]]; then 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. +# --- build assembler ------------------------------------------------------ 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" @@ -373,40 +756,17 @@ if [[ ! -x "$AGENTS_DIR/_assembler/target/release/assemble" ]]; then exit 2 fi -# --- build Rust primitives workspace (8 crates) ---------------------------- -# Offline-first like the assembler. Failure here is non-fatal: the fleet and -# shell primitives still work without the 8 Rust binaries. -if [[ -d "$AGENTS_DIR/_primitives/_rust" && -f "$AGENTS_DIR/_primitives/_rust/Cargo.toml" ]]; then - say "building Rust primitive workspace (8 crates, cargo build --release)" - if ! ( cd "$AGENTS_DIR/_primitives/_rust" && cargo build --workspace --release --offline ) 2>/tmp/keiseikit-primitives-offline.log; then - say " offline build failed — fetching deps from crates.io" - if ! ( cd "$AGENTS_DIR/_primitives/_rust" && cargo build --workspace --release ); then - warn "Rust primitive workspace build failed; fleet still functional without binaries" - warn " see log: /tmp/keiseikit-primitives-offline.log" - fi - fi - # Report which binaries built successfully. - built=0 - for bin in kei-ledger kei-migrate kei-changelog ssh-check firewall-diff mock-render visual-diff tokens-sync; do - if [[ -x "$AGENTS_DIR/_primitives/_rust/target/release/$bin" ]]; then - built=$((built+1)) - fi - done - say " $built / 8 Rust primitive binaries available" -fi - -# --- generate .md agents in-place ------------------------------------------ +# --- 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) ------------------- +# --- 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 @@ -422,10 +782,7 @@ elif [ -t 0 ] && [ -t 1 ]; then 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. +# --- optional: render cross-tool bridges into $PWD ----------------------- if [ "${ROLLED_BACK:-0}" = "1" ]; then exit 2 fi @@ -438,9 +795,9 @@ if [[ "$WITH_BRIDGES" == "1" ]]; then fi fi -# --- done ----------------------------------------------------------------- +# --- done ---------------------------------------------------------------- echo -say "install complete" +say "install complete (profile=$PROFILE)" echo if [ "$DID_ACTIVATE" = "1" ]; then cat < Date: Tue, 21 Apr 2026 23:00:32 +0800 Subject: [PATCH 3/3] docs(readme): install profiles table + migration note for v0.9.0 --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d02ffd1..f0ad1e3 100644 --- a/README.md +++ b/README.md @@ -6,33 +6,73 @@ The kit is MIT-licensed and fully generic — install it on a fresh machine and ## Prerequisites -- **Rust** (stable toolchain) — the assembler + 8 primitive crates are a Cargo workspace +**Hard** (needed for every install, regardless of profile): + +- **Rust** (stable toolchain) — the assembler Cargo workspace is always built - **jq** — used by the shell hooks for JSON parsing (`brew install jq` / `apt install jq`) - **Claude Code** — the agents, hooks, and skills target Claude Code's agent / skill / hook surface -- **pandoc** (soft) — the `tomd` primitive falls back to pandoc for `.docx` / `.pptx` / `.html` -- **Node + Playwright** (soft) — required for frontend primitives (`design-scrape`, `live-preview`, `mock-render`); install with `npm i -g playwright && playwright install chromium` -- **sqlite3 CLI** (soft) — the `kei-ledger` / `kei-migrate` crates embed SQLite via `rusqlite`; the CLI is only needed if you want to inspect ledger DBs directly + +**Soft** (only needed if the chosen profile pulls the primitive in — see the profile table below): + +- **pandoc** — `tomd` uses it for `.docx` / `.pptx` / `.html` (needed for `core` / `full` profile) +- **Node + Playwright** — for the 3 browser-driven frontend primitives `design-scrape`, `live-preview`, `mock-render` (`frontend` / `full` profile); install with `npm i -g playwright && playwright install chromium` +- **sqlite3 CLI** — optional for manual DB inspection of `kei-ledger` / `kei-migrate` (their binaries embed SQLite via `rusqlite`; `ops` / `dev` profile) +- **hcloud / vultr-cli** — wrapped by `provision-hetzner` / `provision-vultr` (`ops` profile) +- **yq v4** (mikefarah/yq Go impl) — required by `kei-ci-lint` (`dev` profile) + +`install.sh` checks only the deps relevant to the selected profile and soft-warns once per missing tool. ## Install ```bash git clone KeiSeiKit cd KeiSeiKit -./install.sh +./install.sh # profile=minimal (default, no primitives) ``` `install.sh` is idempotent. It: 1. Creates `~/.claude/agents/{_blocks,_manifests,_primitives,_bridges,_templates,_assembler,_generated}`, `~/.claude/hooks`, `~/.claude/skills` -2. Copies all blocks, primitives (shell + Rust workspace), bridges (overwrites — these are SSoT from the kit) -3. Copies generic manifests (skips if you already have a manifest with that name) -4. Builds the Rust assembler (`cargo build --release` in `_assembler/`) -5. Builds the 8 primitive crates (`cargo build --release` in `_primitives/_rust/`) -6. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place` -7. Copies the five hooks and 34 skills +2. Copies all blocks + bridges (overwrites — these are SSoT from the kit) +3. Copies primitives ONLY for the selected profile (default: `minimal` = none). Tracks installed set in `~/.claude/agents/_primitives/.installed`. +4. Copies generic manifests (skips if you already have a manifest with that name) +5. Builds the Rust assembler (`cargo build --release` in `_assembler/`) +6. If any Rust primitive is in the selected profile: writes a scoped workspace `Cargo.toml` listing ONLY the installed crates, then `cargo build --release` +7. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place` +8. Copies the six hooks and 34 skills After install, the only remaining step is merging `settings-snippet.json` into your `~/.claude/settings.json` to activate the hooks. You can do this automatically with `./install.sh --activate-hooks` or answer `y` at the end-of-install TTY prompt. +## Install profiles + +By default `./install.sh` is **minimal** — agents + hooks + skills + bridges, no primitives. Fastest (~5s) and zero Rust compile for primitives. You opt into primitives via `--profile=` or one-at-a-time via `--add=`. + +| Profile | Primitives added | Install time | Disk (approx) | +|---|---|---|---| +| `minimal` (default) | none | ~5s | ~2 MB | +| `core` | `tomd` | ~5s | ~2 MB | +| `frontend` | 8 site tools: `mock-render`, `visual-diff`, `tokens-sync`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode` | ~60s | ~80 MB | +| `ops` | 8 infra tools: `kei-ledger`, `ssh-check`, `firewall-diff`, `provision-hetzner`, `provision-vultr`, `harden-base`, `metrics-scrape`, `log-ship` | ~90s | ~50 MB | +| `dev` | 4 dev tools: `kei-migrate`, `kei-changelog`, `kei-ci-lint`, `kei-docs-scaffold` | ~60s | ~40 MB | +| `full` | everything (21 primitives) | ~5 min | ~200 MB | + +Examples: + +```bash +./install.sh # minimal (no primitives) +./install.sh --profile=frontend # minimal + 8 site tools +./install.sh --profile=full # everything (old default behaviour) +./install.sh --add=kei-ledger # add a single primitive on top of current install +./install.sh --add=kei-ledger,ssh-check +./install.sh --add=ops # a profile name works too — unions its members in +./install.sh --list # show each primitive: name | kind | installed? | description +./install.sh --remove=kei-migrate # remove one (rebuilds scoped rust workspace if needed) +``` + +Profile resolution lives in `_primitives/MANIFEST.toml` — one `[primitive.]` entry per primitive plus a `[profile]` block. Edit the manifest to define new profiles without touching `install.sh`. + +> **Migrating from a full install:** if you're re-running `install.sh` after an earlier version that installed all 21 primitives unconditionally, the new default (`minimal`) will REMOVE them. To preserve the old behaviour explicitly, pass `--profile=full`. + > **Re-install disclaimer:** `install.sh` is idempotent for clean state but **overwrites kit-owned `_blocks/`, `_primitives/`, `_bridges/`, `_templates/`, `_assembler/`, `hooks/`, and `skills/` on re-run** — local modifications under those directories are backed up to `.bak-TIMESTAMP/` (or, for shared hook files, to `.bak-TIMESTAMP`). User-owned `_manifests/*.toml` are never overwritten. ## What you get @@ -43,8 +83,8 @@ After install, the only remaining step is merging `settings-snippet.json` into y | Generic agents (manifests) | 12 | `kei-code-implementer`, `kei-critic`, `kei-validator`, `kei-security-auditor`, `kei-architect`, `kei-researcher`, `kei-ml-implementer`, `kei-cost-guardian`, `kei-modal-runner`, ... | | Hooks | 6 | `assemble-agents`, `assemble-validate`, `no-hand-edit-agents`, `tomd-preread`, `agent-fork-logger`, `site-wysiwyd-check` | | Portable skills | 34 | `compose-solution`, `new-agent`, `new-project`, `site-create`, `schema-design`, `observability-setup`, `auth-setup`, `api-design`, `ci-scaffold`, `test-matrix`, `docs-scaffold`, `vm-provision`, ... | -| Primitives (Rust crates) | 8 | `kei-ledger`, `kei-migrate`, `kei-changelog`, `ssh-check`, `firewall-diff`, `mock-render`, `visual-diff`, `tokens-sync` | -| Primitives (shell) | 13 | `tomd`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode`, `metrics-scrape`, `log-ship`, `provision-hetzner`, `provision-vultr`, `harden-base`, `kei-ci-lint`, `kei-docs-scaffold` | +| Primitives (Rust crates, opt-in) | 8 | `kei-ledger`, `kei-migrate`, `kei-changelog`, `ssh-check`, `firewall-diff`, `mock-render`, `visual-diff`, `tokens-sync` | +| Primitives (shell, opt-in) | 13 | `tomd`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode`, `metrics-scrape`, `log-ship`, `provision-hetzner`, `provision-vultr`, `harden-base`, `kei-ci-lint`, `kei-docs-scaffold` | | Cross-tool bridges | 11 | Cursor legacy/MDC, Codex, Copilot, Windsurf, Junie, Continue, Gemini, Aider, Replit | Of the 73 blocks, the **8 base blocks** (`baseline`, `evidence-grading`, `memory-protocol`, `rule-pre-dev-gate`, `rule-test-first`, `rule-error-budget`, `rule-double-audit`, `rule-math-first`) are referenced directly by the 12 shipped manifests. The remaining blocks (`stack-*`, `deploy-*`, `api-*`, `scraper-*`, `domain-*`) are a library consumed by the `/new-agent` wizard and the hub-and-spoke pipeline skills: when you compose a project specialist or spin up a site, the wizard / pipeline picks the appropriate blocks and emits artefacts that reference them.