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>
INCIDENT: user installed via curl|bash and reported "kei did not auto-launch".
Root cause: under curl|bash, stdin is the pipe from curl. Every interactive
gate `[ -t 0 ]` returned FALSE, silently skipping FIVE places:
install/lib-i18n.sh language picker
install/lib-preflight.sh preflight checks
install/lib-hooks.sh hook activation prompt
install/lib-wizard.sh sleep wizard
install.sh:270 PATH wiring
bootstrap.sh kei-onboard primary CLI picker
bootstrap.sh launch prompt "запустить kei сейчас?"
Per rules/tty-interactivity-gate.md the rule is "gate on stdin, never stdout"
— and we did. But under curl|bash BOTH are non-tty. /dev/tty is the only
reliable signal that the user is interactive.
FIX: in bootstrap.sh, ONCE, before invoking install.sh:
if [ -r /dev/tty ] && [ -w /dev/tty ]; then
exec </dev/tty
fi
This redirects bootstrap.sh's OWN stdin to /dev/tty. Every child process
inherits it via fork — install.sh, lib-*.sh, kei-onboard.sh, kei-pick.sh,
kei-mcp-wire.sh — and their `[ -t 0 ]` gates now correctly report true.
Headless / CI / nohup (no /dev/tty) → skipped, gates stay false, no prompts.
Simplified the v0.47 onboarding + launch-prompt blocks: they no longer
need have_tty() or explicit </dev/tty redirects on read — the global
reattach handles it cascadally.
Smoke-tested 2 of 3 paths (curl|bash via `echo "" | bash script.sh` ✓;
plain bash needs real TTY which Claude Code sandbox doesn't provide;
headless needs setsid which is Linux-only). Logic verified.
Version bump v0.47 → v0.48 because behaviour materially changes for every
curl|bash user (5 prompts now fire that were silently skipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Splash (bin/kei) — благородная насыщенная жёлто-бронзовая drop-shadow
on the KEISEI ASCII art:
- New CS color: \033[1;38;5;130m (noble saturated bronze-gold).
- Shadow block printed FIRST (6 lines, offset +2 cols right).
- \e[7A cursor-up returns to start; blue art overwrites where they overlap.
- Visible shadow: right-edge 2-col tail on blue rows 2-6 + full
shadow row 6 standalone (offset down-right).
- TTY-gated: no terminal → no shadow, no colors (existing fallback).
2. bootstrap.sh — post-install launch prompt:
"Запустить kei сейчас? [Y/n]" at the very end, after all install +
onboarding + next-steps text. Default Y on Enter.
- Stdin-TTY gate only (rule: tty-interactivity-gate.md — `-t 1` would
falsely skip under curl|bash because the bootstrap log tees stdout).
- Reads from /dev/tty explicitly so curl|bash piped install still works.
- KEI_NO_AUTORUN=1 env opt-out for CI / scripts.
3. README — Platforms section: honest Windows status.
- macOS + Linux: fully supported.
- Windows: substrate is Bash-only; WSL2 recommended path; MCP-server
binary (.exe) ships in releases for MCP-only mode; native PowerShell
port NOT on roadmap (WSL gives 100% coverage with 0 code duplication).
4. v0.47 version bumps: plugin.json + bin/kei splash + README counts header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>