The cure for AI slop is a 1986 aircraft manual/kit

asd-ste100/hooks/ste-refresh.py

5 KB·Raw on GitHub ↗

A short card every 12 tool calls, so the rule stays near the reply on a long turn. Scores written md files too

#!/usr/bin/env python3
"""PostToolUse hook for the asd-ste100 skill. Two jobs:
 
1. The distance problem. The turn-start card injects the rule, but a long
   working turn puts dozens of tool calls between that card and the final
   reply. By the time the model writes the reply, the card sits far back in
   context. This hook counts the tool calls in the turn and re-injects a short
   card every EVERY calls, so the rule is always near the reply.
 
2. The file problem. The Stop gate lints the chat reply only. Prose written to
   an md file never meets it. After a Write or an Edit of a prose md file,
   this hook lints the file and reports the score into context, so the model
   fixes the file in the same turn.
 
Both jobs inject context. Neither blocks. The turn counter is reset by
ste-inject.sh on each user prompt.
 
Canonical copy: the ep01 kit, asd-ste100/hooks/ste-refresh.py
"""
import hashlib
import json
import os
import subprocess
import sys
 
def find_lint():
    """The linter ships next to this skill. Look there first, then at the
    installed path, so the plugin cache, a skills-CLI copy, and the
    settings-route install all resolve."""
    root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
    for path in (os.path.join(root, "scripts", "ste-lint.py"),
                 os.path.expanduser("~/.claude/skills/asd-ste100/scripts/ste-lint.py")):
        if os.path.exists(path):
            return path
    return None
 
 
LINT = find_lint()
STATE = os.path.expanduser("~/.claude/ste-gate")
 
EVERY = 12          # tool calls between two cards
TARGET = 2.5        # flavored target for a written md file
MIN_WORDS = 40      # skip a stub file - the score is noise below this
MAX_BYTES = 200_000
 
# Paths where the advisory stays silent. Blog posts and episode pages have a
# voice. The experiment files are published data. The skill files quote
# violations on purpose. A scratchpad draft is linted by the model itself.
SKIP = ["/blog/", "(episode)", "/experiment/", "/asd-ste100/",
        "/scratchpad/", "/memory/", "/ste-gate/", "/output-styles/"]
 
CARD = (
    "ste check-in, mid-turn: the standing rule still governs the reply you "
    "write at the end of this turn. Layer 1: active voice, simple tenses, "
    "max 20 words for an instruction, no contractions, no semicolons, keep "
    "the articles. Layer 2: the next action first, numbered steps, no "
    "preamble, no recap, no closer. If the reply runs over 60 words, lint "
    "the draft first: python3 {} --fail-over 2.5 FILE"
).format(LINT or "scripts/ste-lint.py")
 
 
def count_path(session):
    os.makedirs(STATE, exist_ok=True)
    key = hashlib.sha1((session or "none").encode()).hexdigest()[:16]
    return os.path.join(STATE, "{}.count".format(key))
 
 
def bump(session):
    path = count_path(session)
    try:
        with open(path, encoding="utf-8") as fh:
            n = int(fh.read().strip() or 0)
    except Exception:
        n = 0
    n += 1
    try:
        with open(path, "w", encoding="utf-8") as fh:
            fh.write(str(n))
    except Exception:
        pass
    return n
 
 
def md_report(payload):
    if payload.get("tool_name") not in ("Write", "Edit"):
        return None
    path = (payload.get("tool_input") or {}).get("file_path") or ""
    if not path.endswith(".md") or any(s in path for s in SKIP):
        return None
    try:
        if os.path.getsize(path) > MAX_BYTES:
            return None
        with open(path, encoding="utf-8", errors="replace") as fh:
            text = fh.read()
        proc = subprocess.run([sys.executable, LINT], input=text,
                              capture_output=True, text=True, timeout=15)
        report = json.loads(proc.stdout)
    except Exception:
        return None
    if report.get("words", 0) < MIN_WORDS:
        return None
    score = report.get("total_per100w", 0)
    if score <= TARGET:
        return None
    hits = {k: v for k, v in (report.get("violations") or {}).items() if v}
    top = sorted(hits.items(), key=lambda kv: -kv[1])[:3]
    longs = report.get("sample_long_sentence") or []
    return (
        "ste: {} scored {} violations per 100 words. The target is {}. "
        "Worst: {}.{} Fix the file before you finish this task."
    ).format(os.path.basename(path), score, TARGET,
             ", ".join("{} {}".format(k, v) for k, v in top),
             " Longest: {}.".format(" | ".join(longs)) if longs else "")
 
 
def main():
    try:
        payload = json.load(sys.stdin)
    except Exception:
        return 0
    if not LINT:
        return 0
    notes = []
    if bump(payload.get("session_id") or "") % EVERY == 0:
        notes.append(CARD)
    report = md_report(payload)
    if report:
        notes.append(report)
    if notes:
        print(json.dumps({"hookSpecificOutput": {
            "hookEventName": "PostToolUse",
            "additionalContext": "\n\n".join(notes),
        }}))
    return 0
 
 
if __name__ == "__main__":
    sys.exit(main())
 

← Back to the episode