diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index b0fd4a3..98685c7 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.", - "version": "0.1.22", + "version": "0.1.23", "author": { "name": "Bryan Van Deusen" }, "mcpServers": { "scribe": { diff --git a/plugin/hooks/scribe_session_context.sh b/plugin/hooks/scribe_session_context.sh index 4c6b52c..77b5a17 100755 --- a/plugin/hooks/scribe_session_context.sh +++ b/plugin/hooks/scribe_session_context.sh @@ -61,6 +61,31 @@ prepend() { if [ -n "$out" ]; then out="$1"$'\n\n---\n\n'"${out}"; else out="$1" # --- Tier 1: static behavioral mandate (always, keyless, networkless) --- [ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md") +# --- Which version is actually RUNNING (keyless, networkless) --- +# +# An install has two halves and only one self-updates: +# +# marketplaces/…/scribe-plugin/ git clone — pulls on its own +# cache/…/scribe// what EXECUTES — refreshed only when the +# manifest version changes +# +# So inspecting the clone shows a fix present while the broken copy keeps +# running, and the obvious debugging move actively misleads (#2209). Twice, the +# only detector was the operator saying "I don't think it updated". +# +# Naming the running version in every session makes that answerable from the +# transcript instead of by archaeology in the cache directory. Deliberately NOT +# a server round-trip or a stored per-user record: the state most needing +# diagnosis is the one where credentials never arrive, and this line still +# appears there. +manifest="$here/../.claude-plugin/plugin.json" +if [ -f "$manifest" ]; then + plugin_version=$(jq -r '.version // empty' "$manifest" 2>/dev/null) || plugin_version="" + if [ -n "$plugin_version" ]; then + append "> Scribe plugin **v${plugin_version}** is executing in this session. A fix merged after this version has not reached it — the marketplace clone updates on its own, but the cache that runs only refreshes when the manifest version changes." + fi +fi + # --- Tier 2: dynamic rules + active-project context (best-effort) --- url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}} token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}} diff --git a/scripts/check_plugin.py b/scripts/check_plugin.py index 22c706b..1fb44ca 100755 --- a/scripts/check_plugin.py +++ b/scripts/check_plugin.py @@ -307,6 +307,44 @@ def check_local_prior_art_needs_no_instance() -> None: ok("prior-art local arm: answers with no instance configured") +def check_session_context_reports_its_version() -> None: + """The SessionStart context must name the plugin version it is running. + + An install has two halves and only one self-updates: the marketplace clone + pulls on its own, while the CACHE is what executes and refreshes only when + the manifest version changes. So a shipped fix can sit unreached while + inspecting the clone shows it present — the obvious debugging move + misleads, and twice the only detector was a human saying "I don't think it + updated" (#2209, #2220). + + Asserted WITHOUT credentials on purpose. The state most needing diagnosis + is the one where the token never arrives, and a marker that vanished there + would be missing exactly when it is wanted. + """ + script = HOOKS_DIR / "scribe_session_context.sh" + if not script.is_file() or not shutil.which("jq"): + skip("version marker: hook or jq missing") + return + + manifest_v = manifest_version() + if manifest_v is None: + fail("version marker: could not read the manifest version") + return + + try: + proc = _run_hook(script, json.dumps({"source": "startup"}), {}) + except subprocess.TimeoutExpired: + fail("version marker: hook hung") + return + if proc.returncode != 0: + fail(f"version marker: hook exited {proc.returncode}") + elif manifest_v not in proc.stdout: + fail(f"version marker: session context never names v{manifest_v} — " + f"a stale install would be undetectable from the transcript") + else: + ok(f"version marker: session context reports v{manifest_v}, no credentials needed") + + def _git(*args: str) -> tuple[int, str]: proc = subprocess.run( ["git", *args], capture_output=True, text=True, cwd=ROOT @@ -399,6 +437,7 @@ def main() -> int: check_shellcheck() check_fail_open() check_local_prior_art_needs_no_instance() + check_session_context_reports_its_version() if not args.no_version: check_version_bump(args.base)