feat(plugin): surface dynamic-tier failures in SessionStart hook (A2)
Fail-open but no longer silent. When the dynamic context fetch yields nothing,
append a short status line to the injected context so a session can tell
'couldn't load live context' apart from 'Scribe had nothing to say':
- endpoint+token present but fetch empty/failed -> 'instance unreachable / request failed'
- endpoint present but token absent -> fingerprints the known Claude Code
userConfig export gap ('API token did not reach this hook')
A fully unconfigured install (no url AND no token) stays quiet — static-only is
the intended mode there. Static Tier 1 still always carries the mandate.
Refs plan 812 item A2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,16 +23,22 @@
|
|||||||
# vars above are the supported channel; SCRIBE_URL / SCRIBE_TOKEN override for
|
# vars above are the supported channel; SCRIBE_URL / SCRIBE_TOKEN override for
|
||||||
# the settings.json dogfooding path.
|
# the settings.json dogfooding path.
|
||||||
#
|
#
|
||||||
# FAIL-OPEN: the dynamic tier injects nothing on any error; the static tier
|
# FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session. A *failed*
|
||||||
# still fires. A session is never blocked by Scribe.
|
# dynamic fetch is surfaced as a short status line (not swallowed), so a session
|
||||||
|
# can tell "couldn't load live context" apart from "Scribe had nothing to say".
|
||||||
|
# A fully unconfigured install (no url AND no token) is the intended static-only
|
||||||
|
# mode and stays quiet.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely
|
command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely
|
||||||
|
|
||||||
here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0
|
here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0
|
||||||
|
|
||||||
# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
|
|
||||||
out=""
|
out=""
|
||||||
|
# Append $1 to $out, separated by a horizontal rule when $out already has content.
|
||||||
|
append() { if [ -n "$out" ]; then out="${out}"$'\n\n---\n\n'"$1"; else out="$1"; fi; }
|
||||||
|
|
||||||
|
# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
|
||||||
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
|
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
|
||||||
|
|
||||||
# --- Tier 2: dynamic rules + active-project context (best-effort) ---
|
# --- Tier 2: dynamic rules + active-project context (best-effort) ---
|
||||||
@@ -44,7 +50,9 @@ token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_api_token:-}}
|
|||||||
case "$url" in *'${'*) url="" ;; esac
|
case "$url" in *'${'*) url="" ;; esac
|
||||||
case "$token" in *'${'*) token="" ;; esac
|
case "$token" in *'${'*) token="" ;; esac
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1 && [ -n "$url" ] && [ -n "$token" ]; then
|
dyn=""
|
||||||
|
status=""
|
||||||
|
if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||||
# Resolve the working repo's remote so the server can map it to a project.
|
# Resolve the working repo's remote so the server can map it to a project.
|
||||||
repo_dir=${CLAUDE_PROJECT_DIR:-$PWD}
|
repo_dir=${CLAUDE_PROJECT_DIR:-$PWD}
|
||||||
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
||||||
@@ -56,19 +64,18 @@ if command -v curl >/dev/null 2>&1 && [ -n "$url" ] && [ -n "$token" ]; then
|
|||||||
body=$(curl -fsS --max-time 8 \
|
body=$(curl -fsS --max-time 8 \
|
||||||
-H "Authorization: Bearer ${token}" \
|
-H "Authorization: Bearer ${token}" \
|
||||||
"${url%/}/api/plugin/context${q}" 2>/dev/null) || body=""
|
"${url%/}/api/plugin/context${q}" 2>/dev/null) || body=""
|
||||||
if [ -n "$body" ]; then
|
[ -n "$body" ] && dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null)
|
||||||
dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || dyn=""
|
[ -z "$dyn" ] && status="> ⚠️ Scribe: live rules/project context could not be loaded this session (instance unreachable or request failed). The standing guidance above still applies — pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\` as needed."
|
||||||
if [ -n "$dyn" ]; then
|
elif [ -n "$url" ] && [ -z "$token" ]; then
|
||||||
if [ -n "$out" ]; then
|
# Endpoint configured but token absent: the signature of the known Claude Code
|
||||||
out="${out}"$'\n\n---\n\n'"${dyn}"
|
# userConfig export gap (sensitive values not always reaching the hook).
|
||||||
else
|
status="> ⚠️ Scribe: live context disabled this session — the API token did not reach this hook (a known Claude Code plugin-config gap). Tools still work; pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\`."
|
||||||
out="$dyn"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Nothing to inject (no static file AND no dynamic context) → stay silent.
|
[ -n "$dyn" ] && append "$dyn"
|
||||||
|
[ -n "$status" ] && append "$status"
|
||||||
|
|
||||||
|
# Nothing at all to inject (no static file, no dynamic context, no status) → stay silent.
|
||||||
[ -n "$out" ] || exit 0
|
[ -n "$out" ] || exit 0
|
||||||
|
|
||||||
jq -n --arg c "$out" \
|
jq -n --arg c "$out" \
|
||||||
|
|||||||
Reference in New Issue
Block a user