feat(install): opt-in hook packs + stack profiles (public posture)

A fresh install now activates only the safety pack; discipline hooks and
agents are opt-in via an onboarding step (step 6) or `kei configure`.
"People don't need Rust-only" — they pick their own stack.

- _primitives/hook-packs.toml: SSoT mapping pack -> hooks, stack -> packs +
  agent groups. safety always on; evidence/observability/epistemic/
  orchestration/git-guard/stack-rust opt-in. rust-first/no-python only under
  the systems stack; git-guard (no-github-push) opt-in only, pulled by no stack.
- lib-profile: extract generic _toml_array (reused by lib-packs); profile_members
  becomes a thin wrapper (no behavior change).
- lib-packs: pack/stack/agent resolvers + selection loader.
- lib-hooks: filter_snippet_by_packs (install-time allowlist) + prune_kit_hooks
  (reconfigure removes deselected kit hooks, keeps foreign ones); activate_hooks
  rewired to prune + filter + merge. No custom settings.json fields (/doctor safe).
- lib-agents: install_manifests filters by stack agent set (empty = install all).
- onboarding: pick_stack step (reuse _onb_read_choice), persists stack_profile +
  enabled_packs to onboarding.toml; i18n STR_* added.
- bin/kei configure -> scripts/kei-configure.sh (re-pick without reinstall);
  install stamps ~/.claude/.kei-kit-dir.
- numeric-claims-guard: money regex no longer matches shell positionals ($1..$9);
  requires decimal / unit / 2+ digits / tilde. Real money + time still caught.
- gate one-liner added to 8 discipline hooks (runtime toggle via hooks-control).

Verified end-to-end (scratch HOME): fresh=safety only; evidence pack adds
numeric+citation; systems stack wires rust-first + 14 base/systems agents (no
data-science/swift); reconfigure-shrink prunes kit hooks but keeps a foreign
hook; settings schema clean; assembler golden 3/3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
KeiSei84 2026-05-25 17:27:14 +08:00
parent 2ffb3a8b1e
commit abae256c1d
22 changed files with 437 additions and 28 deletions

View file

@ -6,6 +6,45 @@
--- ---
## 2026-05-25 — Opt-in hook packs + stack profiles (public-prep posture)
### Context
The kit force-activated every hook via `settings-snippet.json`, including the
author's personal research discipline (numeric-claims evidence markers,
no-downgrade, citation-verify, rust-first / no-python). For a public,
general-audience kit that is presumptuous — users bring their own stack and do
not need a Rust-only policy or evidence-marker enforcement by default.
### Decision
- Posture: **safety hooks on by default; all discipline packs opt-in.** Packs:
`safety` (always), `evidence`, `observability`, `epistemic`, `orchestration`,
`git-guard`, `stack-rust`. SSoT = `_primitives/hook-packs.toml`.
- **Stack profiles** (minimal / web / ml / systems / mobile) pull a set of
discipline packs AND an agent set. `rust-first` / `no-python` live only in
`stack-rust`, which only the `systems` stack enables. `git-guard`
(no-github-push) is opt-in only and pulled by NO stack — a general kit must
not block a user's normal `git push` to github.
- Mechanism: install-time **filter** of the snippet by selected packs
(`filter_snippet_by_packs`) + **prune** of kit-owned hooks on reconfigure
(`prune_kit_hooks`, foreign hooks preserved). Selection persists to
`~/.claude/config/onboarding.toml`; re-runnable via `kei configure`.
- Non-interactive / `--yes` / CI default = minimal (safety + cosmetic only),
all agents (back-compat for power users).
### Consequences
- Gate wiring (`_lib/gate.sh`) added to the 8 highest-friction discipline hooks
for runtime toggling via the `hooks-control` skill; remaining cosmetic/event
hooks deferred (install-time filtering already gives "off by default", so the
runtime gate is a convenience, not a correctness requirement).
- Agent-set changes via `kei configure` apply on the next `./install.sh`
(reconfigure re-applies hooks fully but does not remove already-installed
agent manifests — they are harmless extra `.md` files).
- `_toml_array` extracted from `lib-profile.sh:profile_members` as the shared
one-line-array TOML reader (no new dependency).
## 2026-04-28 — Three scheduling abstractions in workspace ## 2026-04-28 — Three scheduling abstractions in workspace
### Context ### Context

View file

@ -0,0 +1,55 @@
# KeiSeiKit hook-pack + stack-profile map — single source of truth for the
# opt-in install posture. Parsed by install/lib-packs.sh, which reuses the
# generic TOML array reader `_toml_array` extracted from lib-profile.sh
# (python-tomllib preferred, awk fallback). No new dependency.
#
# Values are HOOK BASENAMES WITHOUT `.sh`, matched against the command
# basenames in settings-snippet.json. Every hook wired in the snippet MUST
# appear in exactly one [pack] entry or in [pack-always]; anything missing
# would be silently filtered out of a fresh install.
#
# Posture: only `safety` + `pack-always` are active on a fresh/non-interactive
# install. All other packs are opt-in (via onboarding or `kei configure`).
# `git-guard` (no-github-push) is opt-in ONLY and is pulled by NO stack — a
# general kit must never block a user's normal `git push` to github by default.
[pack]
safety = ["block-dangerous", "safety-guard", "destructive-guard", "disk-headroom-check", "secrets-pre-guard", "no-hand-edit-agents", "assemble-validate", "assemble-agents"]
evidence = ["numeric-claims-guard", "citation-verify", "chat-numeric-prewarn", "chat-numeric-postflag"]
observability = ["task-timer", "session-end-dump", "extract-task-durations", "error-spike-detector", "agent-event-spawn", "agent-event-done", "agent-heartbeat-tick", "stop-verify"]
epistemic = ["alignment-check", "no-downgrade", "recurrence-suggest"]
orchestration = ["agent-fork-logger", "agent-fork-done", "orchestrator-dirty-check", "orchestrator-branch-check", "agent-capability-check", "agent-stub-scan", "milestone-commit-hook", "post-commit-audit", "post-write-check"]
git-guard = ["no-github-push"]
stack-rust = ["rust-first", "no-python-without-approval"]
# Always wired, never filtered (cosmetic / infra). The keisei-pet*.sh status
# updater + the inline pet hook are kept by the filter directly (name match),
# so they are NOT listed here.
[pack-always]
base = ["first-run-onboard", "mailbox-inject", "tomd-preread", "site-wysiwyd-check"]
# Stack profile -> discipline packs auto-enabled (safety is always implicit).
# git-guard intentionally absent from every stack (opt-in only).
[stack-packs]
minimal = []
web = ["evidence", "observability"]
ml = ["evidence", "observability", "epistemic"]
systems = ["evidence", "observability", "stack-rust"]
mobile = ["evidence", "observability"]
# Stack profile -> agent groups installed (the `base` group is always added).
[stack-agents]
minimal = ["base"]
web = ["base", "web"]
ml = ["base", "ml"]
systems = ["base", "systems"]
mobile = ["base", "mobile"]
# Agent group -> manifest basenames (without `.toml`). When no stack is chosen
# (power user / --profile=full / non-interactive), ALL manifests install.
[agent-set]
base = ["architect", "critic", "validator", "researcher", "code-implementer", "security-auditor"]
web = ["code-implementer-typescript", "frontend-validator", "validator-api", "validator-doc", "researcher-web", "researcher-code"]
ml = ["ml-implementer", "ml-researcher", "modal-runner", "cost-guardian", "fal-ai-runner", "code-implementer-python", "validator-benchmark"]
systems = ["code-implementer-rust", "code-implementer-go", "infra-implementer", "infra-implementer-cicd", "infra-implementer-container", "infra-implementer-iac", "infra-implementer-secrets", "validator-version"]
mobile = ["code-implementer-swift", "code-implementer-flutter", "frontend-validator"]

View file

@ -19,12 +19,17 @@
set -e set -e
# --- subcommand dispatch (before splash) --------------------------------- # --- subcommand dispatch (before splash) ---------------------------------
# `kei message ...` → the mailbox CLI; everything else falls through to launch. # `kei message ...` → mailbox CLI; `kei configure` → hook/stack re-picker;
# everything else falls through to launch.
case "${1:-}" in case "${1:-}" in
message|msg|m) message|msg|m)
shift shift
exec "$HOME/.claude/scripts/kei-message.sh" "$@" exec "$HOME/.claude/scripts/kei-message.sh" "$@"
;; ;;
configure|config|reconfigure)
shift
exec "$HOME/.claude/scripts/kei-configure.sh" "$@"
;;
esac esac
# --- args ---------------------------------------------------------------- # --- args ----------------------------------------------------------------

View file

@ -28,6 +28,17 @@ All hooks live under `hooks/` directory. Format: `| Hook Name | Event | Severity
- **remind (exit 0 + stderr on trigger)** — passive reminder - **remind (exit 0 + stderr on trigger)** — passive reminder
- **advisory** — informational, never blocks - **advisory** — informational, never blocks
### Hook packs (opt-in posture)
A fresh install activates **only the `safety` pack** (plus cosmetic/infra hooks).
Discipline packs are opt-in, chosen during onboarding (step 6) or later via
`kei configure`. SSoT for pack membership + stack profiles is
`_primitives/hook-packs.toml`. Packs: `safety` (always on), `evidence`,
`observability`, `epistemic`, `orchestration`, `git-guard` (opt-in only),
`stack-rust` (only under the `systems` stack profile). Discipline hooks also
respect runtime toggling via `KEI_DISABLED_HOOKS` / `KEI_HOOK_PROFILE` (see the
`hooks-control` skill).
### Core Safety Hooks ### Core Safety Hooks
| Hook | Event | Severity | Purpose | Bypass Env | | Hook | Event | Severity | Purpose | Bypass Env |

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "alignment-check" || exit 0; fi
# ALIGNMENT CHECK HOOK # ALIGNMENT CHECK HOOK
# Fires on UserPromptSubmit when comparison/experiment keywords detected. # Fires on UserPromptSubmit when comparison/experiment keywords detected.
# THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment. # THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment.

View file

@ -1,4 +1,7 @@
#!/bin/sh #!/bin/sh
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "chat-numeric-postflag" || exit 0; fi
# chat-numeric-postflag.sh — Stop warn (RULE 0.18 chat-output) # chat-numeric-postflag.sh — Stop warn (RULE 0.18 chat-output)
# #
# Reads the session transcript, extracts the last assistant message, # Reads the session transcript, extracts the last assistant message,

View file

@ -1,4 +1,7 @@
#!/bin/sh #!/bin/sh
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "chat-numeric-prewarn" || exit 0; fi
# chat-numeric-prewarn.sh — UserPromptSubmit remind (RULE 0.18 chat-output) # chat-numeric-prewarn.sh — UserPromptSubmit remind (RULE 0.18 chat-output)
# #
# Detects time/cost/effort keywords in the user's prompt and injects an # Detects time/cost/effort keywords in the user's prompt and injects an

View file

@ -1,4 +1,7 @@
#!/bin/bash #!/bin/bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "citation-verify" || exit 0; fi
# PreToolUse(Edit|Write) — block unverified academic citations # PreToolUse(Edit|Write) — block unverified academic citations
# #
# Rule 0.5 NO HALLUCINATION enforcer. # Rule 0.5 NO HALLUCINATION enforcer.

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "no-downgrade" || exit 0; fi
# RULE -1 NO DOWNGRADE / CONSTRUCTIVE ONLY (2026-04-15 LOCK) enforcement. # RULE -1 NO DOWNGRADE / CONSTRUCTIVE ONLY (2026-04-15 LOCK) enforcement.
# #
# Detects downgrade-style phrases in Write/Edit content without accompanying # Detects downgrade-style phrases in Write/Edit content without accompanying

View file

@ -1,4 +1,7 @@
#!/bin/bash #!/bin/bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "no-python-without-approval" || exit 0; fi
# Hard block on python/python3/python2 invocations in Bash tool. # Hard block on python/python3/python2 invocations in Bash tool.
# RULE 0.2 (Rust First) — Python requires explicit architectural reason. # RULE 0.2 (Rust First) — Python requires explicit architectural reason.
# Claude кroнически нарушает RULE 0.2 inline-вызовами python3 для мелких расчётов. # Claude кroнически нарушает RULE 0.2 inline-вызовами python3 для мелких расчётов.

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "numeric-claims-guard" || exit 0; fi
# RULE 0.18 — Numeric claim enforcement — block Edit/Write of numeric claims # RULE 0.18 — Numeric claim enforcement — block Edit/Write of numeric claims
# without evidence marker. Bypass: RULE_017_BYPASS=1 prefix (kept for compat). # without evidence marker. Bypass: RULE_017_BYPASS=1 prefix (kept for compat).
# #
@ -26,7 +29,7 @@ fi
# - "N MB/GB/LOC/tests/crates/atomars" # - "N MB/GB/LOC/tests/crates/atomars"
# - "~$N", "$N/mo" # - "~$N", "$N/mo"
# - "Nm Ns", "займёт N", "should take N" # - "Nm Ns", "займёт N", "should take N"
NUMERIC_PATTERN='(~\s*[0-9]+(\.[0-9]+)?\s*(min|minute|hour|hr|day|week|month|sec|second|MB|GB|KB|LOC|line|test|crate|atomar|%|µs|ms|ns|TPS|req/s)|[0-9]+m\s*[0-9]+s|\$[0-9]+(\.[0-9]+)?(/(mo|hr|day|run))?|~\s*\$[0-9]+|should take|will take|takes about|займёт|за ~|estimated at|ETA[: ]|approximately\s+[0-9])' NUMERIC_PATTERN='(~\s*[0-9]+(\.[0-9]+)?\s*(min|minute|hour|hr|day|week|month|sec|second|MB|GB|KB|LOC|line|test|crate|atomar|%|µs|ms|ns|TPS|req/s)|[0-9]+m\s*[0-9]+s|\$[0-9]+\.[0-9]+|\$[0-9]+/(mo|hr|day|run)|\$[0-9]{2,}|~\s*\$[0-9]+|should take|will take|takes about|займёт|за ~|estimated at|ETA[: ]|approximately\s+[0-9])'
# Markers that satisfy the rule # Markers that satisfy the rule
EVIDENCE_PATTERN='\[(REAL|FROM-JOURNAL|ESTIMATE-HTC)[: ]' EVIDENCE_PATTERN='\[(REAL|FROM-JOURNAL|ESTIMATE-HTC)[: ]'

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "rust-first" || exit 0; fi
# RULE 0.2 — RUST FIRST reminder hook. # RULE 0.2 — RUST FIRST reminder hook.
# #
# Fires on UserPromptSubmit. Detects keywords indicating language choice # Fires on UserPromptSubmit. Detects keywords indicating language choice

View file

@ -37,6 +37,8 @@ source "$LIB_DIR/lib-log.sh"
source "$LIB_DIR/lib-backup.sh" source "$LIB_DIR/lib-backup.sh"
# shellcheck source=install/lib-profile.sh # shellcheck source=install/lib-profile.sh
source "$LIB_DIR/lib-profile.sh" source "$LIB_DIR/lib-profile.sh"
# shellcheck source=install/lib-packs.sh
source "$LIB_DIR/lib-packs.sh"
# shellcheck source=install/lib-args.sh # shellcheck source=install/lib-args.sh
source "$LIB_DIR/lib-args.sh" source "$LIB_DIR/lib-args.sh"
# shellcheck source=install/lib-menu.sh # shellcheck source=install/lib-menu.sh
@ -150,6 +152,8 @@ say "profile: $PROFILE"
# Stamp the chosen profile so `kei` splash + tools can show it (bin/kei reads this). # Stamp the chosen profile so `kei` splash + tools can show it (bin/kei reads this).
mkdir -p "$HOME_DIR/.claude" 2>/dev/null || true mkdir -p "$HOME_DIR/.claude" 2>/dev/null || true
printf '%s\n' "$PROFILE" > "$HOME_DIR/.claude/.kei-profile" 2>/dev/null || true printf '%s\n' "$PROFILE" > "$HOME_DIR/.claude/.kei-profile" 2>/dev/null || true
# Stamp the kit checkout dir so `kei configure` can re-source the libs later.
printf '%s\n' "$KIT_DIR" > "$HOME_DIR/.claude/.kei-kit-dir" 2>/dev/null || true
# --- resolve profile -> primitive list (UNCONDITIONAL, SSoT) ------------- # --- resolve profile -> primitive list (UNCONDITIONAL, SSoT) -------------
# Must run BEFORE any reader of PROFILE_PRIMS: the --no-execute plan block # Must run BEFORE any reader of PROFILE_PRIMS: the --no-execute plan block

View file

@ -6,7 +6,7 @@ STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools" STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
# Onboarding wizard steps # Onboarding wizard steps
STR_ONBOARDING_INTRO="Onboarding wizard (5 steps)" STR_ONBOARDING_INTRO="Onboarding wizard (6 steps)"
STR_PICK_LANGUAGE="Choose interface language:" STR_PICK_LANGUAGE="Choose interface language:"
STR_PICK_TRANSPORT="Choose connection transport:" STR_PICK_TRANSPORT="Choose connection transport:"
STR_PICK_PROVIDER="Choose provider within" STR_PICK_PROVIDER="Choose provider within"
@ -46,3 +46,19 @@ STR_PICK_INVALID="please type one of the numbers shown"
STR_EXPLAIN_TRANSPORT="How the agents reach the AI. subscription = log in with your plan, no API key (Claude Code is option 1); direct-api = your own API key. Press Enter for the default." STR_EXPLAIN_TRANSPORT="How the agents reach the AI. subscription = log in with your plan, no API key (Claude Code is option 1); direct-api = your own API key. Press Enter for the default."
STR_EXPLAIN_PROVIDER="Which AI service. Option 1 is the recommended default — press Enter." STR_EXPLAIN_PROVIDER="Which AI service. Option 1 is the recommended default — press Enter."
STR_EXPLAIN_MODEL="Default model the agents use. Option 1 is the recommended default — press Enter." STR_EXPLAIN_MODEL="Default model the agents use. Option 1 is the recommended default — press Enter."
# Stack profile + hook-pack picker (step 6)
STR_PICK_STACK="Pick your stack profile (selects which hooks + agents install):"
STR_PICK_STACK_PROMPT="[1-5, default 1=minimal]: "
STR_STACK_MINIMAL="safety hooks + core agents only"
STR_STACK_WEB="TS/frontend agents + evidence, observability"
STR_STACK_ML="ML/data agents + evidence, observability, epistemic"
STR_STACK_SYSTEMS="Rust/Go agents + Rust-first + evidence, observability"
STR_STACK_MOBILE="Swift/Flutter agents + evidence, observability"
STR_PACK_INTRO="Optional discipline packs (safety is always on):"
STR_PACK_EVIDENCE="force evidence markers on numeric/cost claims"
STR_PACK_OBS="task timing, session dumps, agent telemetry"
STR_PACK_EPI="no-downgrade + alignment + recurrence reminders"
STR_PACK_ORCH="multi-agent fork logging + orchestrator git checks"
STR_PACK_GIT="block git push to github (for private-remote teams)"
STR_PACK_ENABLE="enable? [y/N]: "

View file

@ -14,9 +14,18 @@
# when present. # when present.
install_manifests() { install_manifests() {
say "copying generic manifests -> $AGENTS_DIR/_manifests/ (skip if exists)" say "copying generic manifests -> $AGENTS_DIR/_manifests/ (skip if exists)"
local copied=0 skipped=0 f name t has_templates=0 # Stack filter: when a stack profile is chosen, install only its agent set.
# Empty allowlist (no stack / non-interactive) => install ALL (back-compat).
local allow=""
if command -v resolve_selected_agent_manifests >/dev/null 2>&1; then
allow="$(resolve_selected_agent_manifests)"
fi
local copied=0 skipped=0 filtered=0 f name t has_templates=0
for f in "$KIT_DIR/_manifests/"*.toml; do for f in "$KIT_DIR/_manifests/"*.toml; do
name="$(basename "$f")" name="$(basename "$f")"
if [ -n "$allow" ] && ! printf '%s\n' "$allow" | grep -qx "${name%.toml}"; then
filtered=$((filtered+1)); continue
fi
if [[ -f "$AGENTS_DIR/_manifests/$name" ]]; then if [[ -f "$AGENTS_DIR/_manifests/$name" ]]; then
skipped=$((skipped+1)) skipped=$((skipped+1))
else else
@ -24,7 +33,11 @@ install_manifests() {
copied=$((copied+1)) copied=$((copied+1))
fi fi
done done
say " copied $copied, skipped $skipped (already present)" if [ -n "$allow" ]; then
say " copied $copied, skipped $skipped, stack-filtered $filtered"
else
say " copied $copied, skipped $skipped (already present)"
fi
for t in "$KIT_DIR/_templates/"*.template; do for t in "$KIT_DIR/_templates/"*.template; do
[ -f "$t" ] && { has_templates=1; break; } [ -f "$t" ] && { has_templates=1; break; }

View file

@ -102,20 +102,64 @@ _jq_merge_hooks() {
fi fi
} }
# Write a filtered copy of the snippet keeping only hook entries whose command
# basename is in the newline allowlist (plus the cosmetic pet hooks, always
# kept). Drops emptied matcher groups. Echoes the temp path. Arg: $1 = allowlist.
filter_snippet_by_packs() {
local allow="$1" snippet="$KIT_DIR/settings-snippet.json" tmp
tmp="$(mktemp -t kei-snippet.XXXXXX)"
jq --arg allow "$allow" '
def b: sub("^.*/"; "") | sub("\\.sh$"; "");
def keep($ok; $c): (($c | b) as $x | ($ok | index($x)) != null)
or ($c | test("keisei-pet")) or ($c | test("^CMD="));
($allow | split("\n") | map(select(length > 0))) as $ok
| .hooks |= with_entries(
.value |= ( map(.hooks |= map(select(keep($ok; .command))))
| map(select((.hooks | length) > 0)) )
)
' "$snippet" > "$tmp" || { err "snippet filter failed"; rm -f "$tmp"; return 1; }
printf '%s' "$tmp"
}
# Remove every kit-owned hook entry from an existing settings.json (ownership =
# basename in the full pack universe, plus pet hooks). Foreign hooks survive.
# Lets reconfigure REMOVE deselected hooks (the merge alone is additive-only).
# Args: $1 = target settings.json, $2 = newline list of all kit hook basenames.
prune_kit_hooks() {
local target="$1" universe="$2" tmp
tmp="$(mktemp "$target.XXXXXX")"
jq --arg universe "$universe" '
def b: sub("^.*/"; "") | sub("\\.sh$"; "");
def owned($kit; $c): (($c | b) as $x | ($kit | index($x)) != null)
or ($c | test("keisei-pet")) or ($c | test("^CMD="));
($universe | split("\n") | map(select(length > 0))) as $kit
| .hooks |= with_entries(
.value |= ( map(.hooks |= map(select(owned($kit; .command) | not)))
| map(select((.hooks | length) > 0)) )
)
' "$target" > "$tmp" && mv "$tmp" "$target" || { err "prune failed"; rm -f "$tmp"; return 1; }
}
activate_hooks() { activate_hooks() {
local snippet="$KIT_DIR/settings-snippet.json" local snippet="$KIT_DIR/settings-snippet.json"
local target="$HOME_DIR/.claude/settings.json" local target="$HOME_DIR/.claude/settings.json"
[ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; } [ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; }
local allow filtered
allow="$(resolve_selected_hook_basenames)"
filtered="$(filter_snippet_by_packs "$allow")" || return 1
if [ ! -f "$target" ]; then if [ ! -f "$target" ]; then
local tmp local tmp
tmp="$(mktemp "$target.XXXXXX")" tmp="$(mktemp "$target.XXXXXX")"
jq 'del(._comment)' "$snippet" > "$tmp" jq 'del(._comment)' "$filtered" > "$tmp"
mv "$tmp" "$target" mv "$tmp" "$target"
say "created $target from snippet (no prior settings.json)" rm -f "$filtered"
say "created $target from filtered snippet"
return 0 return 0
fi fi
backup_file "$target" backup_file "$target"
_jq_merge_hooks "$snippet" "$target" prune_kit_hooks "$target" "$(all_pack_basenames)"
_jq_merge_hooks "$filtered" "$target"
rm -f "$filtered"
} }
# Flag-or-prompt dispatcher, mirroring the v0.15 behavior: # Flag-or-prompt dispatcher, mirroring the v0.15 behavior:

View file

@ -39,6 +39,8 @@ language = "$ONBOARDING_LANG"
transport = "$ONBOARDING_TRANSPORT" transport = "$ONBOARDING_TRANSPORT"
provider = "$ONBOARDING_PROVIDER" provider = "$ONBOARDING_PROVIDER"
default_model = "$ONBOARDING_MODEL" default_model = "$ONBOARDING_MODEL"
stack_profile = "$ONBOARDING_STACK"
enabled_packs = "$ONBOARDING_PACKS"
EOF EOF
# Override для kei-model-router (HIGH аудит-1). # Override для kei-model-router (HIGH аудит-1).

View file

@ -29,6 +29,53 @@ _onb_read_choice() {
done done
} }
# Step 6 — pick a stack profile (selects which discipline hooks + agents
# install) then optionally toggle discipline packs the stack does not pull.
# Sets ONBOARDING_STACK + ONBOARDING_PACKS. Reuses _onb_read_choice + stack_packs
# (lib-packs.sh). Default = minimal (safety hooks + core agents only).
onboarding_pick_stack() {
echo "" >&2
printf '%s\n' "${STR_PICK_STACK:-Pick your stack profile (selects hooks + agents):}" >&2
local opts="minimal web ml systems mobile" i=1 o d ans
for o in $opts; do
case "$o" in
minimal) d="${STR_STACK_MINIMAL:-safety hooks + core agents only}" ;;
web) d="${STR_STACK_WEB:-TS/frontend agents + evidence, observability}" ;;
ml) d="${STR_STACK_ML:-ML/data agents + evidence, observability, epistemic}" ;;
systems) d="${STR_STACK_SYSTEMS:-Rust/Go agents + Rust-first + evidence, observability}" ;;
mobile) d="${STR_STACK_MOBILE:-Swift/Flutter agents + evidence, observability}" ;;
esac
printf ' %d) %-8s — %s\n' "$i" "$o" "$d" >&2
i=$((i+1))
done
ans="$(_onb_read_choice 5 "${STR_PICK_STACK_PROMPT:-[1-5, default 1=minimal]: }")"
ONBOARDING_STACK="$(echo "$opts" | cut -d' ' -f"$ans")"
[ -n "$ONBOARDING_STACK" ] || ONBOARDING_STACK="minimal"
# Offer discipline packs the chosen stack does not already enable.
local stackpacks p pd reply
stackpacks=" $(command -v stack_packs >/dev/null 2>&1 && stack_packs "$ONBOARDING_STACK") "
ONBOARDING_PACKS=""
printf '%s\n' "${STR_PACK_INTRO:-Optional discipline packs (safety is always on):}" >&2
for p in evidence observability epistemic orchestration git-guard; do
case "$stackpacks" in *" $p "*) continue ;; esac
case "$p" in
evidence) pd="${STR_PACK_EVIDENCE:-force evidence markers on numeric/cost claims}" ;;
observability) pd="${STR_PACK_OBS:-task timing, session dumps, agent telemetry}" ;;
epistemic) pd="${STR_PACK_EPI:-no-downgrade + alignment + recurrence reminders}" ;;
orchestration) pd="${STR_PACK_ORCH:-multi-agent fork logging + orchestrator git checks}" ;;
git-guard) pd="${STR_PACK_GIT:-block git push to github (for private-remote teams)}" ;;
esac
printf ' + %-13s — %s\n' "$p" "$pd" >&2
read -r -p " ${STR_PACK_ENABLE:-enable? [y/N]: }" reply
case "$reply" in y|Y|yes|YES) ONBOARDING_PACKS="$ONBOARDING_PACKS $p" ;; esac
done
ONBOARDING_PACKS="$(echo "$ONBOARDING_PACKS" | sed 's/^ *//;s/ *$//')"
if command -v say >/dev/null 2>&1; then
say "stack: $ONBOARDING_STACK packs: ${ONBOARDING_PACKS:-(stack defaults only)}"
fi
}
onboarding_pick_language() { onboarding_pick_language() {
local langs local langs
langs="$(i18n_available_languages 2>/dev/null)" langs="$(i18n_available_languages 2>/dev/null)"

View file

@ -20,6 +20,8 @@ ONBOARDING_LANG=""
ONBOARDING_TRANSPORT="" ONBOARDING_TRANSPORT=""
ONBOARDING_PROVIDER="" ONBOARDING_PROVIDER=""
ONBOARDING_MODEL="" ONBOARDING_MODEL=""
ONBOARDING_STACK=""
ONBOARDING_PACKS=""
declare -a ONBOARDING_AUTH_ENV_KEYS=() declare -a ONBOARDING_AUTH_ENV_KEYS=()
declare -a ONBOARDING_AUTH_ENV_VALUES=() declare -a ONBOARDING_AUTH_ENV_VALUES=()
@ -49,20 +51,21 @@ onboarding_should_run() {
return 0 return 0
} }
# Оркестратор: 5 шагов + preflight + запись. # Оркестратор: 6 шагов + preflight + запись.
onboarding_run() { onboarding_run() {
onboarding_should_run || return 0 onboarding_should_run || return 0
if command -v say >/dev/null 2>&1; then if command -v say >/dev/null 2>&1; then
say "${STR_ONBOARDING_INTRO:-Onboarding wizard (5 steps)}" say "${STR_ONBOARDING_INTRO:-Onboarding wizard (6 steps)}"
else else
echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (5 steps)} ──" >&2 echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (6 steps)} ──" >&2
fi fi
onboarding_pick_language onboarding_pick_language
onboarding_pick_transport onboarding_pick_transport
onboarding_pick_provider onboarding_pick_provider
onboarding_pick_model onboarding_pick_model
onboarding_pick_stack
# Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей. # Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей.
if command -v preflight_run >/dev/null 2>&1; then if command -v preflight_run >/dev/null 2>&1; then

74
install/lib-packs.sh Normal file
View file

@ -0,0 +1,74 @@
# shellcheck shell=bash
# lib-packs.sh — hook-pack + stack-profile resolver. Reads _primitives/hook-packs.toml
# via the generic _toml_array reader (from lib-profile.sh). Decides which hooks get
# wired into settings.json and which agent manifests install, based on the user's
# onboarding selection (or the safe minimal default when none was made).
#
# Requires: _toml_array from lib-profile.sh.
# Reads globals: $KIT_DIR (kit checkout), optional $ONBOARDING_STACK / $ONBOARDING_PACKS
# (live onboarding), optional $ONBOARDING_CONFIG (persisted selection).
PACKS_TOML="${PACKS_TOML:-$KIT_DIR/_primitives/hook-packs.toml}"
# --- thin table readers ---------------------------------------------------
pack_hooks() { _toml_array "$PACKS_TOML" "pack" "$1"; }
stack_packs() { _toml_array "$PACKS_TOML" "stack-packs" "$1"; }
stack_agent_groups() { _toml_array "$PACKS_TOML" "stack-agents" "$1"; }
agent_set_members() { _toml_array "$PACKS_TOML" "agent-set" "$1"; }
_packs_always() { _toml_array "$PACKS_TOML" "pack-always" "base"; }
# --- selection (live onboarding globals > persisted toml > none) ----------
# Echo the chosen stack, or empty if the user never chose one.
_packs_chosen_stack() {
if [ -n "${ONBOARDING_STACK:-}" ]; then printf '%s' "$ONBOARDING_STACK"; return; fi
local cfg="${ONBOARDING_CONFIG:-$HOME/.claude/config/onboarding.toml}"
[ -f "$cfg" ] && grep -E '^stack_profile[[:space:]]*=' "$cfg" \
| sed -E 's/.*=[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1
}
# Echo the explicitly enabled packs (space-separated), or empty.
_packs_chosen_packs() {
if [ -n "${ONBOARDING_PACKS:-}" ]; then printf '%s' "$ONBOARDING_PACKS"; return; fi
local cfg="${ONBOARDING_CONFIG:-$HOME/.claude/config/onboarding.toml}"
[ -f "$cfg" ] && grep -E '^enabled_packs[[:space:]]*=' "$cfg" \
| sed -E 's/.*=[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1
}
# --- resolution -----------------------------------------------------------
# Newline list of hook basenames to wire on install: safety + always + every
# pack pulled by the chosen stack + every explicitly enabled pack. Default
# (no choice) = safety + always only.
resolve_selected_hook_basenames() {
local stack packs p out
stack="$(_packs_chosen_stack)"; stack="${stack:-minimal}"
packs="$(_packs_chosen_packs)"
out="$(pack_hooks safety) $(_packs_always)"
for p in $(stack_packs "$stack") $packs; do
out="$out $(pack_hooks "$p")"
done
echo "$out" | tr ' ' '\n' | grep -v '^$' | sort -u
}
# Newline list of agent manifest basenames to install for the chosen stack
# (base group always included). EMPTY when no stack was chosen → caller
# installs ALL manifests (power-user / non-interactive default).
resolve_selected_agent_manifests() {
local stack g out
stack="$(_packs_chosen_stack)"
[ -n "$stack" ] || return 0
out=""
for g in $(stack_agent_groups "$stack"); do
out="$out $(agent_set_members "$g")"
done
echo "$out" | tr ' ' '\n' | grep -v '^$' | sort -u
}
# Newline list of EVERY kit-owned hook basename (all packs + always). Used by
# prune_kit_hooks to identify which settings.json entries the kit owns.
all_pack_basenames() {
local p out=""
for p in safety evidence observability epistemic orchestration git-guard stack-rust; do
out="$out $(pack_hooks "$p")"
done
out="$out $(_packs_always)"
echo "$out" | tr ' ' '\n' | grep -v '^$' | sort -u
}

View file

@ -19,13 +19,16 @@ have_python_toml() {
return 1 return 1
} }
# Echo space-separated primitive names for a given profile. # Generic one-line-array TOML reader. Echoes space-separated values of
# Usage: profile_members <profile-name> # [<table>]
profile_members() { # <key> = ["a", "b", ...]
local profile="$1" # python-tomllib preferred; awk fallback handles one-line arrays only.
[ -f "$MANIFEST" ] || { err "MANIFEST.toml not found at $MANIFEST"; return 1; } # Usage: _toml_array <file> <table> <key>
_toml_array() {
local file="$1" table="$2" key="$3"
[ -f "$file" ] || return 1
if have_python_toml; then if have_python_toml; then
python3 - "$MANIFEST" "$profile" <<'PY' 2>/dev/null || return 1 python3 - "$file" "$table" "$key" <<'PY' 2>/dev/null || return 1
import sys import sys
try: try:
import tomllib import tomllib
@ -33,20 +36,19 @@ try:
except ImportError: except ImportError:
import toml as tomllib import toml as tomllib
mode = "r" mode = "r"
path, prof = sys.argv[1], sys.argv[2] path, table, key = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path, mode) as f: with open(path, mode) as f:
data = tomllib.load(f) if mode == "rb" else tomllib.load(f) data = tomllib.load(f)
members = data.get("profile", {}).get(prof) vals = data.get(table, {}).get(key)
if members is None: if vals is None:
sys.exit(2) sys.exit(2)
print(" ".join(members)) print(" ".join(vals))
PY PY
else else
# awk fallback — only handles `profile.<name> = [...]` on one line awk -v table="$table" -v key="$key" '
awk -v prof="$profile" ' $0 ~ "^\\[" table "\\]" { in_t=1; next }
/^\[profile\]/ { in_profile=1; next } /^\[/ { in_t=0 }
/^\[/ && !/^\[profile\]/ { in_profile=0 } in_t && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
in_profile && $0 ~ "^[[:space:]]*" prof "[[:space:]]*=" {
line = $0 line = $0
sub(/^[^\[]*\[/, "", line) sub(/^[^\[]*\[/, "", line)
sub(/\].*$/, "", line) sub(/\].*$/, "", line)
@ -55,10 +57,18 @@ PY
print line print line
exit exit
} }
' "$MANIFEST" ' "$file"
fi fi
} }
# Echo space-separated primitive names for a given profile.
# Usage: profile_members <profile-name>
profile_members() {
local profile="$1"
[ -f "$MANIFEST" ] || { err "MANIFEST.toml not found at $MANIFEST"; return 1; }
_toml_array "$MANIFEST" "profile" "$profile"
}
# Echo a field of a primitive. Usage: primitive_field <name> <field> # Echo a field of a primitive. Usage: primitive_field <name> <field>
# field ∈ { kind, file, crate, desc, deps } # field ∈ { kind, file, crate, desc, deps }
primitive_field() { primitive_field() {

62
scripts/kei-configure.sh Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
# kei-configure — re-pick hook packs + stack profile after install, without a
# full reinstall. Updates ~/.claude/config/onboarding.toml and re-applies the
# hook selection to settings.json (adds newly selected hooks, removes deselected
# kit hooks, leaves your own hooks untouched). Agent-set changes apply on the
# next `./install.sh`.
#
# Invoked via `kei configure`. Interactive (needs a terminal).
set -u
set -o pipefail 2>/dev/null || true
HOME_DIR="${HOME:?HOME not set}"
KIT_DIR="$(cat "$HOME_DIR/.claude/.kei-kit-dir" 2>/dev/null || true)"
if [ -z "$KIT_DIR" ] || [ ! -d "$KIT_DIR/install" ]; then
echo "kei configure: KeiSeiKit checkout not found." >&2
echo " (expected its path in ~/.claude/.kei-kit-dir; re-run ./install.sh from your checkout)" >&2
exit 1
fi
if [ ! -t 0 ]; then
echo "kei configure: interactive only — run it from a terminal." >&2
exit 1
fi
LIB_DIR="$KIT_DIR/install"
MANIFEST="$KIT_DIR/_primitives/MANIFEST.toml"
PACKS_TOML="$KIT_DIR/_primitives/hook-packs.toml"
ONBOARDING_CONFIG="$HOME_DIR/.claude/config/onboarding.toml"
export HOME_DIR KIT_DIR LIB_DIR MANIFEST PACKS_TOML ONBOARDING_CONFIG
# shellcheck source=/dev/null
source "$LIB_DIR/lib-log.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-backup.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-profile.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-packs.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-hooks.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-onboarding-ui.sh"
ONBOARDING_STACK=""
ONBOARDING_PACKS=""
onboarding_pick_stack
# Update only stack_profile/enabled_packs in onboarding.toml; preserve the rest.
mkdir -p "$(dirname "$ONBOARDING_CONFIG")"
touch "$ONBOARDING_CONFIG"
_tmp="$(mktemp)"
grep -vE '^(stack_profile|enabled_packs)[[:space:]]*=' "$ONBOARDING_CONFIG" > "$_tmp" 2>/dev/null || true
{
printf 'stack_profile = "%s"\n' "$ONBOARDING_STACK"
printf 'enabled_packs = "%s"\n' "$ONBOARDING_PACKS"
} >> "$_tmp"
mv "$_tmp" "$ONBOARDING_CONFIG"
# Re-apply hooks: prune kit-owned entries, merge the newly selected set.
activate_hooks
say "reconfigured: stack=$ONBOARDING_STACK packs=${ONBOARDING_PACKS:-none}"
say " settings.json hooks updated. Agent-set changes apply on the next ./install.sh."