mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
fix(setup): hooks register the global-install path and re-point stale ones
Registering hooks from a dev worktree baked that worktree's absolute path into settings.json — deleting the worktree left a dead hook erroring on every session stop, and the presence-only dedup (list-sources | grep) could never re-point it. setup's hook paths now route through _hook_install_path (global install preferred, source dir fallback), and the new ensure-event verb on gstack-settings-hook compares the registered command payload against canonical: identical → no write, different → single atomic replacement (never zero or two registrations). The plan-tune hooks had the same stale pattern and get the same fix without re-triggering their consent prompt. Also hardened: bun 1.3.13 turns an uncaught sync fs error in bun -e into a SILENT exit 0 — the registrar's write path now catches, prints, and exits 1, so a failed update can never report fake-green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bdc0b11664
commit
c9b5ccddcf
@@ -1978,6 +1978,23 @@ if [ -x "$DETECT_BIN" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Hook commands registered into ~/.claude/settings.json must survive deletion
|
||||
# of the directory setup ran from. A dev-worktree setup used to bake
|
||||
# $SOURCE_GSTACK_DIR's absolute path into the registration; deleting that
|
||||
# worktree left a dead hook erroring on every trigger. Prefer the global
|
||||
# install (~/.claude/skills/gstack — a persistent checkout, or a stable
|
||||
# symlink) and fall back to the setup-time source tree only when no global
|
||||
# install exists yet (first install from a fresh clone).
|
||||
_hook_install_path() {
|
||||
local rel="$1"
|
||||
local global_hook="$HOME/.claude/skills/gstack/$rel"
|
||||
if [ -x "$global_hook" ]; then
|
||||
printf '%s' "$global_hook"
|
||||
else
|
||||
printf '%s' "$SOURCE_GSTACK_DIR/$rel"
|
||||
fi
|
||||
}
|
||||
|
||||
# 11. Plan-tune cathedral hook install (T8).
|
||||
#
|
||||
# Registers PostToolUse (deterministic AUQ capture) + PreToolUse (preference
|
||||
@@ -1986,10 +2003,12 @@ fi
|
||||
# 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"
|
||||
AUQ_ERROR_FALLBACK_HOOK="$SOURCE_GSTACK_DIR/hosts/claude/hooks/auq-error-fallback-hook"
|
||||
# already registered under that tag, the install skips the consent prompt and
|
||||
# only refreshes the registered command paths in place (ensure-event is a
|
||||
# no-op when they already match — see the stale-path note above).
|
||||
PLAN_TUNE_LOG_HOOK="$(_hook_install_path hosts/claude/hooks/question-log-hook)"
|
||||
PLAN_TUNE_PREF_HOOK="$(_hook_install_path hosts/claude/hooks/question-preference-hook)"
|
||||
AUQ_ERROR_FALLBACK_HOOK="$(_hook_install_path hosts/claude/hooks/auq-error-fallback-hook)"
|
||||
PLAN_TUNE_INSTALL_MARKER="$HOME/.gstack/.plan-tune-hooks-prompted"
|
||||
|
||||
if [ "$NO_TEAM_MODE" -ne 1 ] \
|
||||
@@ -2040,13 +2059,16 @@ if [ "$NO_TEAM_MODE" -ne 1 ] \
|
||||
fi
|
||||
|
||||
_install_plan_tune_hooks() {
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
# ensure-event (not add-event): registers when missing, RE-POINTS a stale
|
||||
# command path in place when the registration differs, and is a true no-op
|
||||
# (no write, no backup churn) when it already matches.
|
||||
"$SETTINGS_HOOK" ensure-event \
|
||||
--event PostToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$PLAN_TUNE_LOG_HOOK" \
|
||||
--source plan-tune-cathedral \
|
||||
--timeout 5
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
"$SETTINGS_HOOK" ensure-event \
|
||||
--event PreToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$PLAN_TUNE_PREF_HOOK" \
|
||||
@@ -2060,7 +2082,7 @@ if [ "$NO_TEAM_MODE" -ne 1 ] \
|
||||
# question-log capture hook (same event+matcher). A distinct source = a second
|
||||
# PostToolUse entry; both run in parallel.
|
||||
if [ -x "$AUQ_ERROR_FALLBACK_HOOK" ]; then
|
||||
"$SETTINGS_HOOK" add-event \
|
||||
"$SETTINGS_HOOK" ensure-event \
|
||||
--event PostToolUse \
|
||||
--matcher '(AskUserQuestion|mcp__.*__AskUserQuestion)' \
|
||||
--command "$AUQ_ERROR_FALLBACK_HOOK" \
|
||||
@@ -2070,6 +2092,10 @@ if [ "$NO_TEAM_MODE" -ne 1 ] \
|
||||
}
|
||||
|
||||
if [ "$ALREADY_INSTALLED" -eq 1 ]; then
|
||||
# Consent already recorded — no prompt. But a registration from an earlier
|
||||
# setup may carry a stale absolute path (a since-deleted dev worktree);
|
||||
# ensure-event re-points it in place and no-ops when everything matches.
|
||||
_install_plan_tune_hooks >/dev/null 2>&1 || true
|
||||
log ""
|
||||
log "Plan-tune hooks already installed. Run \`$SETTINGS_HOOK list-sources\` to inspect."
|
||||
elif [ "$PT_DECISION" = "yes" ]; then
|
||||
@@ -2162,18 +2188,33 @@ fi
|
||||
# unenforceable — interrupted sessions leaked started > completed forever.
|
||||
# Register a Stop-event hook that closes dangling entries. FAIL-OPEN contract
|
||||
# (F5): the hook always exits 0 and repairs best-effort — it can never block
|
||||
# a session. Idempotent via the (event, source) dedup in gstack-settings-hook;
|
||||
# removed by --no-team and gstack-uninstall.
|
||||
TIMELINE_STOP_HOOK="$SOURCE_GSTACK_DIR/hosts/claude/hooks/timeline-stop-hook"
|
||||
# a session. Removed by --no-team and gstack-uninstall.
|
||||
#
|
||||
# The command path prefers the global install (see _hook_install_path): a
|
||||
# dev-worktree setup used to bake its own absolute dir into settings.json, so
|
||||
# deleting the worktree left a dead hook erroring on every session stop — and
|
||||
# the old presence-only dedup (list-sources | grep) never re-pointed it on a
|
||||
# re-run. ensure-event registers when missing, replaces a stale path in place
|
||||
# (one atomic write — never zero or two registrations), and no-ops when the
|
||||
# registration already matches.
|
||||
TIMELINE_STOP_HOOK="$(_hook_install_path hosts/claude/hooks/timeline-stop-hook)"
|
||||
if [ "$NO_TEAM_MODE" -ne 1 ] && [ -x "$SETTINGS_HOOK" ] && [ -x "$TIMELINE_STOP_HOOK" ]; then
|
||||
if ! "$SETTINGS_HOOK" list-sources 2>/dev/null | grep -q "gstack-timeline-stop"; then
|
||||
if "$SETTINGS_HOOK" add-event \
|
||||
--event Stop \
|
||||
--command "$TIMELINE_STOP_HOOK" \
|
||||
--source gstack-timeline-stop \
|
||||
--timeout 5 >/dev/null 2>&1; then
|
||||
log " registered Stop hook: session timeline entries now close even when a skill is interrupted (backup: settings.json.bak.<ts>; remove: $SETTINGS_HOOK remove-source --source gstack-timeline-stop)"
|
||||
fi
|
||||
if _TL_ENSURE_OUT=$("$SETTINGS_HOOK" ensure-event \
|
||||
--event Stop \
|
||||
--command "$TIMELINE_STOP_HOOK" \
|
||||
--source gstack-timeline-stop \
|
||||
--timeout 5 2>/dev/null); then
|
||||
case "$_TL_ENSURE_OUT" in
|
||||
*unchanged*)
|
||||
: # already registered with the canonical command — quiet no-op
|
||||
;;
|
||||
*re-pointed*)
|
||||
log " re-pointed Stop hook to $TIMELINE_STOP_HOOK (previous registration held a stale path)"
|
||||
;;
|
||||
*)
|
||||
log " registered Stop hook: session timeline entries now close even when a skill is interrupted (backup: settings.json.bak.<ts>; remove: $SETTINGS_HOOK remove-source --source gstack-timeline-stop)"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user