feat(plugin): knowledge auto-inject (Path A) — title-first per-turn awareness
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 21s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 55s
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 21s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 55s
New UserPromptSubmit hook (scribe_autoinject.sh) + GET /api/plugin/retrieve that surface the TITLES (never bodies) of the few notes clearing four anti-bloat gates: a per-user confidence threshold (stricter than pull search), a margin gate, per-session dedup (exclude_ids), and a top-k ceiling. Each retrieval is logged to retrieval_logs as source=auto_inject so the threshold can be tuned from data. Per-user config (enable / threshold / top-k) is DB-backed via /api/settings with a Settings UI card; defaults enabled, threshold 0.55, top-k 3 (conservative — tune once auto_inject telemetry accrues). Scribe: project 2, milestone 93, task 1033. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xz4j1H7pjYSjKsEpgcNH5E
This commit is contained in:
@@ -13,6 +13,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/scribe_autoinject.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Scribe plugin — UserPromptSubmit push channel (knowledge auto-inject, Path A).
|
||||
#
|
||||
# On each user prompt, asks the operator's Scribe instance for a TITLE-FIRST
|
||||
# awareness hint: the few notes that clear the per-user auto-inject gates
|
||||
# (high-confidence threshold, margin gate, session dedup, top-k). Titles + ids
|
||||
# only — never bodies; the agent calls get_note(id) to pull anything it judges
|
||||
# relevant. Most turns inject nothing.
|
||||
#
|
||||
# Best-effort enrichment ONLY: unlike the SessionStart channel there is no
|
||||
# static floor here. If the instance is unconfigured/unreachable, or anything
|
||||
# fails, the hook stays SILENT and exits 0 — it must never block a prompt.
|
||||
#
|
||||
# Config (same as scribe_session_context.sh), exported to the hook by Claude Code:
|
||||
# CLAUDE_PLUGIN_OPTION_api_endpoint base URL, no trailing slash
|
||||
# CLAUDE_PLUGIN_OPTION_api_token fmcp_ API key (sensitive)
|
||||
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
||||
#
|
||||
# Session dedup: each surfaced note id is remembered in a per-session file so a
|
||||
# note is injected at most once per session. Passed back as exclude_ids.
|
||||
set -uo pipefail
|
||||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
command -v curl >/dev/null 2>&1 || exit 0
|
||||
|
||||
# UserPromptSubmit delivers a JSON event on stdin: { prompt, session_id, cwd, ... }
|
||||
event=$(cat 2>/dev/null || true)
|
||||
prompt=$(printf '%s' "$event" | jq -r '.prompt // empty' 2>/dev/null) || prompt=""
|
||||
session_id=$(printf '%s' "$event" | jq -r '.session_id // empty' 2>/dev/null) || session_id=""
|
||||
event_cwd=$(printf '%s' "$event" | jq -r '.cwd // empty' 2>/dev/null) || event_cwd=""
|
||||
|
||||
# Nothing to retrieve against.
|
||||
[ -n "$prompt" ] || exit 0
|
||||
|
||||
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_api_endpoint:-}}
|
||||
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}}
|
||||
# Guard against an unexpanded ${...} placeholder arriving as a literal.
|
||||
case "$url" in *'${'*) url="" ;; esac
|
||||
case "$token" in *'${'*) token="" ;; esac
|
||||
# Unconfigured install → silent (auto-inject is pure enrichment).
|
||||
[ -n "$url" ] && [ -n "$token" ] || exit 0
|
||||
|
||||
# Cap the query length — a giant prompt makes a giant URL for no extra signal.
|
||||
q=$(printf '%s' "$prompt" | cut -c1-2000)
|
||||
q_enc=$(printf '%s' "$q" | jq -rR '@uri' 2>/dev/null) || exit 0
|
||||
|
||||
# Resolve the working repo's remote so the server can scope to the bound project.
|
||||
repo_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
||||
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
||||
repo_q=""
|
||||
if [ -n "$repo" ]; then
|
||||
enc=$(printf '%s' "$repo" | jq -rR '@uri' 2>/dev/null) || enc=""
|
||||
[ -n "$enc" ] && repo_q="&repo=${enc}"
|
||||
fi
|
||||
|
||||
# Per-session dedup: ids already injected this session are skipped.
|
||||
state_dir="${TMPDIR:-/tmp}/scribe-autoinject"
|
||||
mkdir -p "$state_dir" 2>/dev/null || true
|
||||
idfile=""
|
||||
exclude_q=""
|
||||
if [ -n "$session_id" ]; then
|
||||
# session_id is an opaque token from Claude Code; keep only filename-safe chars.
|
||||
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
||||
idfile="$state_dir/${safe_sid}.ids"
|
||||
if [ -f "$idfile" ]; then
|
||||
seen=$(tr '\n' ',' < "$idfile" 2>/dev/null | sed 's/,$//')
|
||||
[ -n "$seen" ] && exclude_q="&exclude_ids=${seen}"
|
||||
fi
|
||||
fi
|
||||
|
||||
body=$(curl -fsS --max-time 5 \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
"${url%/}/api/plugin/retrieve?q=${q_enc}${repo_q}${exclude_q}" 2>/dev/null) || exit 0
|
||||
[ -n "$body" ] || exit 0
|
||||
|
||||
context=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || exit 0
|
||||
[ -n "$context" ] || exit 0
|
||||
|
||||
# Remember the surfaced ids so they aren't injected again this session.
|
||||
if [ -n "$idfile" ]; then
|
||||
printf '%s' "$body" | jq -r '.note_ids[]? // empty' 2>/dev/null >> "$idfile" || true
|
||||
fi
|
||||
|
||||
jq -n --arg c "$context" \
|
||||
'{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $c}}'
|
||||
exit 0
|
||||
Reference in New Issue
Block a user