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

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:
2026-10-21 19:45:09 -04:00
co-authored by Claude Opus 5.5
parent fc8f554cee
commit 17777e9bb1
43 changed files with 641 additions and 777 deletions
+32 -40
View File
@@ -8,16 +8,16 @@
# that path or in its directory, plus snippets resembling the code about to be
# written. Titles + ids only, never bodies.
#
# The answer comes in two framings (#2708). A snippet recorded AT the exact
# The answer comes in two framings. A snippet recorded AT the exact
# file being edited is the SYNC class — "you are editing the recorded file;
# updating the record is part of the edit" — which is how records stay current
# on an instance with no forge connection (decision #2707). Everything else is
# on an instance with no forge connection. Everything else is
# the REUSE menu. The two dedup separately (see the state files below).
#
# It is also the shape ledger's write-path feed (#2791): it names the
# It is also the shape ledger's write-path feed: it names the
# definitions being written (`shapes=`), and the server — only when the
# session has PULLED a snippet this code references or resembles — records
# it as a PROPOSAL on them ("looks like #N"), never a status (milestone 439).
# it as a PROPOSAL on them ("looks like #N"), never a status.
# The agent's own end-of-turn judgment is the verdict; the same names go to
# the written-shapes ledger the Stop hook asks about.
#
@@ -27,8 +27,8 @@
# recall aid must not be able to stop the operator's work.
#
# Config (same as the other hooks), exported to the hook by Claude Code with the
# userConfig key UPPERCASED (see #2198 — the lowercase spelling reads as empty
# and this hook then exits 0 in silence, looking exactly like "no prior art"):
# userConfig key UPPERCASED (the lowercase spelling reads as empty, and this
# hook then exits 0 in silence, looking exactly like "no prior art"):
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
@@ -36,10 +36,10 @@ set -uo pipefail
command -v curl >/dev/null 2>&1 || exit 0
# Shared with the after-write hook (#2901): the prose/data skip list, the
# definition extractor and the local by-name duplicate arm live in
# scribe_defs.sh so the two hooks cannot drift apart. Sourced FIRST because the
# JSON reader is there too now (#4107).
# Shared with the after-write hook: the prose/data skip list, the definition
# extractor and the local by-name duplicate arm live in scribe_defs.sh so the
# two hooks cannot drift apart. Sourced FIRST because the JSON reader is there
# too.
# shellcheck source=plugin/hooks/scribe_defs.sh
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
@@ -77,11 +77,11 @@ if [ -n "$repo_root" ]; then
esac
fi
# ARM 1 — BY NAME, LOCALLY (#2280): does a definition of this already exist
# ARM 1 — BY NAME, LOCALLY: does a definition of this already exist
# in the repo? (scribe_local_dups in scribe_defs.sh carries the why.)
names=""
if [ -n "$code" ]; then
# `|| true` inside: an early-exiting `head` must not void its own output (#4042).
# `|| true` inside: an early-exiting `head` must not void its own output.
names=$(printf '%s' "$code" | scribe_defs | sort -u | head -12 || true)
fi
@@ -97,7 +97,7 @@ if [ -n "$local_lines" ]; then
fi
# ---------------------------------------------------------------------------
# THE LEDGER FEED (#2791). The server keeps a shape ledger — every definition
# THE LEDGER FEED. The server keeps a shape ledger — every definition
# in the bound repo, classified against recorded canon — and this hook is the
# one place that sees a shape AT THE MOMENT IT IS WRITTEN. So it names the
# shapes in play: every definition in the payload, or — for an Edit that
@@ -109,14 +109,9 @@ fi
# names, not bodies.
# ---------------------------------------------------------------------------
#
# THE ENCLOSING DEFINITION IS THE LAST ONE AT OR ABOVE THE EDIT, and taking it
# needs no `tac`. This read `head -n $ln | tac | scribe_defs | head -1` — reverse
# the lines, extract, take the first. `tac` is GNU-only: it is absent on macOS
# (whose equivalent is `tail -r`), so the guard below it meant this arm did
# nothing at all on every Mac, silently, since the day it shipped. scribe_defs
# judges each line independently, so reversing first and taking the head is the
# same answer as extracting forward and taking the tail — and `tail` also drops
# the `head -1` whose early exit is the SIGPIPE trap #4042 was filed for.
# THE ENCLOSING DEFINITION IS THE LAST ONE AT OR ABOVE THE EDIT: extract
# forward and take the tail. No `tac`, which is GNU-only and absent on macOS,
# and no `head -1`, whose early exit is a SIGPIPE trap under pipefail.
shapes="$names"
if [ -z "$shapes" ] && [ -f "$file_path" ]; then
old_first=$(scribe_json_pick "$event_flat" '.tool_input.old_string')
@@ -156,12 +151,9 @@ fi
# built a URL from the whole payload. head -c caps the total, which is the point.
q=$(printf '%s' "$code" | head -c 1200)
# Encoded whole, never line by line. The predecessor (`jq -rR`) read input LINE
# BY LINE, so a multi-line payload came back as several separately-encoded lines
# joined by raw newlines — an invalid URL that made curl fail, and this hook then
# exited 0 in silence. Newlines are exactly what code contains, so this hook
# could never have worked that way (issue #2198 / #2082). scribe_urlenc reads
# bytes and has no notion of a line.
# Encoded whole, never line by line: a payload encoded per line comes back as
# separately-encoded lines joined by raw newlines, an invalid URL, and code is
# full of newlines. scribe_urlenc reads bytes and has no notion of a line.
path_enc=$(printf '%s' "$rel_path" | scribe_urlenc)
[ -n "$path_enc" ] || exit 0
code_enc=$(printf '%s' "$q" | scribe_urlenc)
@@ -176,7 +168,7 @@ repo_q=""
# each other: a title that flew past in a prompt menu twenty turns ago is
# exactly what should reappear at the moment the duplicate is being written.
#
# TWO channels, not one (#2708). The server answers in two classes — REUSE
# TWO channels, not one. The server answers in two classes — REUSE
# ("something similar/nearby is recorded") and SYNC ("a snippet records the
# exact file being edited — updating the record is part of the edit"). They
# dedup separately: a reuse hint shown early in the session must not suppress
@@ -184,16 +176,16 @@ repo_q=""
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
mkdir -p "$state_dir" 2>/dev/null || true
#
# A THIRD channel (#2900): the ledger's derive arm names a duplicate family
# A THIRD channel: the ledger's derive arm names a duplicate family
# (a derive group id) or a canon elsewhere (`canon:<snippet_id>`) for the
# shapes being written. Keyed by that token, not a note id, so it dedups on
# its own file and a family is named once per session, not at every edit.
#
# A FOURTH channel (milestone 307): standing RULES the write resembles. Its own
# A FOURTH channel: standing RULES the write resembles. Its own
# file for the same reason as the others — a rule named once should not be
# re-offered on every subsequent write in the session.
#
# A FIFTH (milestone 444): the Systems whose RULINGS were shown in full. SHARED
# A FIFTH: the Systems whose RULINGS were shown in full. SHARED
# with scribe_tool_rules.sh, like the rule file: an area's rulings shown before
# a command must not be repeated in full before the next edit there.
idfile=""
@@ -208,7 +200,7 @@ rulingsfile=""
rulings_q=""
if [ -n "$session_id" ]; then
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
# What this write defined, for the end-of-turn question (milestone 439). A
# What this write defined, for the end-of-turn question. A
# Write onto a path that does not exist yet creates a file, which may be a
# unit in its own right (see scribe_written_append). Written before any
# server call: the ask at the end of the turn must not depend on this
@@ -234,17 +226,17 @@ if [ -n "$session_id" ]; then
derive_seen=$(tr '\n' ',' < "$derivefile" 2>/dev/null | sed 's/,$//' | scribe_urlenc) || derive_seen=""
[ -n "$derive_seen" ] && derive_exclude_q="&exclude_derive=${derive_seen}"
fi
# Ageing, not a flat read (#3751), and the ONLY ledger here that ages: the
# Ageing, not a flat read, and the ONLY ledger here that ages: the
# note channels above are a different question with a different answer, and
# this arm's sibling hook reads the same rule file through the same helper.
rule_seen=$(scribe_rules_live "$rulefile")
[ -n "$rule_seen" ] && rule_exclude_q="&exclude_rule_ids=${rule_seen}"
# What the session actually OPENED, as against what it was shown (#4100).
# What the session actually OPENED, as against what it was shown.
rule_exclude_q="${rule_exclude_q}$(scribe_held_query "$state_dir/${safe_sid}.opened.ids")"
fi
# Not `|| exit 0`: an unreachable instance must not discard a local finding
# that needed no instance to produce. And not silence either (#2932): a call
# that needed no instance to produce. And not silence either: a call
# that was owed and didn't come back is said, once per outage, so the session
# knows this write went unchecked.
reached=1
@@ -285,13 +277,13 @@ if [ -n "$body" ]; then
fi
fi
# ARM 1½ — the RECORD nudge (#2664). The local arm just proved the thing being
# ARM 1½ — the RECORD nudge. The local arm just proved the thing being
# written already exists elsewhere in this repo, and Scribe returned no record
# of anything for it. That is the one moment "record it" is earned rather than
# noise: the duplication is demonstrated, not guessed. Gated on BOTH sides so
# an ordinary new helper (no other copies) and an already-recorded one (the
# server spoke) stay nudge-free — a reflex that fires on everything is one
# that gets skipped. A server that did not ANSWER earns no nudge (#2932): "none
# that gets skipped. A server that did not ANSWER earns no nudge: "none
# of those copies is recorded" is a claim only an answer can back — the
# unreached line says what actually happened instead.
if [ -n "$local_lines" ] && [ "$reached" = 1 ]; then
@@ -302,11 +294,11 @@ if [ -n "$local_lines" ] && [ "$reached" = 1 ]; then
fi
# ---------------------------------------------------------------------------
# ARM 0 — THE CONTRACT AROUND THE CHANGE (#4215, milestone 419).
# ARM 0 — THE CONTRACT AROUND THE CHANGE.
#
# "You altered the arity, name or shape of something — here is everything that
# reads it." Rule 33's interface-contract check one scope down: not between
# layers but between a definition and its callers.
# reads it." An interface-contract check between a definition and its
# callers.
#
# LOCAL AND SERVERLESS, like ARM 1. It needs the working tree and nothing
# else — the server has no checkout, so this is the only place the question