docs(6204): shipped instruction text describes the product, not this instance's records
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 39s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 39s
MCP tool docstrings and comments, the server's orientation notes, the plugin skills, every hook script and the plugin README/PACKAGING drop record ids, milestone numbers, dates and incident stories, and state each non-obvious why as a present-tense fact. History stays in the task logs. Corrections found on the way: - plugin README/PACKAGING said every hook is a GET; moment, rule-opened and reply-rules POST, so the plugin's key needs write scope. Both now say so, name Settings -> MCP Access, and list every wired hook and endpoint. - the contract-change hint printed "(lesson #4207)", this instance's record, to every install. It no longer cites a record. - tasks.py claimed there is no delete_task; there is. - rulebooks.py cited delete_event and a design spec that no longer exist. - scribe_after_write.sh carried an always-empty etag_q; removed. - test_write_path_trigger pinned "| tac | scribe_defs | head -1", text that existed only in a comment about the old code; it now pins the real tail -1 pipeline. - docs/development.md: CI runs on Gitea Actions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,9 @@
|
||||
# Tier 1 (STATIC, always fires, no auth, no network): injects the Claude Code
|
||||
# adapter's own guidance (scribe_static_context.md) — where Scribe's reflexes
|
||||
# are stated (the using-scribe skill), Claude Code's memory files, /compact,
|
||||
# /scribe:sync, and what to do when Scribe is unavailable. Since milestone 410
|
||||
# (decision #4027) it carries only what this client needs said; the reflexes
|
||||
# themselves are owned by the shared skills and the server's index.
|
||||
# /scribe:sync, and what to do when Scribe is unavailable. It carries only what
|
||||
# this client needs said; the reflexes themselves are owned by the shared
|
||||
# skills and the server's index.
|
||||
#
|
||||
# Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Scribe instance
|
||||
# for active-project context and appends it. Config comes from
|
||||
@@ -15,8 +15,8 @@
|
||||
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
||||
# NOTE THE CASE: Claude Code uppercases the userConfig key when exporting it, so
|
||||
# the `api_token` option arrives as CLAUDE_PLUGIN_OPTION_API_TOKEN. Reading the
|
||||
# lowercase spelling silently yields nothing — that was issue #2198, and it
|
||||
# disabled the dynamic tier, auto-inject, and the write-path trigger at once.
|
||||
# lowercase spelling silently yields nothing, which disables the dynamic tier,
|
||||
# auto-inject and the write-path trigger at once.
|
||||
# The active project is resolved server-side from the working repo's git remote
|
||||
# (see services/repo_bindings); bind each repo once with the bind_repo MCP tool.
|
||||
#
|
||||
@@ -27,8 +27,8 @@
|
||||
# in-flight tasks from Scribe. This stays the durable path: record-as-you-go
|
||||
# plus a post-compaction reload from the instance.
|
||||
#
|
||||
# The other half arrived with #3680. A host hook still cannot make the model
|
||||
# flush before a compaction — but scribe_precompact_preserve.sh steers the
|
||||
# The other half: a host hook cannot make the model flush before a compaction,
|
||||
# but scribe_precompact_preserve.sh steers the
|
||||
# SUMMARY, because a PreCompact hook's stdout becomes the compaction's custom
|
||||
# instructions. The two are complementary, and neither replaces the other: that
|
||||
# hook decides what survives into the summary, this one reloads from the record
|
||||
@@ -43,7 +43,7 @@
|
||||
# FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session, and every
|
||||
# way it can come up empty produces a short status line — failed fetch, missing
|
||||
# token, and missing-everything alike. Nothing about the credential path is
|
||||
# allowed to fail quietly; see the #2198 comment at the status block below.
|
||||
# allowed to fail quietly; see the comment at the status block below.
|
||||
set -uo pipefail
|
||||
|
||||
# shellcheck source=plugin/hooks/scribe_defs.sh
|
||||
@@ -61,7 +61,7 @@ event=$(cat 2>/dev/null || true)
|
||||
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
||||
source=$(scribe_json_pick "$event_flat" '.source')
|
||||
|
||||
# --- The rule ledger outlives the context it describes (#3749) ---
|
||||
# --- The rule ledger outlives the context it describes ---
|
||||
#
|
||||
# scribe_prior_art.sh and scribe_tool_rules.sh record every rule id they have
|
||||
# named in <state>/<sid>.rules.ids and hand it back as exclude_rule_ids, so a
|
||||
@@ -78,9 +78,7 @@ source=$(scribe_json_pick "$event_flat" '.source')
|
||||
#
|
||||
# The session id survives a compaction — the ledgers are keyed by it and are
|
||||
# still found under the same name afterwards, so a stale ledger is genuinely
|
||||
# reached again rather than orphaned. (This used to point at an etag marker as
|
||||
# the evidence for that; milestone 394 removed the preload the etag described,
|
||||
# and `plugin_context.py` records its retirement.)
|
||||
# reached again rather than orphaned.
|
||||
#
|
||||
# CLEARED ON THE SOURCES THAT DESTROY CONTEXT, AND ONLY THOSE:
|
||||
#
|
||||
@@ -94,36 +92,28 @@ source=$(scribe_json_pick "$event_flat" '.source')
|
||||
# fork carries the conversation, so if it inherits the id the ledger
|
||||
# is accurate, and if it gets a new one the file is empty anyway.
|
||||
#
|
||||
# EVERY LEDGER, AND THE NOTE ARMS NEEDED IT MOST (#4101).
|
||||
#
|
||||
# This used to clear the two rule ledgers by name and say, in a comment, that
|
||||
# leaving `.ids` / `.sync.ids` / `.derive.ids` alone was "a decision rather than
|
||||
# an oversight". Reading the note arms says otherwise, on two counts:
|
||||
# EVERY LEDGER, AND THE NOTE ARMS NEED IT MOST:
|
||||
#
|
||||
# - They are HARD exclusions. `exclude_ids` goes into `semantic_search_notes`
|
||||
# itself, so a surfaced note is removed from the result set — it is not
|
||||
# rendered as a reference the way #3750 made a repeated rule. There is no
|
||||
# weaker form for it to fall back to.
|
||||
# - They never AGE. #3751 gave the rules ledger a TTL precisely because
|
||||
# salience decays without a context event; the note channels were left on a
|
||||
# flat read.
|
||||
# rendered as a reference the way a repeated rule is. There is no weaker
|
||||
# form for it to fall back to.
|
||||
# - They never AGE. The rules ledger has a TTL because salience decays
|
||||
# without a context event; the note channels are a flat read.
|
||||
#
|
||||
# Hard plus permanent plus never cleared means a note surfaced in the first
|
||||
# minute of a session is unreachable for the rest of it, through any number of
|
||||
# compactions. That is milestone 386's original defect, alive on the arms that
|
||||
# fire most often, and nothing about it was decided.
|
||||
# Hard plus permanent plus never cleared would make a note surfaced in the
|
||||
# first minute of a session unreachable for the rest of it, through any number
|
||||
# of compactions.
|
||||
#
|
||||
# THE LIST WAS THE BUG, so the fix is not a longer list. `scribe_clear_session_
|
||||
# ledgers` matches on the naming convention — a per-session ledger is
|
||||
# `<sid>[.<kind>].ids` — which covers all five and covers the sixth on the day
|
||||
# it is written. Best-effort, like every other filesystem touch in these hooks:
|
||||
# `scribe_clear_session_ledgers` matches on the naming convention — a
|
||||
# per-session ledger is `<sid>[.<kind>].ids` — so it covers every ledger,
|
||||
# including one added later. Best-effort, like every other filesystem touch in these hooks:
|
||||
# a ledger that cannot be removed costs a repeated exclusion, never a session.
|
||||
#
|
||||
# NOT swept: `<sid>.unreached`, which records that the instance was unreachable
|
||||
# rather than what the session holds, and survives on purpose. The directories
|
||||
# swept are `scribe_defs.sh`'s `SCRIBE_LEDGER_DIRS` — plural, because the
|
||||
# auto-inject arm keeps its ledger somewhere else and a single-directory sweep
|
||||
# missed exactly the arm that fires most.
|
||||
# auto-inject arm keeps its ledger somewhere else.
|
||||
case "$source" in
|
||||
compact|clear)
|
||||
sid=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
@@ -152,8 +142,7 @@ prepend() { if [ -n "$out" ]; then out="$1"$'\n\n---\n\n'"${out}"; else out="$1"
|
||||
# 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".
|
||||
# running, and the obvious debugging move actively misleads.
|
||||
#
|
||||
# Naming the running version in every session makes that answerable from the
|
||||
# transcript instead of by archaeology in the cache directory. Deliberately NOT
|
||||
@@ -177,7 +166,7 @@ dyn=""
|
||||
status=""
|
||||
if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
# Which project is this directory's? A `.scribe` marker first, then the git
|
||||
# remote (#4085). scribe_scope_query answers that, and is shared with the
|
||||
# remote. scribe_scope_query answers that, and is shared with the
|
||||
# other five hooks so a directory scopes the same way everywhere; the pieces
|
||||
# are re-read here only to explain what happened when nothing resolved.
|
||||
repo_dir=${CLAUDE_PROJECT_DIR:-$PWD}
|
||||
@@ -187,19 +176,18 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
marker_why=${marker_read#*$'\t'}
|
||||
repo=$(git -C "$repo_dir" remote get-url origin 2>/dev/null || true)
|
||||
scope=$(scribe_scope_query "$repo_dir")
|
||||
# The source and the session id decide what the claim section says
|
||||
# (milestone 381): a compaction gets back the work this session had claimed,
|
||||
# The source and the session id decide what the claim section says: a
|
||||
# compaction gets back the work this session had claimed,
|
||||
# with its latest logs; a new session hears about other sessions' claims.
|
||||
sid_now=$(scribe_json_pick "$event_flat" '.session_id')
|
||||
q="source=$(printf '%s' "$source" | scribe_urlenc)"
|
||||
[ -n "$sid_now" ] && q="${q}&session_id=$(printf '%s' "$sid_now" | scribe_urlenc)"
|
||||
[ -n "$scope" ] && q="${q}&${scope}"
|
||||
q="?${q}"
|
||||
# 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.
|
||||
# ONE FETCH, NAMED WHEN IT FAILS. A timeout, an HTTP error and a refused key
|
||||
# need different fixes, so each gets its own sentence. 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}' \
|
||||
@@ -212,7 +200,7 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
ctx_code=${meta%% *}
|
||||
ctx_took=${meta#* }
|
||||
}
|
||||
# Deadlines: 8s is the long-standing first try, sized for a cold instance.
|
||||
# Deadlines: 8s for the 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
|
||||
@@ -227,10 +215,6 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
body_flat=$(printf '%s' "$body" | scribe_json_flat)
|
||||
dyn=$(scribe_json_pick "$body_flat" '.context')
|
||||
fi
|
||||
# The rules marker is gone with the resident set it described
|
||||
# (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.
|
||||
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)"
|
||||
@@ -243,9 +227,9 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
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).
|
||||
# The first move, stated as one rather than as optional: a session that
|
||||
# skips it starts with no recent milestones or open tasks, so it cannot
|
||||
# know what prior work exists to look for.
|
||||
if [ -n "$marker_id" ] && [ -z "$marker_why" ]; then
|
||||
first="Start by calling \`enter_project(${marker_id})\`"
|
||||
else
|
||||
@@ -256,23 +240,20 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
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
|
||||
# NEITHER value arrived. Previously this case stayed silent as "an unconfigured
|
||||
# install", which made issue #2198 invisible for weeks: a *casing* bug here
|
||||
# (reading CLAUDE_PLUGIN_OPTION_api_token when Claude Code exports the key
|
||||
# UPPERCASED) looks identical to never having configured the plugin, and
|
||||
# silently disabled auto-inject and the write-path trigger too. It is not a
|
||||
# benign state — the plugin prompts for both values at enable time, so if
|
||||
# neither reached the hook, something is wrong. Say so.
|
||||
# NEITHER value arrived. Not a benign "unconfigured install": the plugin
|
||||
# prompts for both values at enable time, so if neither reached the hook,
|
||||
# something is wrong (an env-var casing mismatch looks exactly like this).
|
||||
# Say so.
|
||||
status="> ⚠️ Scribe: live context disabled this session — neither the Scribe base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Scribe), or export SCRIBE_URL + SCRIBE_TOKEN. Note this also disables prompt auto-inject and the write-path prior-art trigger. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
|
||||
fi
|
||||
|
||||
[ -n "$dyn" ] && append "$dyn"
|
||||
[ -n "$status" ] && append "$status"
|
||||
|
||||
# --- Nothing resolved: say WHICH nothing, and what would fix it (#4085) ---
|
||||
# --- Nothing resolved: say WHICH nothing, and what would fix it ---
|
||||
#
|
||||
# The server can say "no project is bound to this working directory", and
|
||||
# until now that was the whole answer. It is the same sentence for a directory
|
||||
# The server can say "no project is bound to this working directory". It is
|
||||
# the same sentence for a directory
|
||||
# that is not a repo, a repo whose remote nobody bound, and a marker file
|
||||
# naming a project this account cannot read — three different problems with
|
||||
# three different fixes, and no way to tell them apart from inside the session.
|
||||
|
||||
Reference in New Issue
Block a user