vestige/hooks/synthesis-stop-validator.sh

119 lines
3.5 KiB
Bash
Raw Normal View History

2026-04-27 13:20:51 -05:00
#!/bin/bash
# synthesis-stop-validator.sh — optional Stop hook
2026-04-27 13:20:51 -05:00
#
# Blocks a narrow failure mode: a response that cites multiple memories but
# stops at summary instead of composing them into a decision. This public-safe
# version contains no private examples or local-user paths.
2026-04-27 13:20:51 -05:00
set -euo pipefail
INPUT="$(cat)"
TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')"
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
exit 0
fi
export TRANSCRIPT_PATH
PYFILE=$(mktemp -t vestige-stop-validator.XXXXXX)
trap 'rm -f "$PYFILE"' EXIT
cat > "$PYFILE" <<'PYEOF'
import json, os, re, sys
transcript = os.environ.get("TRANSCRIPT_PATH", "")
last_user = ""
last_assistant = ""
try:
with open(transcript) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except Exception:
continue
role = obj.get("role") or obj.get("type", "")
content = obj.get("message", {}).get("content", obj.get("content", ""))
text = ""
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
text += block.get("text", "") + "\n"
elif isinstance(content, str):
text = content
if role == "user":
last_user = text
elif role == "assistant":
last_assistant = text
except Exception:
sys.exit(0)
decision_re = re.compile(
r"(submit|submission|final|ship|launch|deploy|commit|decide|decision|"
r"recommend|should i|what should|purchase|buy|invest|architect|architecture|"
r"strategy|prep|prioriti|compose|tradeoff|trade-off|config|which "
r"(should|model|approach|one)|pick|choose|benchmark|competition|perform)",
2026-04-27 13:20:51 -05:00
re.IGNORECASE,
)
if not decision_re.search(last_user):
2026-04-27 13:20:51 -05:00
sys.exit(0)
memory_re = re.compile(
2026-04-27 13:20:51 -05:00
r"(memory|vestige|recall|retriev|saved memor|stored memor|prior memor|"
r"fsrs|trust score|deep_reference|smart_ingest)",
re.IGNORECASE,
)
if not memory_re.search(last_assistant):
2026-04-27 13:20:51 -05:00
sys.exit(0)
summary_patterns = [
r"memory\s+[a-f0-9]{4,}",
2026-04-27 13:20:51 -05:00
r"saved memory",
r"according to memory",
r"the memory (says|states|notes|indicates)",
r"per memory",
r"memories? (say|says|state|note|indicate)",
]
summary_hits = 0
for pat in summary_patterns:
2026-04-27 13:20:51 -05:00
summary_hits += len(re.findall(pat, last_assistant, re.IGNORECASE))
composition_re = re.compile(
r"(compos|combin|together|concrete action|recommend(ation)? [:\-]|"
r"never[- ]composed|the synthesis is|therefore|so the action is)",
2026-04-27 13:20:51 -05:00
re.IGNORECASE,
)
composition_hits = len(composition_re.findall(last_assistant))
2026-04-27 13:20:51 -05:00
if summary_hits >= 3 and composition_hits == 0:
print("BLOCK_SUMMARY")
sys.exit(0)
print("PASS")
2026-04-27 13:20:51 -05:00
PYEOF
RESULT="$(/usr/bin/python3 "$PYFILE")"
case "$RESULT" in
BLOCK_SUMMARY)
cat >&2 <<'BLOCKMSG'
[STOP BLOCKED — VESTIGE SYNTHESIS VALIDATOR]
2026-04-27 13:20:51 -05:00
The response cites multiple memories but does not compose them into a decision.
Rewrite it so the retrieved evidence becomes:
2026-04-27 13:20:51 -05:00
1. Evidence: the memory facts that matter.
2. Implication: what those facts change.
3. Action: the concrete recommendation.
2026-04-27 13:20:51 -05:00
Do not stop at "Memory A says X, Memory B says Y." Compose the evidence.
2026-04-27 13:20:51 -05:00
BLOCKMSG
exit 2
;;
*)
exit 0
;;
esac