Constructor Pattern fix replacing v0.47-v0.48 patch series. The "is the
user interactive?" logic was previously duplicated across 15+ places:
bootstrap.sh x4 ([ -t 0 ] gates on profile/onboard/launch/etc)
install.sh x1 (PATH wiring decision)
install/lib-hooks.sh (activate-hooks prompt)
install/lib-plan.sh (auto-confirm gate)
install/lib-menu.sh (skip-menu gate)
install/lib-wizard.sh (sleep-wizard gate)
install/lib-onboarding.sh x2 (onboarding_should_run + preflight retry)
install/lib-preflight.sh (install-tool prompt)
Every duplicated check was a chance to get curl|bash semantics wrong.
v0.47 used `[ -t 1 ]` (broke under tee'd stdout). v0.48 used `[ -t 0 ]`
(broke under curl pipe stdin). Each fix was a patch on top of the same
architectural defect: scattered truth.
ARCHITECTURAL FIX (Rule Zero — 1 cube = 1 responsibility):
scripts/kei-prompt.sh (NEW, ~110 LOC, public API):
kei_is_interactive → 0 if user is at a terminal, 1 if headless
kei_prompt Q [DEFAULT] → answer or default to stdout
kei_prompt_yn Q [Y|N] → exit 0=yes, 1=no, with [Y/n] hint
kei_prompt_secret Q → no-echo input (tokens, keys)
Truth signal: /dev/tty accessibility, with [ -t 0 ] as second-choice
fallback. KEI_NONINTERACTIVE=1 for CI override. Same contract as the
inline rules — now in ONE place.
bootstrap.sh + install.sh: source the cube at the top, with self-
contained inline fallback (mirrors the kei_is_interactive contract
only) so they remain self-bootable even if scripts/ is missing.
All 15+ inline gates replaced with `kei_is_interactive` calls.
All 3 `read -r -p` prompts in installer cubes replaced with
`kei_prompt` / `kei_prompt_yn`.
Existing copy_pet_scripts() in lib-scaffold.sh installs scripts/*.sh
into ~/.claude/scripts/ automatically — no install logic change needed.
WHAT THIS PREVENTS:
- Next time someone writes a prompt in installer code, the only path
is `kei_prompt`. They CANNOT accidentally type `[ -t 0 ]` because
there is no `[ -t 0 ]` to copy-paste anymore (except inside the
cube itself).
- The v2 tty-interactivity-gate-guard.sh hook (added 2026-05-27)
becomes a regression net rather than the first line of defence.
- Two real install incidents this month (May 2026, 7 prompts each)
do not happen a third time.
VERIFIED:
- Syntax check passes on all 9 modified files + new cube.
- Primitive functions smoke-tested across 8 cases: headless,
KEI_NONINTERACTIVE override, default fallback, yn convenience,
re-source guard, /dev/tty available, /dev/tty open() fails.
- 2 remaining [ -t 0 ] in tree: BOTH inside kei_is_interactive
fallback in bootstrap.sh + install.sh (single-source contract,
not patches).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
web-install.sh tees stdout to a logfile, so -t 1 is false even in an interactive
curl|bash; the /dev/tty fix reattached only stdin. Wizard gates required BOTH
-t 0 and -t 1, so onboarding (language select) and bootstrap's profile wizard
were silently skipped on the primary install path. Prompts go to stderr and read
from stdin — interactive stdin is the only real requirement.
Gates: bootstrap.sh, lib-onboarding.sh (×2), lib-preflight.sh, lib-hooks.sh.
Non-interactive (CI / </dev/null) still skips — verified.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- preflight failure handling: вместо `|| true` (молчаливое продолжение
при упавшем preflight) — явный prompt «продолжить? [y/N]» с return 1
при отказе. Без TTY печатает warning и продолжает. Это закрывает
HIGH bug-9: «.onboarded флаг выставляется при нерабочей конфигурации».
- lib-preflight.sh::preflight_check_cli — общий helper (command -v +
offer-install + version echo). Убирает 6-file boilerplate (хотя сами
per-provider файлы пока не переписаны под него — это отдельный шаг).
- onboarding_fallback_providers: расширен с 3 до 14 провайдеров,
покрывает все 7 транспортов. Был дрейф vs providers.toml (14 vs 3),
юзер без submodule видел только anthropic+openai+ollama.
- STR_PICK_PROVIDER plural mismatch: whiptail и plain ветки теперь
используют один fallback "Provider within" (раньше plain имел
"Providers within", whiptail — "Provider within").
- STR_DONE_NEXT удалён из en.sh + ru.sh (мёртвый ключ).
- Новые ключи: STR_MENU_* (для lib-menu.sh) + STR_PREFLIGHT_FAILED +
STR_PREFLIGHT_CONTINUE. lib-menu.sh начал использовать
STR_MENU_TITLE / STR_MENU_SUBSTRATE (частичная локализация, остальное
меню — отдельной задачей).
Тесты: bash -n чисто, i18n round-trip EN/RU работает, non-TTY smoke
install --no-execute проходит.
1. HIGH-1: onboarding ↔ kei-model-router связка
До: onboarding мастер писал ~/.claude/config/onboarding.toml,
но router его не читал — выбор провайдера декоративный.
После: lib-onboarding.sh::onboarding_write_config доп. пишет
~/.claude/config/user-model-override.toml; registry.rs::Registry
получил load_user_override() возвращающий UserModelOverride.
Приоритет: --pinned > user-override > agent-profiles default_model_ref.
2 новых теста (round-trip TOML, optional transport).
2. HIGH-2: eval "$install_cmd" → bash -c "$install_cmd"
До: lib-preflight.sh::preflight_offer_install делал eval.
После: bash -c с явным subshell + печать команды юзеру до запуска.
3. HIGH-3: codex.sh regex false-pass
До: grep -qiE "logged.in|active" пропускал "not logged in" как pass.
После: сначала negative-pattern (not logged|signed out|please log in),
потом positive (\blogged in\b|status: active|auth: yes).
4. HIGH-4: path traversal в source preflight
До: lib-preflight.sh::preflight_run делал source без валидации
provider id — `../../../evil` сработал бы.
После: whitelist regex ^[a-z0-9][a-z0-9_-]{0,63}$ + realpath
проверка что resolved путь не вышел за PREFLIGHT_DIR.
5. HIGH-5: curl|sh без verification
ollama-local.sh + google-vertex.sh теперь печатают предупреждение
что Linux-установка тянет shell-скрипт с внешнего сервера без
проверки хэша/подписи, и предлагают альтернативу.
MEDIUM попутно:
- anthropic-bedrock.sh: один вызов aws sts get-caller-identity
вместо двух (экономит 1-3с), различает cred-error от network
по тексту stderr, маскирует account ID в ARN перед печатью.
- mlx-local.sh: pip install --user mlx-lm вместо global pip install
(не требует sudo, не загрязняет system Python).
Тесты: cargo test --lib 80/80, bash -n всех изменённых файлов чисто.