#!/usr/bin/env bash
# UserPromptSubmit hook: inject the standing prompt-optimiser directive plus the
# per-turn four-rung routing ladder as additionalContext on every turn, so the
# agent restates each non-trivial prompt and routes it before acting. Mirrors
# doctrine.sh: emit hookSpecificOutput.additionalContext via python3, fail-open
# with exit 0.
#
# The ladder ranks, in order: (1) delegate to a specialist that owns the
# deliverable via the Agent tool; (2) only if none fits, load an admin-usable
# skill; (3) only if the deliverable recurs and no specialist or skill owns it,
# author its owning skill via skill-builder (or agent-builder) and produce
# through it; (4) only if it is a genuine one-off, author inline as the last
# resort. The full agent roster and admin-usable skills list are re-emitted every
# turn by reading two generated files from the account dir; the hook never walks
# the plugins tree per turn (Task 698).
#
# Compliance is behavioural, not deterministic. A hook cannot force a skill call
# or a dispatch; the directive steers the agent, it does not gate the turn. The
# raw prompt always still reaches the model (UserPromptSubmit output is ADDED to
# the prompt, never substituted).
#
# Input  (stdin, UserPromptSubmit contract): { "prompt": "...", ... } — drained
#         and ignored; the directive is standing and prompt-independent.
# Output (stdout JSON):
#   { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
#                             "additionalContext": "<directive+ladder>" } }
#
# The two lists are read from <cwd>/agents/admin/ — the hook fires with the
# account dir as cwd (the same resolution admin-authoring-observer.sh relies on).
# Fail-open is VISIBLE: a missing list logs `[prompt-optimiser] missing=<file>
# emitting-partial` to stderr and the ladder still injects with whatever resolves.
# If python3 is unavailable or JSON encoding fails, exit 0 with no stdout. The
# hook must never block or corrupt a turn.

set -uo pipefail

# Capture stdin: the directive does not depend on the prompt text, but the
# UserPromptSubmit payload carries session_id, used for the durable breadcrumb.
HOOK_INPUT=$(cat 2>/dev/null || true)

# Fail-open if the JSON encoder is unavailable.
command -v python3 >/dev/null 2>&1 || exit 0

# Task 753 — suppress the directive on a native channel turn. A channel inbound
# arrives as a `<channel source="..." ...>` event; the channel service already
# reframes that event text into a select-and-dispatch instruction, so injecting
# the standing routing ladder on top is redundant. The marker is matched on the
# parsed `.prompt` field (not raw stdin) and only at the start of the prompt, so
# a normal admin prompt that merely mentions "channel" is never suppressed.
# Fail-open: if the prompt cannot be parsed, do NOT suppress (inject as usual).
IS_CHANNEL_TURN=$(printf '%s' "$HOOK_INPUT" | python3 -c '
import json, sys
try:
    p = json.load(sys.stdin).get("prompt", "")
except Exception:
    print("no"); sys.exit(0)
print("yes" if isinstance(p, str) and p.lstrip().startswith("<channel source=") else "no")
' 2>/dev/null || echo "no")
if [ "$IS_CHANNEL_TURN" = "yes" ]; then
  SID_C=$(printf '%s' "$HOOK_INPUT" | python3 -c 'import json,sys
try:
    print(json.load(sys.stdin).get("session_id","") or "?")
except Exception:
    print("?")' 2>/dev/null)
  echo "[prompt-optimiser-directive] skipped reason=channel-turn session=${SID_C:-?}" >&2
  exit 0
fi

# Read a generated list from the account dir (cwd). On a missing file, emit a
# visible breadcrumb to stderr and echo a one-line placeholder so the ladder
# still names the tier.
ADMIN_DIR="$PWD/agents/admin"
read_list() {  # $1 = filename under agents/admin/
  if [ -f "$ADMIN_DIR/$1" ]; then
    cat "$ADMIN_DIR/$1"
  else
    echo "[prompt-optimiser] missing=$1 emitting-partial" >&2
    echo "($1 unavailable this turn)"
  fi
}
ROSTER=$(read_list AGENTS.md)
ADMIN_SKILLS=$(read_list ADMIN-SKILLS.md)

# Build the directive from LITERAL (quoted-heredoc) static parts joined with the
# generated lists via printf. The parts are captured with `read -r -d ''` rather
# than `$(cat <<'EOF')` because bash 3.2's command-substitution parser miscounts
# the parentheses inside the ladder prose; `read` has no such limitation. The
# quoted delimiter keeps backticks and parens literal, and the lists are spliced
# in as printf data, never evaluated. `read -d ''` returns non-zero at EOF (no
# NUL terminator) but populates the variable; the `|| true` keeps that benign.
IFS= read -r -d '' DIRECTIVE_HEAD <<'DIRECTIVE_EOF' || true
PROMPT-OPTIMISER DIRECTIVE (standing)
Before acting on this turn's request, run the prompt-optimiser skill on the user's raw prompt in its in-session restatement mode, then treat the restated task as the authoritative statement of what was asked and proceed from it. Set aside your own first reading of the raw wording; the restatement governs. The raw prompt stays in context for reference only.
Skip the restatement when the prompt is a one-word confirmation, a slash-command invocation, or a direct continuation of the immediately preceding turn; restatement adds nothing in those cases.

CAPABILITY QUESTIONS ARE OWNED WORK. A question about what this platform can do or how to do it — "how do I set up X", "do you have instructions for X", any configuration or how-to about a platform feature — is answered from the owning specialist or the owning plugin's tool or reference, never from your training memory. Your prior on the platform's own features is stale relative to what shipped, so memory is the wrong source here even when it feels confident. Route the question (dispatch the owning specialist, or skill-load / call the owning plugin's tool or reference) and source the answer from what that route returns. Freestyle tier 3 is not a route for these: "there is no instruction surface for this" is itself something you establish by routing and finding nothing, not by recalling.

ROUTING LADDER (apply to the restated task, in this order):
1) Delegate to a specialist. If an installed specialist owns this deliverable's type, dispatch it with the Agent tool and the matching subagent_type. Ownership covers origination, not just the final render: you may not author the substance here and hand the specialist only the formatting, the voice pass, or the PDF step. The brief states the outcome (what is true when the specialist returns) plus the binding constraints (account, file scope, deadline, acceptance check). It never names lines, anchors, or literal text to insert; deriving those is the specialist's job. Wrong shape: naming the file, the line range, and the exact text to drop in. Right shape: "on the realagent-code site, the footer respects iOS safe-area-inset-bottom on mobile; the site's CSS is the only file in scope." Installed specialists:
DIRECTIVE_EOF

IFS= read -r -d '' DIRECTIVE_MID <<'DIRECTIVE_EOF' || true
2) Only if no specialist owns it, use an admin skill. Load one from the admin-usable list with skill-load and do the work from this seat. Admin-usable skills:
DIRECTIVE_EOF

IFS= read -r -d '' DIRECTIVE_TAIL <<'DIRECTIVE_EOF' || true
3) Only if neither a specialist nor an admin skill already owns it, decide whether the deliverable recurs. A deliverable the business issues repeatedly, or one the operator gives a fixed template, numbering, naming, or filing rule for, is authored once as its own skill: load skill-builder with skill-load (or agent-builder for a worker agent), create the owning skill, then produce the deliverable through it. This rung owns every repeatable business document or process; hand-building a recurring deliverable here is not a route.
4) Only if the deliverable is a genuine one-off, author it directly. Freestyle inline authoring is the last resort, not a peer of the routes above.
DIRECTIVE_EOF

DIRECTIVE=$(printf '%s\n%s\n%s\n%s\n%s' \
  "$DIRECTIVE_HEAD" "$ROSTER" "$DIRECTIVE_MID" "$ADMIN_SKILLS" "$DIRECTIVE_TAIL")

OUT=$(printf '%s' "$DIRECTIVE" | python3 -c '
import json, sys
ctx = sys.stdin.read()
print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": ctx,
    }
}))
' 2>/dev/null) || exit 0

[ -n "$OUT" ] || exit 0
printf '%s\n' "$OUT"

# Session id, content hash, timestamp — computed unconditionally (cheap) so the
# stored file path and the breadcrumb can reference them even when LOG_DIR is
# absent (file=-). session_id is parsed from the captured stdin; absent -> "?".
SID=$(printf '%s' "$HOOK_INPUT" | python3 -c 'import json,sys
try:
    print(json.load(sys.stdin).get("session_id","") or "")
except Exception:
    print("")' 2>/dev/null)
SID=${SID:-?}
SHA=$(printf '%s' "$DIRECTIVE" | python3 -c 'import hashlib,sys
sys.stdout.write(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())' 2>/dev/null || echo "?")
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Task 746: persist the EXACT emitted additionalContext bytes per turn, so the
# full directive is recoverable from an operator surface (the JSONL attachment
# row Claude Code keeps is truncated to <=2.6 KB; this file is not). Keyed by
# session + epoch seconds + pid -> one file per injection. Fail-open: a missing
# LOG_DIR, an unknown session, an un-creatable dir, or an unwritable file all
# skip the write and leave DIR_FILE=- ; the turn is never blocked.
DIR_FILE="-"
if [ -n "${LOG_DIR:-}" ] && [ -d "$LOG_DIR" ] && [ "$SID" != "?" ]; then
  STORE="$LOG_DIR/prompt-optimiser-directives/$SID"
  if mkdir -p "$STORE" 2>/dev/null; then
    CAND="$STORE/$(date +%s)-$$.txt"
    if printf '%s' "$DIRECTIVE" > "$CAND" 2>/dev/null; then
      DIR_FILE="$CAND"
    fi
  fi
fi

# Durable breadcrumb (Task 719 + 746): length + content hash + stored path +
# session, so "directive fired on this turn" carries enough to recover and
# verify the exact bytes. The `injected len=` and `session=` substrings are
# preserved for the existing greps/tests.
if [ -n "${LOG_DIR:-}" ] && [ -d "$LOG_DIR" ]; then
  printf '%s [prompt-optimiser-directive] injected len=%s sha256=%s file=%s session=%s\n' \
    "$TS" "${#DIRECTIVE}" "$SHA" "$DIR_FILE" "$SID" \
    >> "$LOG_DIR/prompt-optimiser-directive.log" 2>/dev/null || true
fi

echo "[prompt-optimiser-directive] injected len=${#DIRECTIVE} sha256=$SHA file=$DIR_FILE session=$SID" >&2
exit 0
