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:
parent
2ffb3a8b1e
commit
abae256c1d
22 changed files with 437 additions and 28 deletions
39
DECISIONS.md
39
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
|
## 2026-04-28 — Three scheduling abstractions in workspace
|
||||||
|
|
||||||
### Context
|
### Context
|
||||||
|
|
|
||||||
55
_primitives/hook-packs.toml
Normal file
55
_primitives/hook-packs.toml
Normal 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"]
|
||||||
7
bin/kei
7
bin/kei
|
|
@ -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 ----------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 для мелких расчётов.
|
||||||
|
|
|
||||||
|
|
@ -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)[: ]'
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]: "
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
if [ -n "$allow" ]; then
|
||||||
|
say " copied $copied, skipped $skipped, stack-filtered $filtered"
|
||||||
|
else
|
||||||
say " copied $copied, skipped $skipped (already present)"
|
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; }
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
|
|
|
||||||
|
|
@ -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
74
install/lib-packs.sh
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
62
scripts/kei-configure.sh
Normal 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."
|
||||||
Loading…
Reference in a new issue