mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-02 16:21:38 +02:00
v1.53.1.0 fix: non-interactive-safe plan-tune hook install (flags + smart defaults) (#1805)
* feat(config): add plan_tune_hooks setting (prompt|yes|no) Registers a new gstack-config key controlling whether ./setup installs the plan-tune Claude Code hooks. Default "prompt". Documented in the config header and surfaced in `gstack-config defaults` / `list`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(setup): make plan-tune hook install non-interactive-safe The plan-tune consent prompt used a blocking `read -r` with no timeout. Under a forwarded/automated TTY (conductor workspace setup, CI with a pty) it hung setup forever. Move the decision into flags + env + saved config with a smart default: --plan-tune-hooks / --no-plan-tune-hooks / --plan-tune-hooks=yes|no|prompt > GSTACK_PLAN_TUNE_HOOKS env > plan_tune_hooks config > prompt-on-real-TTY. Explicit yes/no act non-interactively. The remaining interactive branch is gated on a real (non-quiet) TTY and uses a time-bounded `read -t 10 </dev/tty` that defaults to skip, so it can never hang. A timeout no longer persists a decline marker, so a later hands-on run can still offer the install. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dev-setup): run setup non-interactively in dev/workspace mode Conductor runs bin/dev-setup under a forwarded pty, so any setup prompt (skill-prefix, plan-tune consent) would hang the workspace. Detach stdin (`setup </dev/null`) so every prompt takes its smart non-interactive default: flat skill names, skip the global plan-tune hook install without writing a decline marker. Saved prefix/config preferences are still honored, and a dev workspace no longer silently mutates ~/.claude/settings.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(setup): guard plan-tune hooks stay non-interactive Static + binary-level regression test (free, <1s): asserts the flags are wired, the plan-tune read is time-bounded (no bare blocking read), explicit yes/no decisions short-circuit before the prompt, and gstack-config knows the plan_tune_hooks key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(setup,config): harden plan-tune decision against bad input Review follow-ups to the non-interactive plan-tune work: - setup now lowercases + whitespace-strips the resolved decision before the case match, so an explicit opt-in via flag/env ("YES", "Yes", " yes") is honored instead of silently falling through to "prompt"/skip. Also accepts on/off and 1/0. - gstack-config rejects out-of-domain plan_tune_hooks values (anything but prompt|yes|no) with a warning + fallback to prompt, matching the existing value-whitelist pattern for explain_level / artifacts_sync_mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dev-setup): never mutate global hooks during workspace setup Closing stdin alone only suppresses the prompt branch; a saved `plan_tune_hooks: yes` or exported GSTACK_PLAN_TUNE_HOOKS=yes would still resolve to "install" and rewrite the user's global ~/.claude/settings.json to point at THIS ephemeral worktree — which breaks once the workspace is deleted. Pass --plan-tune-hooks=prompt (highest precedence) so dev-setup pins resolution to prompt-mode; with stdin closed that is a guaranteed no-op skip (no install, no decline marker). To install the hooks, run ./setup --plan-tune-hooks directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(setup): isolate config tests from host + cover new guards - Point gstack-config tests at a temp GSTACK_HOME so `get plan_tune_hooks` reads the built-in default, not whatever the host machine has in ~/.gstack/config.yaml (the prior test was non-deterministic). - Add behavioral coverage: yes/no/prompt round-trip, out-of-domain rejection. - Add a normalization guard (decision input is lowercased/trimmed) and a dev-setup guard (runs setup with --plan-tune-hooks=prompt + stdin detached). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: rebaseline parity-suite v1.44.1 -> v1.53.0.0 The frozen v1.44.1 anchor went stale: five planning skills (plan-ceo-review, plan-eng-review, plan-design-review, investigate, office-hours) crept past the 1.05x ceiling via legitimate v1.49-v1.53 growth (brain-aware planning + the v1.53 redaction guard), so `bun test` was red on a clean checkout of main. Capture a fresh baseline at HEAD (bun run scripts/capture-baseline.ts --tag v1.53.0.0) and re-point the test at it. The per-skill 1.05 ratio is kept, so future bloat is still caught; only the anchor moved. Mirrors the earlier skill-size-budget rebase (v1.44.1 -> v1.47.0.0). Historical v1.44.1 / v1.46.0.0 / v1.47.0.0 baselines are retained for the v1->v2 audit trail. The captured skill bytes equal origin/main exactly (this branch left every SKILL.md untouched). Clears the pre-existing failures noted in the v1.53.0.0 CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(plan-tune): de-flake "derive pushes scope_appetite up" The test was ~25-50% flaky (worse on main). gstack-question-log fires a fire-and-forget background `--derive` after every write; the 5 rapid log writes spawned 5 racing background derives that collided with the test's explicit --derive — a late one that only saw 3 entries could clobber developer-profile.json after the explicit one wrote sample_size=5. Set GSTACK_QUESTION_LOG_NO_DERIVE=1 (the flag the binary documents for exactly this case) so the writes don't spawn background derives. The explicit --derive still runs, so real derive behavior is still asserted. 20/20 green after. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.53.1.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document non-interactive dev-setup + plan-tune hook flags (v1.53.1.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,7 @@ SKILL_PREFIX=1
|
||||
SKILL_PREFIX_FLAG=0
|
||||
TEAM_MODE=0
|
||||
NO_TEAM_MODE=0
|
||||
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
|
||||
@@ -91,6 +92,9 @@ while [ $# -gt 0 ]; do
|
||||
--no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;;
|
||||
--team) TEAM_MODE=1; shift ;;
|
||||
--no-team) NO_TEAM_MODE=1; shift ;;
|
||||
--plan-tune-hooks) PLAN_TUNE_HOOKS_MODE="yes"; shift ;;
|
||||
--no-plan-tune-hooks) PLAN_TUNE_HOOKS_MODE="no"; shift ;;
|
||||
--plan-tune-hooks=*) PLAN_TUNE_HOOKS_MODE="${1#--plan-tune-hooks=}"; shift ;;
|
||||
-q|--quiet) QUIET=1; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
@@ -1304,14 +1308,65 @@ if [ "$NO_TEAM_MODE" -ne 1 ] \
|
||||
ALREADY_INSTALLED=1
|
||||
fi
|
||||
|
||||
# Resolve the desired action without ever blocking.
|
||||
# Priority: CLI flag (--plan-tune-hooks / --no-plan-tune-hooks)
|
||||
# > env (GSTACK_PLAN_TUNE_HOOKS=yes|no)
|
||||
# > saved config (plan_tune_hooks)
|
||||
# > smart default ("prompt" → timed prompt on a real TTY, else skip).
|
||||
# This guarantees scripted/workspace setups (conductor, CI) are never
|
||||
# interactive: pass --no-plan-tune-hooks (or --plan-tune-hooks) and the
|
||||
# block runs to completion with no `read`.
|
||||
PT_DECISION="$PLAN_TUNE_HOOKS_MODE"
|
||||
[ -z "$PT_DECISION" ] && PT_DECISION="${GSTACK_PLAN_TUNE_HOOKS:-}"
|
||||
[ -z "$PT_DECISION" ] && PT_DECISION="$("$GSTACK_CONFIG" get plan_tune_hooks 2>/dev/null || true)"
|
||||
# Normalize: strip whitespace + lowercase so "YES", "Yes", " yes" from a flag
|
||||
# or env var all resolve correctly (an unrecognized opt-in must NOT silently
|
||||
# downgrade to skip). Unknown values fall through to "prompt".
|
||||
PT_DECISION=$(printf '%s' "$PT_DECISION" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
|
||||
case "$PT_DECISION" in
|
||||
y|yes|true|install|on|1) PT_DECISION="yes" ;;
|
||||
n|no|false|skip|off|0) PT_DECISION="no" ;;
|
||||
*) PT_DECISION="prompt" ;;
|
||||
esac
|
||||
|
||||
_install_plan_tune_hooks() {
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
--event PostToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$PLAN_TUNE_LOG_HOOK" \
|
||||
--source plan-tune-cathedral \
|
||||
--timeout 5
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
--event PreToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$PLAN_TUNE_PREF_HOOK" \
|
||||
--source plan-tune-cathedral \
|
||||
--timeout 5
|
||||
}
|
||||
|
||||
if [ "$ALREADY_INSTALLED" -eq 1 ]; then
|
||||
log ""
|
||||
log "Plan-tune hooks already installed. Run \`$SETTINGS_HOOK list-sources\` to inspect."
|
||||
elif [ "$PT_DECISION" = "yes" ]; then
|
||||
# Explicit opt-in (flag / env / config). Non-interactive.
|
||||
_install_plan_tune_hooks
|
||||
log ""
|
||||
log "Plan-tune hooks installed. Run /plan-tune anytime to inspect."
|
||||
touch "$PLAN_TUNE_INSTALL_MARKER"
|
||||
elif [ "$PT_DECISION" = "no" ]; then
|
||||
# Explicit opt-out (flag / env / config). Non-interactive.
|
||||
log ""
|
||||
log "Plan-tune cathedral hooks not installed (opted out)."
|
||||
log "Install later with: ./setup --plan-tune-hooks (or /update-config)."
|
||||
touch "$PLAN_TUNE_INSTALL_MARKER"
|
||||
elif [ -f "$PLAN_TUNE_INSTALL_MARKER" ]; then
|
||||
# Previously declined. Don't re-ask. User can re-enable via /update-config.
|
||||
:
|
||||
elif [ -t 0 ] && [ -t 1 ]; then
|
||||
# Interactive install with explicit consent + diff preview.
|
||||
elif [ "$QUIET" -ne 1 ] && [ -t 0 ] && [ -t 1 ]; then
|
||||
# Real interactive terminal with no recorded preference: ask, with explicit
|
||||
# consent + diff preview. The read is time-bounded and defaults to "skip" so
|
||||
# it can never hang an automated/forwarded TTY (the conductor failure mode).
|
||||
_PT_PROMPT_TIMEOUT=10 # single source of truth for the read + the countdown text
|
||||
log ""
|
||||
log "──────────────────────────────────────────────────────────"
|
||||
log "Plan-tune cathedral: install Claude Code hooks?"
|
||||
@@ -1336,33 +1391,32 @@ if [ "$NO_TEAM_MODE" -ne 1 ] \
|
||||
log "Backup: settings.json.bak.<ts> written before any mutation."
|
||||
log "Rollback: $SETTINGS_HOOK rollback"
|
||||
log ""
|
||||
printf "Install both hooks now? [y/N] "
|
||||
read -r PLAN_TUNE_INSTALL_REPLY
|
||||
if [ "$PLAN_TUNE_INSTALL_REPLY" = "y" ] || [ "$PLAN_TUNE_INSTALL_REPLY" = "Y" ]; then
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
--event PostToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$PLAN_TUNE_LOG_HOOK" \
|
||||
--source plan-tune-cathedral \
|
||||
--timeout 5
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
--event PreToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$PLAN_TUNE_PREF_HOOK" \
|
||||
--source plan-tune-cathedral \
|
||||
--timeout 5
|
||||
log ""
|
||||
log "Plan-tune hooks installed. Run /plan-tune anytime to inspect."
|
||||
else
|
||||
log ""
|
||||
log "Skipped. Re-run ./setup or use /update-config to install later."
|
||||
fi
|
||||
touch "$PLAN_TUNE_INSTALL_MARKER"
|
||||
printf "Install both hooks now? [y/N] (default: N, auto-skips in %ss): " "$_PT_PROMPT_TIMEOUT"
|
||||
read -t "$_PT_PROMPT_TIMEOUT" -r PLAN_TUNE_INSTALL_REPLY </dev/tty 2>/dev/null || PLAN_TUNE_INSTALL_REPLY=""
|
||||
case "$PLAN_TUNE_INSTALL_REPLY" in
|
||||
y|Y)
|
||||
_install_plan_tune_hooks
|
||||
log ""
|
||||
log "Plan-tune hooks installed. Run /plan-tune anytime to inspect."
|
||||
touch "$PLAN_TUNE_INSTALL_MARKER"
|
||||
;;
|
||||
n|N)
|
||||
log ""
|
||||
log "Skipped. Re-run ./setup --plan-tune-hooks or use /update-config to install later."
|
||||
touch "$PLAN_TUNE_INSTALL_MARKER"
|
||||
;;
|
||||
*)
|
||||
# Empty / timed out — treat as "ask me again" (don't persist a decline).
|
||||
log ""
|
||||
log "No response — skipped for now. Re-run ./setup --plan-tune-hooks to install."
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Non-interactive (CI, scripted setup). Don't prompt; print one-liner.
|
||||
# Non-interactive (CI, scripted/workspace setup, quiet). Never prompt.
|
||||
log ""
|
||||
log "Plan-tune cathedral hooks not installed (non-interactive setup)."
|
||||
log "Install with:"
|
||||
log "Install with: ./setup --plan-tune-hooks"
|
||||
log " (or set GSTACK_PLAN_TUNE_HOOKS=yes, or run the commands below)"
|
||||
log " $SETTINGS_HOOK add-event --event PostToolUse \\"
|
||||
log " --matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \\"
|
||||
log " --command $PLAN_TUNE_LOG_HOOK --source plan-tune-cathedral --timeout 5"
|
||||
|
||||
Reference in New Issue
Block a user