From dbe4844d22840b64ff5e0ad0f03f1660ed050996 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 27 May 2026 07:46:16 -0700 Subject: [PATCH] feat(setup): install plan-tune cathedral hooks with explicit consent UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan-tune cathedral T8. Wires the new PostToolUse capture hook and PreToolUse enforcement hook into ~/.claude/settings.json via the schema-aware gstack-settings-hook (T3) — respecting D4's "never mutate settings.json silently" boundary and the Codex outside-voice warning. Behavior at setup time: - Idempotency: if list-sources already shows 'plan-tune-cathedral', no-op with a one-line note. - Marker present (previously declined): no-op, no re-prompt. - Interactive terminal: print rationale + diff preview from settings-hook, rollback command, and prompt y/N. On accept, register both hooks (PostToolUse and PreToolUse) with --source plan-tune-cathedral. On decline, touch ~/.gstack/.plan-tune-hooks-prompted so we don't re-ask. - Non-interactive (CI / scripted): no prompt; print the two exact commands the user would need to install manually. - --no-team teardown also removes the plan-tune hooks via remove-source. gstack-uninstall extended to clean up plan-tune-cathedral hooks alongside the existing SessionStart cleanup. Listed as a separate "plan-tune cathedral hooks" line in the REMOVED summary when it fires. No new test file — coverage from T3's gstack-settings-hook-schema-aware tests proves the underlying bin behavior; setup-level integration is verified manually (re-running ./setup is cheap and the prompt makes it obvious whether install happened). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/gstack-uninstall | 4 ++ setup | 97 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/bin/gstack-uninstall b/bin/gstack-uninstall index 4f7b0fc1e..17d7d30bc 100755 --- a/bin/gstack-uninstall +++ b/bin/gstack-uninstall @@ -232,6 +232,10 @@ SETTINGS_HOOK="$(dirname "$0")/gstack-settings-hook" SESSION_UPDATE="$(dirname "$0")/gstack-session-update" if [ -x "$SETTINGS_HOOK" ]; then "$SETTINGS_HOOK" remove "$SESSION_UPDATE" 2>/dev/null && REMOVED+=("SessionStart hook") || true + # Cathedral T8 cleanup: also remove plan-tune PreToolUse + PostToolUse hooks. + if "$SETTINGS_HOOK" remove-source --source plan-tune-cathedral 2>/dev/null | grep -q "removed [1-9]"; then + REMOVED+=("plan-tune cathedral hooks") + fi fi # ─── Remove global state ──────────────────────────────────── diff --git a/setup b/setup index 163865731..a9ab892c8 100755 --- a/setup +++ b/setup @@ -1150,3 +1150,100 @@ if [ "$NO_TEAM_MODE" -eq 1 ]; then log "Team mode disabled: auto-update hook removed." fi + +# 11. Plan-tune cathedral hook install (T8). +# +# Registers PostToolUse (deterministic AUQ capture) + PreToolUse (preference +# enforcement) hooks in ~/.claude/settings.json so /plan-tune actually does +# something at runtime instead of being agent-convention. Explicit consent UX +# per D4 + Codex: never mutate settings.json silently. +# +# Idempotent via _gstack_source tag = 'plan-tune-cathedral'. If both hooks +# already registered under that tag, the install is a no-op (no prompt). +PLAN_TUNE_LOG_HOOK="$SOURCE_GSTACK_DIR/hosts/claude/hooks/question-log-hook" +PLAN_TUNE_PREF_HOOK="$SOURCE_GSTACK_DIR/hosts/claude/hooks/question-preference-hook" +PLAN_TUNE_INSTALL_MARKER="$HOME/.gstack/.plan-tune-hooks-prompted" + +if [ "$NO_TEAM_MODE" -ne 1 ] \ + && [ -x "$SETTINGS_HOOK" ] \ + && [ -x "$PLAN_TUNE_LOG_HOOK" ] \ + && [ -x "$PLAN_TUNE_PREF_HOOK" ]; then + + # Already installed? Check the settings.json for our source tag. + ALREADY_INSTALLED=0 + if "$SETTINGS_HOOK" list-sources 2>/dev/null | grep -q "plan-tune-cathedral"; then + ALREADY_INSTALLED=1 + fi + + if [ "$ALREADY_INSTALLED" -eq 1 ]; then + log "" + log "Plan-tune hooks already installed. Run \`$SETTINGS_HOOK list-sources\` to inspect." + 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. + log "" + log "──────────────────────────────────────────────────────────" + log "Plan-tune cathedral: install Claude Code hooks?" + log "──────────────────────────────────────────────────────────" + log "" + log "These hooks make /plan-tune settings actually bind at runtime:" + log " • PostToolUse hook captures every AskUserQuestion fire (no agent" + log " compliance required). Today it's agent-convention and the log" + log " is empty in dogfood." + log " • PreToolUse hook enforces 'never-ask' preferences via Claude Code's" + log " permissionDecision protocol. Today preferences are agent-honored" + log " convention; this makes them binding." + log "" + log "Diff preview (PostToolUse capture hook):" + "$SETTINGS_HOOK" diff-event \ + --event PostToolUse \ + --matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \ + --command "$PLAN_TUNE_LOG_HOOK" \ + --source plan-tune-cathedral \ + --timeout 5 2>/dev/null || true + log "" + log "Backup: settings.json.bak. 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" + else + # Non-interactive (CI, scripted setup). Don't prompt; print one-liner. + log "" + log "Plan-tune cathedral hooks not installed (non-interactive setup)." + log "Install with:" + log " $SETTINGS_HOOK add-event --event PostToolUse \\" + log " --matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \\" + log " --command $PLAN_TUNE_LOG_HOOK --source plan-tune-cathedral --timeout 5" + log " $SETTINGS_HOOK add-event --event PreToolUse \\" + log " --matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \\" + log " --command $PLAN_TUNE_PREF_HOOK --source plan-tune-cathedral --timeout 5" + fi +fi + +# Also tear down plan-tune hooks on --no-team (matches the existing pattern). +if [ "$NO_TEAM_MODE" -eq 1 ] && [ -x "$SETTINGS_HOOK" ]; then + "$SETTINGS_HOOK" remove-source --source plan-tune-cathedral 2>/dev/null || true +fi