A canon is only urged on a shape that could be it, the ledger can say what looks wrong, and the agent is the judge #173

Merged
bvandeusen merged 7 commits from dev into main 2026-09-20 23:07:04 -04:00
Owner

Seven commits. CI 7099 green on e87bcfa.

The ledger stops asserting on thin evidence (#4204)

The write-path hook stamped instance rows on elif sid in resemblesany score at all. 0.69 asserted as confidently as 0.95, and the number was printed into the reason line without ever being compared to anything. Worse, that score is computed against the whole payload, so one number spoke for every symbol in the file.

Measured, not assumed: on Portal that recorded a class, three version getters, a TypeScript composable and 17 test functions as instances of one snippet about tmux session attachment. Every one scored 0.69–0.77.

Now: a floor of 0.80, and the candidate is chosen per shape rather than per file. 0.80 rather than the retrieval floors near 0.70 because the decisions differ in cost — a retrieval floor decides whether to SHOW a record, where being wrong costs a glance; this decides whether to RECORD a claim unattended, where being wrong misinstructs everyone who writes in that directory afterwards.

The divergence check also compares the candidate's derived form against the canon's before urging anything, at family level and only on contradiction. The burden scales with the evidence: a by-name reference needs only the absence of contradiction; a whole-file resemblance score must positively agree.

Two corrections CI forced, both recorded in their commits: the first gate was inverted for divergence (a form mismatch is the premise of that prompt, not an objection to it — it would have killed milestone #2793's acceptance case), and a tuple widened 4→5 left one consumer destructuring four.

Honest limit, asserted in a test so it fails if it changes: this silences 1 of #4204's 5 false prompts. The other four are callables against a callable canon, indistinguishable at signature level from a real divergence. #4208 carries that.

The ledger can say "these look wrong" without acting on it (#4208)

A floor guards new writes; every poisoned row already stored stands (lesson #4202). stamps_to_review reports rows stamped below the floor and canons whose own judged rows no longer agree what they are — with score, signature and derived form, so the reader judges the evidence rather than trusting a threshold.

It deliberately cannot write. The first version was an automatic sweep that reset rows by score; that is the original defect pointed the other way, since what harmed the ledger was a machine recording permanent classifications unattended. test_the_service_carries_no_machinery_for_bulk_withdrawal pins that structurally.

The stamp's score also gets a parser (stamp_score / _RESEMBLE_REASON, one constant apart, round-trip tested against reason strings taken verbatim from two real ledgers) — it previously existed only inside a prose sentence, so nothing could ask how strong the evidence for a row was.

The agent is the judge — in the product, not a rule (#4218)

reporting-back prescribed the Finding shape as "Symptom · Cause · Size of the fix · Offer to fix it". An agent that wrote a finding up well and handed the decision over was doing what the product told it to, which across a session produces a backlog of well-written findings nobody ruled on.

Stated now on three surfaces per the ownership registry (#4027): a one-line JUDGE: entry in _INSTRUCTIONS, the authority in using-scribe, the report shape in reporting-back. Two registered topics, not one, so trimming either cannot silently take the other.

With the half that keeps it from becoming the defect above: judging is attended. An agent reading evidence and recording why is judgment; a threshold reclassifying in bulk with nobody reading is not.

Recorded first as a project rule and deleted — rule 119 puts agent guidance in the product, and the reason that matters is that a corpus rule is true on one install.

_INSTRUCTIONS was at 1998/2000 (past ~2048 Claude Code truncates mid-word, #2562). The JUDGE line is paid for by compressing prose around the existing markers; every marker the registry requires survives verbatim, and it is back to 1998.

Data judged alongside this, already applied

Portal: 35 rows re-judged (41 judged → 11); two promoted to canonical as the snippet's own declared locations. Scribe: 32 rows under #2860 withdrawn — all hook-stamped, none the async_session unit. The 302 legitimate rows in services/ are audit-classified with no score and were never in scope.

Needs a deploy

Migration 0105 has been on main since PR #170 and the #3431 telemetry still needs a deploy to reach the running instance. Plugin manifest is 2026.09.21.0300 — the skills change only reaches a session after /plugin + /reload-plugins.

🤖 Generated with Claude Code

https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy

Seven commits. CI 7099 green on `e87bcfa`. ## The ledger stops asserting on thin evidence (#4204) The write-path hook stamped `instance` rows on `elif sid in resembles` — **any** score at all. 0.69 asserted as confidently as 0.95, and the number was printed into the reason line without ever being compared to anything. Worse, that score is computed against the **whole payload**, so one number spoke for every symbol in the file. Measured, not assumed: on Portal that recorded a class, three version getters, a TypeScript composable and 17 test functions as instances of one snippet about tmux session attachment. Every one scored 0.69–0.77. Now: a floor of 0.80, and the candidate is chosen **per shape** rather than per file. 0.80 rather than the retrieval floors near 0.70 because the decisions differ in cost — a retrieval floor decides whether to SHOW a record, where being wrong costs a glance; this decides whether to RECORD a claim unattended, where being wrong misinstructs everyone who writes in that directory afterwards. The divergence check also compares the candidate's derived form against the canon's before urging anything, at family level and only on contradiction. **The burden scales with the evidence:** a by-name reference needs only the absence of contradiction; a whole-file resemblance score must positively agree. Two corrections CI forced, both recorded in their commits: the first gate was **inverted** for divergence (a form mismatch is the premise of that prompt, not an objection to it — it would have killed milestone #2793's acceptance case), and a tuple widened 4→5 left one consumer destructuring four. **Honest limit, asserted in a test so it fails if it changes:** this silences 1 of #4204's 5 false prompts. The other four are callables against a callable canon, indistinguishable at signature level from a real divergence. #4208 carries that. ## The ledger can say "these look wrong" without acting on it (#4208) A floor guards new writes; every poisoned row already stored stands (lesson #4202). `stamps_to_review` reports rows stamped below the floor and canons whose own judged rows no longer agree what they are — with score, signature and derived form, so the reader judges the evidence rather than trusting a threshold. **It deliberately cannot write.** The first version was an automatic sweep that reset rows by score; that is the original defect pointed the other way, since what harmed the ledger was a machine recording permanent classifications unattended. `test_the_service_carries_no_machinery_for_bulk_withdrawal` pins that structurally. The stamp's score also gets a parser (`stamp_score` / `_RESEMBLE_REASON`, one constant apart, round-trip tested against reason strings taken verbatim from two real ledgers) — it previously existed only inside a prose sentence, so nothing could ask how strong the evidence for a row was. ## The agent is the judge — in the product, not a rule (#4218) `reporting-back` prescribed the Finding shape as *"Symptom · Cause · Size of the fix · **Offer to fix it**"*. An agent that wrote a finding up well and handed the decision over was doing what the product told it to, which across a session produces a backlog of well-written findings nobody ruled on. Stated now on three surfaces per the ownership registry (#4027): a one-line `JUDGE:` entry in `_INSTRUCTIONS`, the authority in `using-scribe`, the report shape in `reporting-back`. Two registered topics, not one, so trimming either cannot silently take the other. With the half that keeps it from becoming the defect above: **judging is attended.** An agent reading evidence and recording why is judgment; a threshold reclassifying in bulk with nobody reading is not. Recorded first as a project rule and deleted — rule 119 puts agent guidance in the product, and the reason that matters is that a corpus rule is true on one install. `_INSTRUCTIONS` was at 1998/2000 (past ~2048 Claude Code truncates mid-word, #2562). The JUDGE line is paid for by compressing prose around the existing markers; every marker the registry requires survives verbatim, and it is back to 1998. ## Data judged alongside this, already applied Portal: 35 rows re-judged (41 judged → 11); two promoted to `canonical` as the snippet's own declared locations. Scribe: 32 rows under #2860 withdrawn — all `hook`-stamped, none the `async_session` unit. The 302 legitimate rows in `services/` are `audit`-classified with no score and were never in scope. ## Needs a deploy Migration `0105` has been on `main` since PR #170 and the #3431 telemetry still needs a deploy to reach the running instance. Plugin manifest is `2026.09.21.0300` — the skills change only reaches a session after `/plugin` + `/reload-plugins`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
bvandeusen added 7 commits 2026-09-20 23:06:54 -04:00
fix(ledger): a canon is only urged on a shape that could be it (#4204)
CI & Build / integration (push) Failing after 51s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Successful in 25s
947203fa44
Two halves of one defect, found while writing #3431 and measured on a second
project.

THE DIVERGENCE CHECK WAS A BASE RATE. `dominant_canon` answers "what is most
common in this directory" and never "is this that" — the candidate's
signature was not examined at all. With `kind` carrying only `css | sym`, a
frozen dataclass, a module constant, a sync predicate, a class and an async
service function are all siblings, so the prior was not merely the best
signal, it was the only one. Writing a registry module of pure helpers
produced five prompts to build them from the `async_session` service canon.

THE AUTO-STAMP HAD NO FLOOR. `elif sid in resembles` took any score at all:
0.69 asserted as confidently as 0.95, and the number went into the reason
line without ever being compared to anything. Worse, the score is computed
against the WHOLE PAYLOAD, so one number spoke for every symbol in the file.
On Portal that recorded `class SessionAbsent`, `def build_channel`,
`async def attach` and a dozen test functions as instances of one snippet —
17 rows under #3283, which then made that directory "canon-dense" and started
instructing every later writer in it.

The two compound: loose stamping manufactures the density the divergence
check reads as authority. Both are fixed by one primitive.

`shape_form` derives a coarse form — css / type / async-fn / fn / binding —
from the signature, on READ. `kind` is part of the row identity, so widening
that column needs a migration and a re-extract (#4204 option 2, still the
principled fix); deriving costs nothing and is reversible. Every caller asks
`shape_form`, so the day the column carries the answer it returns that.

THE BURDEN SCALES WITH THE EVIDENCE, and getting this wrong was the first
version. A by-name reference — the payload names the canon's symbol — is
strong and needs only the absence of contradiction; demanding positive
agreement there silenced it whenever a shape's definition was not in the
payload (an Edit rather than a Write), turning strong evidence into none for
a reason unrelated to the code. A resemblance score is weak and must
positively agree.

`canon_form` reads the form a canon's own judged rows agree on, and returns
unknown when they disagree. That makes the halves cooperate: a canon already
poisoned by loose stamping — Portal's #3283 — falls silent instead of
flagging anyone else.

`forms_agree` requires BOTH sides known, so an unreadable signature makes the
checks quieter rather than more confident. `_RESEMBLE_MIN` is 0.80 rather
than the retrieval floors near 0.70: those decide whether to SHOW a record,
where being wrong costs a glance; this decides whether to RECORD a claim
unattended, where being wrong misinstructs everyone who writes there after.

Caught by the tests, not by review: a first pass required `const`/`let`/`var`
before a binding, so every Python module constant read as unreadable and a
whole form was silently excluded from both checks.

Not addressed: rows already carrying a wrong snippet_id are not undone by a
guard at the point of classification (lesson #4202). Portal's 17 keep
producing dominance until something re-judges them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
fix(ledger): divergence asks at family level — the first gate was inverted (#4204)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Failing after 56s
CI & Build / Python tests (push) Successful in 1m29s
CI & Build / Build & push image (push) Successful in 28s
a4883c8ac1
CI run 7090 caught this; the unit suite could not. Integration job 25763 —
`test_a_second_confirm_dialog_is_detected_and_named`, the acceptance case of
milestone #2793.

WHAT I GOT WRONG. The previous commit gated BOTH halves of the ledger on
`forms_agree`. That is right for stamping and backwards for divergence,
because the two assert opposite things:

  STAMPING says "this IS that canon". Agreement in form is evidence FOR the
  claim, so demanding it is correct.

  DIVERGENCE says "this is NOT the canon that dominates here — did you mean
  to?" A form MISMATCH is the PREMISE of that prompt. Requiring the candidate
  to match the canon silences the check precisely where it belongs.

So #2793's case stopped firing: a hand-rolled sync `confirmDanger` in a
directory where an async confirm helper is canon read as `fn` against
`async-fn`, disagreed, and was dropped. `flag_divergence` returned 0 where the
test demands 1, and the write-time check returned nothing where it must name
the canon. That is a real flag the milestone exists to produce, and my change
removed it.

THE FIX. Divergence now gates at FAMILY level — callable {fn, async-fn},
type, value, css — and only on contradiction. A sync function beside an async
one is still a fair question. A frozen dataclass told to build from an async
service function is not a question at all.

WHAT THIS DOES NOT FIX, asserted rather than commented so it fails the day it
changes (`test_how_many_of_the_five_the_divergence_gate_actually_silences`):
of #4204's five false prompts this silences ONE. `Point` is a type against a
callable canon. `_p`, `get_point`, `is_registered` and
`sources_expected_to_emit` are callables like the canon and still ask — and
at the signature level they are indistinguishable from the #2793 case above,
so nothing readable here can separate them. That needs #4204 option 2 (widen
`kind` past `css | sym`) or a comparison of meaning rather than form.

The stamping half — `_RESEMBLE_MIN` 0.80 and the graded burden — is unchanged
and unaffected by this failure. It is also the half that matters more: loose
stamping is what manufactures the density the divergence check reads as
authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
fix(ledger): the in-play tuple widened and one consumer kept reading four (#4204)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 1m42s
CI & Build / Build & push image (push) Successful in 26s
d5b46ffc45
[s_id for rank, _at, s_id, _why in bucket if rank == 2]
    ValueError: too many values to unpack (expected 4, got 5)

`in_play` gained the canon's form as a fifth element so the stamp could be
decided per shape rather than per file. The `record_uses` call eighty lines
below still destructured four, and every stamp that reached it raised.

Both integration failures on runs 7090 and 7091 are this one line —
`test_write_path_stamp_is_evidence_that_yields_to_judgment` and
`test_a_brand_new_shape_gets_a_provisional_row_the_sync_settles`.

Now indexed rather than destructured, matching the candidate scan above it, so
the next widening cannot break it positionally.

WHY THE UNIT LANE STAYED GREEN THROUGH TWO PUSHES. `record_uses` is only
reached once a stamp is actually written, which needs a real snippet, a real
ledger row and the write ACL — so no unit test crosses that line. 139 local
assertions and the whole unit suite passed on code that raised on every
successful stamp. The integration lane was the only thing that could say so,
which is the case for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
feat(ledger): the ledger can say "these look wrong" without acting on it (#4208)
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m6s
CI & Build / Python tests (push) Successful in 1m49s
CI & Build / Build & push image (push) Skipped
400253d039
THE HALF THAT WAS MISSING. #4204 put a floor under what the write-path hook
may assert. A floor only guards new writes; every row already stored stands
(lesson #4202). Measured after that fix shipped: Portal carried 32 rows under
one canon and 3 under another, all stamped on scores of 0.69-0.77 — below the
0.80 floor, so none of them could be written today, and all of them were still
there. Scribe's own ledger carries 334 under #2860.

`stamps_to_review` reports two things and changes nothing:

  weak       — rows the hook stamped on a resemblance below the current floor,
               each with its score, signature and derived form.
  incoherent — canons whose own judged rows do not agree on a form. A canon
               claims some shapes are the same sort of thing; when its members
               are a class, three getters and a dozen tests, that claim has
               stopped being true and every base-rate reading built on it is
               reading noise. `canon_form` already made such a canon fall
               silent — nothing made it VISIBLE.

IT DELIBERATELY CANNOT FIX ANYTHING, and that is the design, not an omission.
The first version of this commit was an automatic sweep that reset rows by
score. That is the original defect pointed the other way: what harmed the
ledger was not one wrong score, it was a machine recording permanent
classifications unattended. Un-recording them unattended is the same act with
a wider blast radius. An agent reads the evidence, judges, and records the
judgment under its own name through `classify_shapes`.

`test_the_service_carries_no_machinery_for_bulk_withdrawal` asserts that
structurally, so the next person to reach for an auto-retire has the argument
again on purpose rather than in a diff nobody reads.

A JUDGMENT IS NEVER LISTED AS WEAK, whatever its age. This is the measured
correction to an assumption I nearly shipped: of Scribe's 334 rows under
#2860, 302 are in `services/` — the canon's own home — and the ones sampled
there are `classified_by="audit"` with no score at all. The legitimate bulk
of that canon was never scored; it was judged by an agent in batch. Listing
those as weak would invite an agent to withdraw the only real judgments in the
ledger. An agent's decision is a different KIND of evidence, not a worse one.

THE SCORE NOW HAS A PARSER. It lived only inside a prose sentence, so nothing
could ask how strong the evidence for a row was without re-deriving it — which
is how 32 rows sat unexamined for nineteen days. Format and reader are one
constant apart (`_RESEMBLE_REASON` / `stamp_score`), with a round-trip test and
a test pinned to reason strings taken verbatim from the two poisoned ledgers.

`live_rows_for` is `live_rows` behind the project read gate, for callers that
arrive from outside rather than from a job that already knows who is asking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
fix(ledger): live_rows_for called access with nothing in scope (#4208)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / Build & push image (push) Successful in 29s
2be17828a9
Lint caught an F821 that would have been a NameError the first time
`stamps_to_review` was called: `access` is imported locally inside each of the
seven functions in this module that need it — services/access reaches back
here, so a module-level import closes a cycle — and the new function used it
without one.

I wrote the function by pattern-matching its neighbours and did not check what
those neighbours do to make themselves work. Same shape as the tuple unpack
two commits ago (#4207): the mistake is not in the logic I was thinking about,
it is in the surrounding contract I did not read.

Unit and integration were both green on the failing run (7095); only lint was
red. Worth recording because the lane that caught it is the cheapest one and I
had read its command as covering tests — `ruff check src/ scripts/` does not
look at tests/ at all, so a clean test suite says nothing about it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
feat(guidance): the agent is the judge — stated in the product, not in a rule
CI & Build / Python lint (push) Successful in 8s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Failing after 1m10s
CI & Build / Build & push image (push) Skipped
76bf21633e
I recorded this as project rule 174 first. That was wrong twice over, and the
second reason is the one that matters.

RULE 119 SAYS THIS EXACTLY: guidance about how an agent should behave with
Scribe belongs in `_INSTRUCTIONS`, `plugin/skills/*` or the adapter's static
context, never in the corpus. I read 119 while writing the rule, decided it was
"about authority rather than about using Scribe", and wrote it anyway — which
is the reasoning preference 29 exists to catch, performed in full.

THE REASON THAT MATTERS: a rule in the corpus is true on ONE install. If the
agent being the judge is how Scribe works, every install gets it or none does.
Baked in, it ships. As a rule it was one operator's private note about a
product stance.

WHAT IT SAYS. The agent is the judge of record for the work — what a shape is,
whether a finding holds, whether something is done. Surfacing a finding for the
operator to rule on is the judgment NOT made, however well written up: it reads
as diligence and functions as a backlog. Escalate the acts that are genuinely
theirs — their money, their infrastructure, anything hard to reverse or facing
outward — and keep the decisions. A hard call is still yours; an irreversible
act is still theirs.

And the half that keeps this from becoming the previous defect: JUDGING IS
ATTENDED. An agent reading evidence and recording why is judgment; a threshold
or a sweep reclassifying in bulk with nobody reading is the thing that fills a
ledger with confident nonsense (#4208, and Portal's 35 rows). When the fix for
bad unattended writes is another unattended write, stop.

THE PRODUCT WAS TEACHING THE OPPOSITE. reporting-back's Finding row read
"Symptom · Cause · Size of the fix · **Offer to fix it**". So the behaviour I
was corrected for is the behaviour the skill prescribed — which is the better
argument for fixing it here than any rule could be.

Three surfaces, per 119 and the ownership registry (#4027): `_INSTRUCTIONS`
gets a one-line JUDGE index entry; using-scribe owns the authority and the
attended/unattended distinction; reporting-back owns the report shape. Two
topics rather than one, registered separately in test_guidance_ownership so
trimming one cannot quietly take the other.

Plugin version minted — skills only reach a session when the manifest moves
(#2209).

Rule 174 deleted (trash 074434a2, recoverable).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
fix(guidance): the index had two characters of headroom, and I spent 391
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 44s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 25s
e87bcfa48c
CI 7098: unit tests red, everything else green. `_INSTRUCTIONS` was 2439
against a 2000 budget.

WHAT I DID NOT CHECK. That block is capped because Claude Code injects only
the first ~2,048 characters of a server's instructions and cuts the rest
mid-word (#2562, observed live — a 20k version delivered ~10% of itself and
the Systems guidance never reached a session). The cap is stated in a comment
directly above the literal I edited. It was at 1998/2000 before this batch:
a shared, nearly-exhausted resource, and I added a six-line entry to it.

THE JUDGE LINE STAYS, and paying for it is the decision rather than dropping
it. A client with no Agent Skills support receives this index and nothing
else, so of everything here, "you are the judge of record" is among the least
safe to leave past the fold — an agent that never learns it defers every call
to an operator who was never going to make them.

So the line is earned by compressing prose AROUND the existing markers, not by
removing anyone's entry: RULES loses a clause, RECORD and REPORT lose trailing
restatement, PLAN drops a sentence the two markers already imply, and the
opening paragraph tightens. Every index marker the ownership registry requires
survives verbatim — that is what test_the_index_names_each_reflex_it_points_at
checks, and it passes.

Back to 1998/2000: the same headroom as before, with one more reflex indexed.
The next addition pays the same way.

Three guidance modules run green locally (21 tests) — they read files and need
no database, so this one did not have to go to CI to be known.

Plugin version re-minted; the previous mint is on a commit that never went
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
bvandeusen merged commit e2de5ad5f6 into main 2026-09-20 23:07:04 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#173