feat(plugin): every session says which plugin version is executing
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 23s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 19s
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 23s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 19s
Twice a shipped fix failed to reach a live install, and both times the only detector was the operator saying "I don't think it updated" (#2198, #2209). The reason it is hard to see: an install has two halves and only one self-updates. The marketplace clone pulls on its own; the CACHE is what executes and refreshes only when the manifest version changes. So inspecting the clone shows the fix present while the broken copy keeps running — the obvious debugging move actively misleads. The SessionStart context now names the version it is running. That makes "what is actually executing?" answerable from the transcript rather than by archaeology in the cache directory. DELIBERATELY SMALLER THAN THE ISSUE PROPOSED. #2220 recommended reporting the version to the server, storing last-seen per user, and surfacing it in Settings. That is three surfaces and a migration to answer a question the session can answer about itself. Per the operator's framing on #2338 — "I'm afraid of building another integration between two more surfaces, you being able to notice is enough" — the visibility is the deliverable, not the plumbing. It also lands the issue's own caveat, which the server-side design could not: the state most needing diagnosis is the one where credentials never arrive, and there the dynamic tier does not run at all. This marker is keyless and networkless, so it still appears — verified against both the unconfigured and unreachable-instance paths. CI pins it, asserting WITHOUT credentials for the same reason. Manifest 0.1.22 -> 0.1.23, because a shipped hook changed and an unbumped manifest is precisely the failure this commit is about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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/<version>/ 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:-}}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user