# shellcheck shell=sh
# Write gate (FR-7, AD-19/20/21/32/33). Invoked by a PreToolUse hook.
#
# Denies the FIRST write-class tool call of a session until the router has
# been consulted, then unlocks the session. Not executable by design: the
# installer never sets a mode bit, so this runs through an interpreter.
#
# AD-20: the deliberate exit status is ALWAYS 0, and never 2 -- exit 2 is a
# blocking error for PreToolUse. Every uncertainty resolves to "proceed".
# AD-33: denial requires positive knowledge, not absence of evidence.

payload_=$(cat 2>/dev/null) || exit 0
[ -n "${payload_}" ] || exit 0

field_() {
  # field_ <key> -- first JSON string value for that key, or empty.
  printf '%s' "${payload_}" \
    | LC_ALL=C tr -d '\n' \
    | LC_ALL=C sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" \
    | head -n 1
}

# A denial inside a subagent has no human reader and would spend the
# session's one denial where nobody sees it.
[ -n "$(field_ agent_id)" ] && exit 0

session_="$(field_ session_id)"
printf '%s' "${session_}" | LC_ALL=C grep -Eq '^[A-Za-z0-9._-]+$' || exit 0

transcript_="$(field_ transcript_path)"
[ -n "${transcript_}" ] || exit 0
[ -r "${transcript_}" ] || exit 0

# AD-32: one namespace, one behaviour name, one meaning -- "this
# behaviour already ran once in this session". mkdir is the atomic
# test-and-set, so exactly one caller can decide under parallel calls.
base_="${TMPDIR:-/tmp}/bestaiconfig/${session_}"
mkdir -p "${base_}" 2>/dev/null || exit 0
mkdir "${base_}/write-gate" 2>/dev/null || exit 0

# AD-33: the search space must be provably complete. A compacted or
# rotated transcript parses cleanly with the evidence gone, so treat any
# truncation marker as UNKNOWN and proceed.
LC_ALL=C grep -a -F -q -e 'compact_boundary' -e 'isCompactSummary' \
  "${transcript_}" 2>/dev/null && exit 0

# Two needles, both exact. The tool-call form, and the slash-typed form
# in its delimiters -- a bare "/skill-router" would also match the hook
# command's own path in the transcript and silently open the gate.
# Whitespace-tolerant on purpose: real transcripts are compact JSON, but
# the gate must not hinge on a serializer's spacing choice.
LC_ALL=C grep -a -E -q \
  -e '"skill"[[:space:]]*:[[:space:]]*"skill-router"' \
  -e '>/skill-router<' \
  "${transcript_}" 2>/dev/null
probe_status_=$?
# Status is read from grep directly, never after an `if`: a POSIX `if`
# whose condition fails and which has no else returns 0, which would
# turn "no router found" into "proceed" and make the gate a no-op.
[ "${probe_status_}" = "0" ] && exit 0
# 0 = found, 1 = absent, >=2 = grep error. Only 1 is knowledge (AD-33).
[ "${probe_status_}" = "1" ] || exit 0

# AD-34: every sentence addressed to a person comes from the catalogue,
# never from this script. The catalogue is an owned artifact replaced on
# every install, so wording improvements actually reach existing
# installs -- which is precisely why it is not a seed.
catalogue_="$(dirname "$0")/messages.txt"
message_() {
  # message_ <key> -- the catalogue text for that key, or nothing.
  [ -r "${catalogue_}" ] || return 0
  LC_ALL=C awk -F'\t' -v k="$1" '$1==k {print $2; exit}' "${catalogue_}" 2>/dev/null
}

reason_="$(message_ write-gate.reason.model)"
human_="$(message_ write-gate.denied.a3)"

# AD-20: if the catalogue is missing or unreadable we do NOT invent a
# sentence and we do NOT deny. An unexplained refusal is worse than a
# missed gate, and inventing text here would make this script a second
# wording source -- the exact drift AD-34 removes.
[ -n "${reason_}" ] || exit 0
[ -n "${human_}" ] || exit 0

printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"},"systemMessage":"%s"}\n' \
  "${reason_}" "${human_}"
exit 0
