From 5f51822214b08b44640edee4d124b642503899f7 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 03:34:42 +0800 Subject: [PATCH 01/10] fix(install): trap ERR and roll back from .bak-* snapshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If cargo build or any later step fails, the ERR trap walks the list of backups created during this run and atomically swaps each .bak-TIMESTAMP back onto its original. Idempotent via ROLLED_BACK guard. On success nothing is rolled back — backups remain as the user's recovery copy. --- install.sh | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 9f6bf04..6483ca8 100755 --- a/install.sh +++ b/install.sh @@ -14,11 +14,41 @@ 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; } +# --- 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 + rm -rf "$orig" + 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, $HOOKS_DIR, -# $SKILLS_DIR — never on $KIT_DIR source. +# 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 @@ -28,6 +58,7 @@ backup_dir() { fi local backup="${target}.bak-$(date +%s)" cp -a "$target" "$backup" + BACKUP_PAIRS+=("$target|$backup") say "backed up existing $target to $backup" } From 3c9e89e8c8179b4032aaab66c1a1e036cb078b94 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 03:35:55 +0800 Subject: [PATCH 02/10] feat(install): optional --activate-hooks jq-merge into settings.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds --activate-hooks flag for non-interactive hook activation, plus a TTY prompt at end-of-install. Merge is jq-based, groups by matcher, and de-dupes hook entries by command — idempotent across re-runs. Existing user hooks on the same matcher are preserved. Non-TTY without the flag keeps the manual instructions. --- install.sh | 130 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index 6483ca8..4435ed1 100755 --- a/install.sh +++ b/install.sh @@ -10,6 +10,24 @@ AGENTS_DIR="$HOME_DIR/.claude/agents" HOOKS_DIR="$HOME_DIR/.claude/hooks" SKILLS_DIR="$HOME_DIR/.claude/skills" +# --- flag parsing ---------------------------------------------------------- +ACTIVATE_HOOKS=0 +for arg in "$@"; do + case "$arg" in + --activate-hooks) ACTIVATE_HOOKS=1 ;; + --help|-h) + cat <&2; } @@ -62,6 +80,57 @@ backup_dir() { 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: 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 @@ -176,27 +245,35 @@ fi 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 + printf '\033[1;36m[install]\033[0m activate hooks now? [y/N] ' + read -r reply + case "$reply" in + y|Y|yes|YES) activate_hooks && DID_ACTIVATE=1 ;; + *) say "skipping hook activation" ;; + esac +fi + # --- done ----------------------------------------------------------------- echo say "install complete" echo -cat < Date: Tue, 21 Apr 2026 03:36:18 +0800 Subject: [PATCH 03/10] docs(readme): clarify block usage split + backup-on-reinstall disclaimer Explains that 8 of 34 blocks are used by shipped manifests; the other 26 feed the /new-agent wizard. Adds a v0.1.1 disclaimer that re-running install.sh backs up kit-owned directories and hook files to .bak-TIMESTAMP. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 023d4ea..77f26ec 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ cd KeiSeiKit 5. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place` 6. Copies the three hooks and six skills -After install, the only remaining step is merging `settings-snippet.json` into your `~/.claude/settings.json` to activate the hooks. +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. + +> **Re-install disclaimer (v0.1.1):** `install.sh` is idempotent for clean state but **overwrites kit-owned `_blocks/`, `_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 @@ -38,6 +40,8 @@ After install, the only remaining step is merging `settings-snippet.json` into y | Hooks | 3 | `assemble-agents` (PostToolUse), `assemble-validate` (PreToolUse Bash), `no-hand-edit-agents` (PreToolUse Edit/Write) | | Skills | 6 | `new-agent`, `research`, `test-gen`, `pr-review`, `refactor`, `debug-deep` | +Of the 34 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 14 shipped manifests. The remaining **26 blocks** (`stack-*`, `deploy-*`, `api-*`, `scraper-*`, `domain-*`) are a library consumed by the `/new-agent` wizard: when you compose a project specialist, the wizard picks the appropriate stack / deploy / API / scraper / domain blocks and emits a manifest that references them. The kit's generic 14 agents do not import them by default. + ## Creating a new agent Run the wizard in Claude Code: From 33192a06e0cf37fa3a95d87494a064d926659343 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Tue, 21 Apr 2026 03:46:18 +0800 Subject: [PATCH 04/10] refactor(hooks): port to POSIX sh All three hooks changed shebang from bash to sh. No bashisms were in use (no [[, no local, no arrays) so only the interpreter line moved. Verified with sh -n, and dash smoke-run with a sample tool_input JSON. --- hooks/assemble-agents.sh | 2 +- hooks/assemble-validate.sh | 2 +- hooks/no-hand-edit-agents.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hooks/assemble-agents.sh b/hooks/assemble-agents.sh index ca5ed3a..78ad68f 100755 --- a/hooks/assemble-agents.sh +++ b/hooks/assemble-agents.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # PostToolUse(Edit|Write) — auto-regenerate agent .md files. # # Trigger logic: diff --git a/hooks/assemble-validate.sh b/hooks/assemble-validate.sh index 5e281c6..06de144 100755 --- a/hooks/assemble-validate.sh +++ b/hooks/assemble-validate.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # PreToolUse(Bash) — validate all agent manifests before `git commit` in ~/.claude. # # Trigger: Bash command contains "git commit" AND current directory is under ~/.claude. diff --git a/hooks/no-hand-edit-agents.sh b/hooks/no-hand-edit-agents.sh index ce1c0cf..92b89bd 100755 --- a/hooks/no-hand-edit-agents.sh +++ b/hooks/no-hand-edit-agents.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # PreToolUse(Edit|Write) — block hand-editing generated agent .md files. # # Generated files start with: