fix(plugin): a SessionStart that loads no project says why, and names the first move (#4366)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / Python tests (push) Canceled after 23s
CI & Build / integration (push) Canceled after 27s
CI & Build / TypeScript typecheck (push) Canceled after 28s

The context fetch folded a timeout, an HTTP error and a refused key into
one sentence that ended "enter_project() as needed" -- read as optional,
so a session could start with no recent milestones or open tasks and no
way to know what prior work existed. The fetch now names the cause and
the elapsed time, retries once (6s) only where a retry can change the
answer, and the fallback states enter_project as the first step, with
the marker's project id when there is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:07:33 -04:00
co-authored by Claude Opus 5.5
parent 574b27ae74
commit eb00554976
3 changed files with 105 additions and 6 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "scribe",
"description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).",
"version": "2026.09.23.2005",
"version": "2026.09.23.2007",
"author": {
"name": "Bryan Van Deusen"
},
+50 -5
View File
@@ -189,11 +189,35 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
scope=$(scribe_scope_query "$repo_dir")
q=""
[ -n "$scope" ] && q="?${scope}"
body=$(curl -fsS --max-time 8 \
-H "Authorization: Bearer ${token}" \
"${url%/}/api/plugin/context${q}" 2>/dev/null) || body=""
# ONE FETCH, NAMED WHEN IT FAILS (#4366). This used to be `curl -f … ||
# body=""`, which folded a timeout, an HTTP error and a refused key into one
# sentence — so a session that started blind could not say why, and neither
# could the operator afterwards. curl's write-out still prints on a failed
# transfer (as `000`), so the status and the elapsed time come back either way.
fetch_context() {
local resp meta
resp=$(curl -sS --max-time "$1" -w '\n%{http_code} %{time_total}' \
-H "Authorization: Bearer ${token}" \
"${url%/}/api/plugin/context${q}" 2>/dev/null)
ctx_rc=$?
meta=${resp##*$'\n'}
body=${resp%$'\n'*}
[ "$body" = "$resp" ] && body=""
ctx_code=${meta%% *}
ctx_took=${meta#* }
}
# Deadlines: 8s is the long-standing first try, sized for a cold instance.
# ONE retry at 6s, and only for failures a second try can change — a
# timeout, a dropped connection, a 5xx. A 4xx is the key or the scope and
# will say the same thing twice. Worst case is ~14s of startup, against a
# whole session run without its project.
fetch_context 8
case "$ctx_rc:$ctx_code" in
0:2*|0:4*) ;;
*) fetch_context 6 ;;
esac
body_flat=""
if [ -n "$body" ]; then
if [ "$ctx_rc" = 0 ] && [ "${ctx_code#2}" != "$ctx_code" ] && [ -n "$body" ]; then
body_flat=$(printf '%s' "$body" | scribe_json_flat)
dyn=$(scribe_json_pick "$body_flat" '.context')
fi
@@ -201,7 +225,28 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
# (milestone 394). Nothing is preloaded, so there is no set whose
# drift a later write could be told about — a rule is retrieved at
# the moment it applies, which cannot be stale.
[ -z "$dyn" ] && status="> ⚠️ Scribe: live project context could not be loaded this session (instance unreachable or request failed). The using-scribe skill still applies — ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\` as needed."
if [ -z "$dyn" ]; then
if [ "$ctx_rc" = 28 ]; then
why="the instance did not answer in time (8s, then ${ctx_took}s on a retry)"
elif [ "$ctx_rc" != 0 ]; then
why="the instance could not be reached (curl exit ${ctx_rc}, after a retry)"
elif [ "$ctx_code" = 401 ] || [ "$ctx_code" = 403 ]; then
why="the API key was refused (HTTP ${ctx_code})"
elif [ "${ctx_code#2}" = "$ctx_code" ]; then
why="the instance answered HTTP ${ctx_code}"
else
why="the instance answered but sent no context"
fi
# The first move, stated as one. "As needed" read as optional, and a
# session that skips it starts with no recent milestones or open tasks —
# so it cannot know what prior work exists to look for (#4366).
if [ -n "$marker_id" ] && [ -z "$marker_why" ]; then
first="Start by calling \`enter_project(${marker_id})\`"
else
first="Start by finding this repo's project with \`list_projects()\` and calling \`enter_project(<id>)\`"
fi
status="> ⚠️ Scribe: live project context was not loaded this session — ${why}. The tools may still answer. ${first} before any other work: it loads the recent milestones and open tasks this session would otherwise begin without, and prior work you cannot see is work you will redo. The using-scribe skill still applies."
fi
elif [ -n "$url" ] && [ -z "$token" ]; then
status="> ⚠️ Scribe: live context disabled this session — the API key is not configured (Scribe base URL is). Set it with \`/plugin\` → Scribe → configure, or export SCRIBE_TOKEN. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
elif [ -z "$url" ] && [ -z "$token" ]; then