Expand description
probe-hook — the edit-time ENVELOPE as a SHIPPED binary (the eleventh ask).
The whole edit-time sense organ, every voice priced in silence — this hook is FOR the agent, so it surfaces everything a path-and-text edit can derive:
- the GUARD (
Agenda::edit_guard) — refusals that already exist downstream, pre-fired; - the SHAPE TICKER (
discover::watch::Ticker) — a coupling move on ANY Rust edit: a theory nets on its sorts, a plain module on its own declared types, and the ticker speaks only when the edit bridges two clusters or opens a new one — a sixth sense, not a firehose; - the QUALIFY voice (
spec/qualify.spec) — the edit-time LOCK DELTA as a DRIFT LEDGER: the whole current drift of the surface census, re-derived from the tree viaboundary_enforce::qualify_census_linesand diffed against the committed lock. Surfaced on ANY edit (un-scoped) but DEDUPED against the last-shown ledger, so it speaks only when the drift moves — the behavioural mirror at the edit, for the one lock a text edit can re-derive (no how-to-bless recipe: the named lock’s own header carries it); - the TYPE-LIBRARY voice (
library_voice) — the anti-duplication sense (rung 0 of the bundle candidate): when the edited file touches sorts that already carry operators in OTHER files’ census lines, the existing families are named — per-file, deduped, the census intersection doubling as the noise filter; - the TIER voice (
spec/tiers.spec) — on first edit of a file, its derived tier and the rules it carries (the reader-service the deleted//! Tier:markers gave); - the FREEZE-DELTA courier (
freeze_delta_voice) — the recommendation movement the last build derived viaspec_lock::Lock::delta(a placement re-settling, a seam candidate appearing), inserted once into the window and then cleared.
The first five voices the hook DERIVES from a single text edit. The sixth it does not
compute at all: distance, cohesion, and placement need the compiled theory (running eval),
which a text edit cannot afford — so the movement of those recommendations is derived where
it is cheap, at the build that emits the locks (spec_lock::Lock::delta holds both sides at
that instant), and the hook only COURIERS the result into the next context window. The qualify
voice is the boundary case that DOES fit a text edit: qualification is a structural property
(operator-shaped functions), so its lock delta is computed live here, while the behavioural
locks stay couriered. Nothing is watched or reconstructed: the emitter narrates its own delta,
the hook carries it (or, for qualify, derives it). (The
freedom/survivor census stays at session start, read from the committed mutation locks.) All
the voices are mutation-tested and register-driven, and before this crate every
consumer wrapped them in the same four pieces of unjudged glue: a bash wrapper, inline
JSON-parsing Python, a build-on-demand fallback that could run a stale binary, and
hand-authored settings.json plumbing. This crate is that envelope, inside the boundary —
and this repo now dogfoods it in its own .claude/settings.json, the retired
shape-watch.sh being exactly the glue described:
- speaks the Claude Code hook protocol natively — reads the PostToolUse JSON
from stdin, extracts
tool_input.file_path, honoursCLAUDE_PROJECT_DIR; - discovers the repo’s own declarations — voices derived from the tree
(
GuardVoices::for_edit), classes taught fromspec/agenda.register; the consumer writes zero code; - carries the fail-open contract inside the boundary — every internal failure
(malformed JSON, missing path, unreadable file, refused register) is silence,
as a drilled property of
respond, not a|| exit 0convention; - installs its own wiring —
installwrites or idempotently merges thesettings.jsonentry, so the plumbing is derived output, never hand-authored.
Honest frame — version skew: a globally installed probe-hook can be newer or
older than the probe-algebra a repo pins, and the two can disagree about register
grammar or voice derivation. The floor shipped here: every non-silent voice block
carries the binary’s version on its last line, and the guard is advisory and
fail-open, so skew degrades to weaker advice, never a false refusal. (Re-execing a
repo-local build is the known nicer form; deliberately not built until skew is
observed hurting.)
Functions§
- extract_
path - The edited file’s path, out of the hook protocol’s stdin JSON
(
tool_input.file_path).Nonefor anything else — malformed JSON, a tool event without a path — because the hook’s silence must be total on input it does not understand. - install
- Write or merge the
.claude/settings.jsonhook entry — the plumbing as DERIVED output. Idempotent: an entry whose command already invokesprobe-hookis left alone; everything else in the file is preserved untouched. Returns a line saying what happened; errs only on asettings.jsonthat exists but does not parse (never overwrite what cannot be read — that file is not ours). - respond
- The whole envelope, total: hook JSON in, whichever of the guard / shape-ticker / tier
voices fire out (joined, one version footer), or
None. Every failure path isNone— the fail-open contract as a return type. The voices are derived fromproject_dir(the register, the shim evidence), the classes come fromspec/agenda.register, and the edited path is normalized repo-relative so messages match what the repo’s own gates would say.