#!/bin/bash
# UserPromptSubmit hook — per-turn reinforcement for the Cabinet Register
# output style (act:9a0af01a).
#
# WHY THIS EXISTS. The docs say: "All output styles trigger reminders for
# Claude to adhere to the output style instructions during the conversation."
# THAT IS FALSE FOR CUSTOM STYLES, and the docs are wrong. Verified by
# controlled experiment (two isolated temp projects, headless `claude -p`, two
# turns each, one variable):
#
#   built-in `Explanatory` → reminder PRESENT ("Explanatory output style is
#                            active. Remember to follow the specific
#                            guidelines for this style.")
#   custom   `CC Probe`    → ABSENT. None.
#
# The mechanism is visible in the shipped 2.1.209 binary:
#   output_style: (e) => { let t = Lle[e.style]; if (!t) return []; ... }
# where `Lle` is the BUILT-INS-ONLY registry (default/Proactive/Explanatory/
# Learning). A custom style isn't in it, so the reminder generator returns
# nothing, and `turn-reminder` does not exist as frontmatter (0 occurrences) —
# a custom style cannot supply its own.
#
# So: Anthropic ships a per-turn reminder for its OWN styles. That is the
# platform's own concession that system-prompt placement alone does not hold a
# register. Custom styles don't get it. This hook supplies it. Deleting this
# "because the style already covers it" reintroduces exactly the decay this
# whole thing exists to fix (caveman issue #175: "Cavemans lasts for like 4
# msgs" — the same discovery, made the hard way).
#
# A POINTER, NOT A RESTATEMENT. caveman must restate its entire ruleset every
# turn because its rules live in a decaying conversation layer. Ours live in
# the system prompt, re-sent in full on every request — so this line's job is
# ATTENTION, not memory. One line. It fires on every turn forever, which makes
# it the most expensive-by-repetition thing CC ships; keep it to one line.
#
# SILENT UNLESS LIVE. Emits nothing when outputStyle is absent or names a
# foreign style. Never inject a reminder for a register that isn't active.
#
# FAIL-OPEN, ALWAYS EXIT 0. A UserPromptSubmit hook that errors is a spurious
# failure on every single turn. Missing python3, malformed stdin, unreadable
# settings, broken pipe — all pass through silently.

# Ignore SIGPIPE. If our reader hangs up before we finish writing, the default
# disposition kills this script and it exits 141 — a "failed hook" on a turn
# where nothing actually went wrong. Ignoring it turns that into an EPIPE write
# error we swallow below, so the exit stays 0.
trap '' PIPE

# Consume stdin unconditionally (a hook that doesn't read its payload can
# SIGPIPE the caller). Never fail on a malformed or empty payload.
PAYLOAD=$(cat 2>/dev/null)

OUT=$(CC_HOOK_PAYLOAD="$PAYLOAD" python3 <<'PYEOF' 2>/dev/null
import json, os, sys

# The style's identity. Claude Code keys a custom style by its FRONTMATTER
# `name`, verbatim (verified in 2.1.209), so this string must stay in step
# with templates/output-styles/cabinet-register.md and with
# REGISTER_STYLE_NAME in lib/output-register.js. The test suite asserts all
# three agree — drift here means the hook goes silent while looking fine.
STYLE_NAME = "Cabinet Register"

LINE = (
    "Cabinet Register active — plain English, concept first, stakes first. "
    "Name projects and actions in words, never by id alone. "
    "Paths, ids, and code stay in the artifact, not the prose."
)

try:
    raw = os.environ.get("CC_HOOK_PAYLOAD", "")
    try:
        data = json.loads(raw) if raw.strip() else {}
    except Exception:
        data = {}          # malformed stdin is not a reason to fail a turn
    if not isinstance(data, dict):
        data = {}

    cwd = data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()

    # Same layers, same order as lib/output-register.js's resolveRegisterLevel
    # and as the binary's own precedence array ("Ordered low-to-high priority
    # — later entries override earlier ones"). We can read only these three;
    # CLI --settings and managed policy outrank them and are invisible here.
    # That is acceptable for a hook whose only failure mode is staying quiet.
    layers = [
        os.path.join(os.path.expanduser("~"), ".claude", "settings.json"),
        os.path.join(cwd, ".claude", "settings.json"),
        os.path.join(cwd, ".claude", "settings.local.json"),
    ]

    active = None
    for path in layers:
        try:
            with open(path, "r") as fh:
                parsed = json.load(fh)
        except Exception:
            continue       # absent or unreadable → this layer says nothing
        if not isinstance(parsed, dict):
            continue
        value = parsed.get("outputStyle")
        if isinstance(value, str) and value.strip():
            # act:9bf4411d — compare the RAW value, never a trimmed one.
            # Claude Code resolves a custom style by EXACT lookup on the
            # settings string. A padded "  Cabinet Register  " therefore
            # matches no style and loads NOTHING. Trimming here made the hook
            # announce "Cabinet Register active" for a register that never
            # landed — violating this file's own SILENT UNLESS LIVE invariant
            # above, and contradicting resolveRegisterLevel (which compares
            # raw) so the two halves of the module disagreed about reality.
            # The emptiness test still strips, because "   " is absence.
            active = value

    if active != STYLE_NAME:
        sys.exit(0)        # off, foreign, or unloadable padding → say NOTHING

    sys.stdout.write(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": LINE,
        }
    }))
except Exception:
    sys.exit(0)
PYEOF
)

# Emit only a non-empty payload. Anything else — python3 absent, a throw, a
# foreign style — falls through to a bare exit 0. The write itself is allowed
# to fail (see the PIPE trap above); a reader that hung up is not our problem.
if [ -n "$OUT" ]; then
  printf '%s' "$OUT" 2>/dev/null || true
fi

exit 0
