|
| 1 | +#!/usr/bin/env bash |
| 2 | +# dev-loop — Stop hook: automatically flush the insight queue into a reviewed PR, |
| 3 | +# with no manual /dev-loop:knowledge-flush needed. |
| 4 | +# |
| 5 | +# "Verified routing" needs an LLM (real search + dedup + layer/category decision), |
| 6 | +# so a pure shell promote can't do it. This hook instead spawns a DETACHED, |
| 7 | +# headless `claude` run of the knowledge-flush skill — which does the full |
| 8 | +# research→verify→route→ingest→PR pipeline (the pre-flush-pr-gate still enforces |
| 9 | +# the INGEST_REPORT) and opens ONE reviewed PR under the user's OWN gh identity. |
| 10 | +# No auto-merge; the owner reviews every PR. |
| 11 | +# |
| 12 | +# Heavily guarded so it never spams or recurses: |
| 13 | +# - kill switch: DEV_LOOP_AUTOFLUSH=0 |
| 14 | +# - recursion: skipped inside the headless flush run (DEV_LOOP_FLUSHING=1) |
| 15 | +# and inside the flush checkout (~/.dev-loop/repo) |
| 16 | +# - rate limit: at most once per DEV_LOOP_AUTOFLUSH_INTERVAL sec (default 3600) |
| 17 | +# - threshold: only when >= DEV_LOOP_AUTOFLUSH_MIN pending items (default 3) |
| 18 | +# - single-flight: a TTL lock dir |
| 19 | +# - fail-safe: if `claude`/`gh` are missing it silently no-ops; the manual |
| 20 | +# /dev-loop:knowledge-flush skill still works. |
| 21 | +set +e |
| 22 | + |
| 23 | +# --- kill switch + recursion guards --------------------------------------- |
| 24 | +[ "${DEV_LOOP_AUTOFLUSH:-1}" = "0" ] && exit 0 |
| 25 | +[ -n "${DEV_LOOP_FLUSHING:-}" ] && exit 0 |
| 26 | + |
| 27 | +INPUT="$(cat 2>/dev/null)" |
| 28 | +# Skip re-entrant Stop events (e.g. the loop-gate re-prompting a managed session); |
| 29 | +# only flush on a genuine session end. |
| 30 | +printf '%s' "$INPUT" | grep -q '"stop_hook_active"[[:space:]]*:[[:space:]]*true' && exit 0 |
| 31 | +CWD="$(printf '%s' "$INPUT" | node -e 'let s="";try{s=require("fs").readFileSync(0,"utf8")}catch{};let o={};try{o=JSON.parse(s)}catch{};process.stdout.write(String(o.cwd||""))' 2>/dev/null)" |
| 32 | +case "$(cd "${CWD:-$PWD}" 2>/dev/null && pwd)" in |
| 33 | + "$HOME/.dev-loop/repo"*) exit 0 ;; |
| 34 | +esac |
| 35 | + |
| 36 | +# --- prerequisites (fail-safe: no-op if missing) -------------------------- |
| 37 | +command -v claude >/dev/null 2>&1 || exit 0 |
| 38 | +command -v gh >/dev/null 2>&1 || exit 0 |
| 39 | + |
| 40 | +DIR="$HOME/.dev-loop" |
| 41 | +QUEUE="$DIR/queue" |
| 42 | +[ -d "$QUEUE" ] || exit 0 |
| 43 | + |
| 44 | +# --- threshold: count PENDING rows (exclude the retired .processed.jsonl) -- |
| 45 | +PENDING=0 |
| 46 | +for f in "$QUEUE"/*.jsonl; do |
| 47 | + case "$f" in *"/.processed.jsonl") continue ;; esac |
| 48 | + [ -f "$f" ] || continue |
| 49 | + n=$(grep -c '"status":"pending"' "$f" 2>/dev/null) |
| 50 | + PENDING=$((PENDING + ${n:-0})) |
| 51 | +done |
| 52 | +[ "$PENDING" -ge "${DEV_LOOP_AUTOFLUSH_MIN:-3}" ] || exit 0 |
| 53 | + |
| 54 | +# --- rate limit: at most once per interval -------------------------------- |
| 55 | +STAMP="$DIR/.last-autoflush" |
| 56 | +INTERVAL_SEC="${DEV_LOOP_AUTOFLUSH_INTERVAL:-3600}" |
| 57 | +INTERVAL_MIN=$(( INTERVAL_SEC / 60 )); [ "$INTERVAL_MIN" -lt 1 ] && INTERVAL_MIN=1 |
| 58 | +if [ -f "$STAMP" ] && [ -n "$(find "$STAMP" -mmin "-$INTERVAL_MIN" 2>/dev/null)" ]; then |
| 59 | + exit 0 |
| 60 | +fi |
| 61 | + |
| 62 | +# --- single-flight lock (TTL ~15 min via mkdir atomicity) ----------------- |
| 63 | +LOCK="$DIR/.autoflush.lock" |
| 64 | +if ! mkdir "$LOCK" 2>/dev/null; then |
| 65 | + # stale lock older than 15 min → reclaim |
| 66 | + [ -n "$(find "$LOCK" -mmin -15 2>/dev/null)" ] && exit 0 |
| 67 | + rmdir "$LOCK" 2>/dev/null; mkdir "$LOCK" 2>/dev/null || exit 0 |
| 68 | +fi |
| 69 | +touch "$STAMP" 2>/dev/null |
| 70 | + |
| 71 | +# --- spawn the detached headless flush ------------------------------------ |
| 72 | +CLAUDE_BIN="$(command -v claude)" |
| 73 | +PROMPT='Run the dev-loop:knowledge-flush skill now. Drain ~/.dev-loop/queue: for each pending insight research and verify the best-practice against real sources, check existing wiki layers for duplicates/links, decide the target layer/category, run wiki-ingest, write the INGEST_REPORT, then open exactly ONE reviewed PR (label dev-loop:knowledge) under my own gh identity. Do NOT merge. If the queue is empty, do nothing.' |
| 74 | + |
| 75 | +( |
| 76 | + DEV_LOOP_FLUSHING=1 nohup "$CLAUDE_BIN" -p "$PROMPT" \ |
| 77 | + --permission-mode bypassPermissions \ |
| 78 | + > "$DIR/autoflush.log" 2>&1 |
| 79 | + rmdir "$LOCK" 2>/dev/null |
| 80 | +) & |
| 81 | +disown 2>/dev/null |
| 82 | + |
| 83 | +exit 0 |
0 commit comments