diff --git a/DECISIONS.md b/DECISIONS.md index e7b0b3f..57b3f82 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -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 ### Context diff --git a/_primitives/hook-packs.toml b/_primitives/hook-packs.toml new file mode 100644 index 0000000..c87f2f8 --- /dev/null +++ b/_primitives/hook-packs.toml @@ -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"] diff --git a/bin/kei b/bin/kei index b151d19..c7fc534 100755 --- a/bin/kei +++ b/bin/kei @@ -19,12 +19,17 @@ set -e # --- 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 message|msg|m) shift exec "$HOME/.claude/scripts/kei-message.sh" "$@" ;; + configure|config|reconfigure) + shift + exec "$HOME/.claude/scripts/kei-configure.sh" "$@" + ;; esac # --- args ---------------------------------------------------------------- diff --git a/docs/encyclopedia/hooks-and-blocks.md b/docs/encyclopedia/hooks-and-blocks.md index 1535ac3..e879acc 100644 --- a/docs/encyclopedia/hooks-and-blocks.md +++ b/docs/encyclopedia/hooks-and-blocks.md @@ -28,6 +28,17 @@ All hooks live under `hooks/` directory. Format: `| Hook Name | Event | Severity - **remind (exit 0 + stderr on trigger)** — passive reminder - **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 | Hook | Event | Severity | Purpose | Bypass Env | diff --git a/hooks/alignment-check.sh b/hooks/alignment-check.sh index 9d08782..e197d96 100755 --- a/hooks/alignment-check.sh +++ b/hooks/alignment-check.sh @@ -1,4 +1,7 @@ #!/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 # Fires on UserPromptSubmit when comparison/experiment keywords detected. # THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment. diff --git a/hooks/chat-numeric-postflag.sh b/hooks/chat-numeric-postflag.sh index fc18969..4769996 100755 --- a/hooks/chat-numeric-postflag.sh +++ b/hooks/chat-numeric-postflag.sh @@ -1,4 +1,7 @@ #!/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) # # Reads the session transcript, extracts the last assistant message, diff --git a/hooks/chat-numeric-prewarn.sh b/hooks/chat-numeric-prewarn.sh index 8d8411b..6655118 100755 --- a/hooks/chat-numeric-prewarn.sh +++ b/hooks/chat-numeric-prewarn.sh @@ -1,4 +1,7 @@ #!/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) # # Detects time/cost/effort keywords in the user's prompt and injects an diff --git a/hooks/citation-verify.sh b/hooks/citation-verify.sh index 7f473e9..57bc142 100755 --- a/hooks/citation-verify.sh +++ b/hooks/citation-verify.sh @@ -1,4 +1,7 @@ #!/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 # # Rule 0.5 NO HALLUCINATION enforcer. diff --git a/hooks/no-downgrade.sh b/hooks/no-downgrade.sh index 8bcf491..f12e612 100755 --- a/hooks/no-downgrade.sh +++ b/hooks/no-downgrade.sh @@ -1,4 +1,7 @@ #!/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. # # Detects downgrade-style phrases in Write/Edit content without accompanying diff --git a/hooks/no-python-without-approval.sh b/hooks/no-python-without-approval.sh index 9bd28ba..103280c 100755 --- a/hooks/no-python-without-approval.sh +++ b/hooks/no-python-without-approval.sh @@ -1,4 +1,7 @@ #!/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. # RULE 0.2 (Rust First) — Python requires explicit architectural reason. # Claude кroнически нарушает RULE 0.2 inline-вызовами python3 для мелких расчётов. diff --git a/hooks/numeric-claims-guard.sh b/hooks/numeric-claims-guard.sh index 28570de..7d373e1 100755 --- a/hooks/numeric-claims-guard.sh +++ b/hooks/numeric-claims-guard.sh @@ -1,4 +1,7 @@ #!/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 # 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", "$N/mo" # - "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 EVIDENCE_PATTERN='\[(REAL|FROM-JOURNAL|ESTIMATE-HTC)[: ]' diff --git a/hooks/rust-first.sh b/hooks/rust-first.sh index 15a3c70..2c665e1 100755 --- a/hooks/rust-first.sh +++ b/hooks/rust-first.sh @@ -1,4 +1,7 @@ #!/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. # # Fires on UserPromptSubmit. Detects keywords indicating language choice diff --git a/install.sh b/install.sh index b849207..3c9ded1 100755 --- a/install.sh +++ b/install.sh @@ -37,6 +37,8 @@ source "$LIB_DIR/lib-log.sh" source "$LIB_DIR/lib-backup.sh" # shellcheck source=install/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 source "$LIB_DIR/lib-args.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). mkdir -p "$HOME_DIR/.claude" 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) ------------- # Must run BEFORE any reader of PROFILE_PRIMS: the --no-execute plan block diff --git a/install/i18n/en.sh b/install/i18n/en.sh index 3c975fa..5184157 100644 --- a/install/i18n/en.sh +++ b/install/i18n/en.sh @@ -6,7 +6,7 @@ STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer" STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools" # 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_TRANSPORT="Choose connection transport:" 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_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." + +# 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]: " diff --git a/install/lib-agents.sh b/install/lib-agents.sh index b595aea..1e813d6 100644 --- a/install/lib-agents.sh +++ b/install/lib-agents.sh @@ -14,9 +14,18 @@ # when present. install_manifests() { 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 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 skipped=$((skipped+1)) else @@ -24,7 +33,11 @@ install_manifests() { copied=$((copied+1)) fi 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 [ -f "$t" ] && { has_templates=1; break; } diff --git a/install/lib-hooks.sh b/install/lib-hooks.sh index ce09f8d..8f6bcb2 100644 --- a/install/lib-hooks.sh +++ b/install/lib-hooks.sh @@ -102,20 +102,64 @@ _jq_merge_hooks() { 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() { local snippet="$KIT_DIR/settings-snippet.json" local target="$HOME_DIR/.claude/settings.json" [ -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 local tmp tmp="$(mktemp "$target.XXXXXX")" - jq 'del(._comment)' "$snippet" > "$tmp" + jq 'del(._comment)' "$filtered" > "$tmp" mv "$tmp" "$target" - say "created $target from snippet (no prior settings.json)" + rm -f "$filtered" + say "created $target from filtered snippet" return 0 fi 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: diff --git a/install/lib-onboarding-state.sh b/install/lib-onboarding-state.sh index e89f0ee..0395898 100644 --- a/install/lib-onboarding-state.sh +++ b/install/lib-onboarding-state.sh @@ -39,6 +39,8 @@ language = "$ONBOARDING_LANG" transport = "$ONBOARDING_TRANSPORT" provider = "$ONBOARDING_PROVIDER" default_model = "$ONBOARDING_MODEL" +stack_profile = "$ONBOARDING_STACK" +enabled_packs = "$ONBOARDING_PACKS" EOF # Override для kei-model-router (HIGH аудит-1). diff --git a/install/lib-onboarding-ui.sh b/install/lib-onboarding-ui.sh index 0b80413..58076ed 100644 --- a/install/lib-onboarding-ui.sh +++ b/install/lib-onboarding-ui.sh @@ -29,6 +29,53 @@ _onb_read_choice() { 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() { local langs langs="$(i18n_available_languages 2>/dev/null)" diff --git a/install/lib-onboarding.sh b/install/lib-onboarding.sh index 444bc5a..91ead5e 100644 --- a/install/lib-onboarding.sh +++ b/install/lib-onboarding.sh @@ -20,6 +20,8 @@ ONBOARDING_LANG="" ONBOARDING_TRANSPORT="" ONBOARDING_PROVIDER="" ONBOARDING_MODEL="" +ONBOARDING_STACK="" +ONBOARDING_PACKS="" declare -a ONBOARDING_AUTH_ENV_KEYS=() declare -a ONBOARDING_AUTH_ENV_VALUES=() @@ -49,20 +51,21 @@ onboarding_should_run() { return 0 } -# Оркестратор: 5 шагов + preflight + запись. +# Оркестратор: 6 шагов + preflight + запись. onboarding_run() { onboarding_should_run || return 0 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 - echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (5 steps)} ──" >&2 + echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (6 steps)} ──" >&2 fi onboarding_pick_language onboarding_pick_transport onboarding_pick_provider onboarding_pick_model + onboarding_pick_stack # Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей. if command -v preflight_run >/dev/null 2>&1; then diff --git a/install/lib-packs.sh b/install/lib-packs.sh new file mode 100644 index 0000000..e6f00cd --- /dev/null +++ b/install/lib-packs.sh @@ -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 +} diff --git a/install/lib-profile.sh b/install/lib-profile.sh index d286f3e..6ca442c 100644 --- a/install/lib-profile.sh +++ b/install/lib-profile.sh @@ -19,13 +19,16 @@ have_python_toml() { 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; } +# Generic one-line-array TOML reader. Echoes space-separated values of +# [] +# = ["a", "b", ...] +# python-tomllib preferred; awk fallback handles one-line arrays only. +# Usage: _toml_array
+_toml_array() { + local file="$1" table="$2" key="$3" + [ -f "$file" ] || return 1 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 try: import tomllib @@ -33,20 +36,19 @@ try: except ImportError: import toml as tomllib 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: - data = tomllib.load(f) if mode == "rb" else tomllib.load(f) -members = data.get("profile", {}).get(prof) -if members is None: + data = tomllib.load(f) +vals = data.get(table, {}).get(key) +if vals is None: sys.exit(2) -print(" ".join(members)) +print(" ".join(vals)) 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:]]*=" { + awk -v table="$table" -v key="$key" ' + $0 ~ "^\\[" table "\\]" { in_t=1; next } + /^\[/ { in_t=0 } + in_t && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { line = $0 sub(/^[^\[]*\[/, "", line) sub(/\].*$/, "", line) @@ -55,10 +57,18 @@ PY print line exit } - ' "$MANIFEST" + ' "$file" fi } +# 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; } + _toml_array "$MANIFEST" "profile" "$profile" +} + # Echo a field of a primitive. Usage: primitive_field # field ∈ { kind, file, crate, desc, deps } primitive_field() { diff --git a/scripts/kei-configure.sh b/scripts/kei-configure.sh new file mode 100644 index 0000000..0188e06 --- /dev/null +++ b/scripts/kei-configure.sh @@ -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."