fix(bootstrap): v0.48 — reattach stdin to /dev/tty so curl|bash prompts fire

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>
This commit is contained in:
KeiSei84 2026-05-27 15:08:09 +08:00
parent 82c4542090
commit 109b69662c
3 changed files with 25 additions and 10 deletions

View file

@ -248,7 +248,7 @@ ${C1} ██╔═██╗ ██╔══╝ ██║╚════█
${C1} ██║ ██╗███████╗██║███████║███████╗██║${C0}
${C1} ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝${C0}
${C2} KeiSeiKit · substrate v0.47${C0}
${C2} KeiSeiKit · substrate v0.48${C0}
${C3} ─────────────────────────────────────${C0}
primary CLI : ${CV}${PRIMARY}${C0}
profile : ${CV}${p}${C0}

View file

@ -227,6 +227,19 @@ log "checkout: $KIT_DIR"
# --- 5. run install ------------------------------------------------------
log "running install.sh --profile=$PROFILE $YES_FLAG ${EXTRA_FLAGS[*]:-}"
cd "$KIT_DIR"
# v0.48: reattach stdin to /dev/tty for the install + everything after.
# Under `curl|bash` stdin is the curl pipe, so install.sh's interactive
# gates (5 places: language pick, preflight, hooks-activate, sleep wizard,
# PATH wiring) all silently skip via [ -t 0 ] being false. Reattaching ONCE
# here cascades correctly: every child script inherits the terminal stdin
# and its [ -t 0 ] returns true. Only do it if /dev/tty is actually
# present and readable (CI / nohup / systemd: skip — those are headless).
if [ -r /dev/tty ] && [ -w /dev/tty ]; then
exec </dev/tty
log "stdin reattached to /dev/tty (curl|bash interactive prompts will work)"
fi
# Defensive: invoke via `bash` not `./install.sh` because GitHub's contents
# API does NOT preserve the executable bit on `gh api -X PUT` updates
# (only the git Data API does). Older clones may have install.sh with
@ -253,9 +266,10 @@ log "===========================================================================
log "DONE — KeiSeiKit installed (profile: $PROFILE)"
log "==========================================================================="
# v0.45: post-install onboarding wizard.
# Auto-triggers if stdin is a TTY (real terminal). Wizard itself re-checks
# and exits cleanly if non-interactive — so curl|bash one-liner runs work too.
# v0.48: post-install onboarding wizard.
# stdin already reattached to /dev/tty above (when present), so [ -t 0 ]
# inside this scope correctly reports interactive vs headless. Wizard
# itself re-checks and exits cleanly if non-interactive.
ONBOARD_SH="$HOME/.claude/scripts/kei-onboard.sh"
if [ -x "$ONBOARD_SH" ] && [ -t 0 ] && [ "${KEI_NO_ONBOARD:-0}" != "1" ]; then
log ""
@ -279,14 +293,15 @@ log " - Run kei-doctor for a full health diagnostic."
log " - For cortex profile: run /cortex-setup inside Claude Code."
log " - For sleep layer: run /sleep-setup inside Claude Code."
# v0.47: offer to launch `kei` for a first status look.
# Gate on stdin TTY only (rule: tty-interactivity-gate.md) — `-t 1` would
# falsely skip under curl|bash because the bootstrap log tees stdout.
# v0.48: offer to launch `kei` for a first status look.
# stdin was reattached to /dev/tty above (when present), so [ -t 0 ] is
# now true under curl|bash too. Simple gate works correctly.
KEI_BIN_PATH="$HOME/.claude/bin/kei"
if [ -x "$KEI_BIN_PATH" ] && [ -t 0 ] && [ "${KEI_NO_AUTORUN:-0}" != "1" ]; then
log ""
printf ' → Запустить kei сейчас? [Y/n] ' >&2
read -r _reply </dev/tty || _reply=""
printf ' → Запустить kei сейчас? [Y/n] '
_reply=""
read -r _reply || _reply=""
case "${_reply:-Y}" in
[Nn]*)
log " (skipped — run 'kei' anytime to see substrate status)"

View file

@ -3,7 +3,7 @@
"name": "keisei",
"displayName": "KeiSei",
"description": "Constructor Pattern multi-LLM agent substrate — 38 agents, 69 skills, 54 hooks, 86 blocks. Cross-CLI policy enforcement (Claude/Grok/Copilot/Agy/Kimi) via kei-mcp + kei_bash/kei_edit/kei_write. Rust primitives via classic ./install.sh.",
"version": "0.47.0",
"version": "0.48.0",
"homepage": "https://keisei.app",
"repository": "https://github.com/KeiSeiLab/KeiSeiKit-1.0.git",
"author": {