Commit Graph
22 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 f5a3643da8 refactor(plugin): retire what the hand-bump scheme left behind — the README that taught it, the floor test, the stale rationale (#3328)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / integration (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 16s
#3127 checklist 19. The step's own deletion list turned out to be largely
spent: `check_version_bump()` came out with #3327, and the machinery the step
expected to delete alongside it is load-bearing for its replacement.

`manifest_version(ref=…)`, `--base`, `--no-version` and the `origin/main`
resolve path all STAY. Derivation makes the value right; it does not make the
comparison unnecessary. `check_version_is_minted` still has to ask "did the
version move when the shipped content did?", and that is a base-branch
question no matter who chose the number. The step was planned before #3327
landed, when the assumption was that these died with the guard.

What was actually still standing, all of it teaching or asserting the retired
scheme:

- `plugin/README.md` told the reader to "set a `version` bump per release."
  A shipped file, instructing the exact act the mint replaced — this is how a
  deleted control gets re-added by someone following the docs. Now says not to
  hand-edit the field, names `make mint-plugin`, and says what a forgotten
  mint costs. (`make` is not installed on every workstation, so the direct
  script invocation is given too.)
- `test_plugin_version_bumped_with_the_hook` asserted `version >= (0,1,31)`
  as a tuple of ints. Under a minted value it passes vacuously — every date
  clears a floor of 0.1.31 — and `int("0415")` silently eats the padding the
  format exists to keep. Superseded by
  `test_the_shipped_manifest_carries_a_minted_version`, which asserts the
  canonical shape instead of an ordering the comparator does not perform.
  Removed whole (rule 22).
- The module preamble still ended on "a written rule that depends on being
  remembered is not a control; this is" — true of the bump guard, and read as
  a stronger claim than the mint can support. Replaced with what the change
  did and did not remove: choosing a number is gone, running the mint is not,
  and the difference is that forgetting is now loud rather than silent.
- An orphaned `# --- the version bump ---` section header with nothing under
  it, and a test docstring still naming `check_version_bump`.

`--no-version` keeps its one legitimate case — on `main` the version is
measured against itself — and now says so in both the usage block and its
`--help`, so it does not read as an escape hatch. `check_session_context_
reports_its_version` stays untouched: a different check with a different job,
and the only thing that makes step 6 readable from a transcript (#2220).

Version minted 2026.09.02.0415 -> 2026.09.02.0438 for the README change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcCs1CcQ1ormdnzSshKqvN
2026-09-02 00:38:50 -04:00
bvandeusenandClaude Opus 5 02c1e37620 feat(rules): the write path can notice a standing rule it was never given (#3031, milestone 307 step 5, hook arm)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 24s
A conditional rule is not resident, so a session can be about to violate one it
was never handed. This arm notices: when what is being written resembles a
rule's trigger, the hint names it and says to read it before deciding it does
not apply.

A SUGGESTION, and the plan was wrong about why it could be more. It claimed the
hook "already resolves a path to an area" — it does not, and nothing in Scribe
maps a path to a System or a canonical area (build_write_path_hint resolves
paths against snippet LOCATIONS, a different index; the learned-alias idea
belongs to another project). Correction logged on the task. Rather than invent
path→area inference to make a stale claim true, the arm does what D7 already
decided and what this surface already IS: tags bind at enter_project, meaning
suggests here. The header of the hook says NEVER BLOCKS; dressing a hint up as
binding would have been the actual mistake.

CONDITIONAL RULES ONLY. An always-on rule is already in the session, so
re-offering it is noise — and noise on a hint that fires on every write is how
a hint gets ignored.

Telemetry goes to retrieval_logs, NOT note_usage_events, and that is a
correctness call rather than a preference: note_usage ids are REMAPPED on a
backup restore, so a rule id written there would come back attached to whatever
note took that number — silently corrupting the evidence the next true-up is
supposed to read. retrieval_logs is never restored and `source` already
separates surfaces. record_retrieval's `results` type widened to match what it
actually needs (an `.id`), instead of passing a Rule to something annotated Note.

The rule dedup gets its OWN state file and query parameter, like the three
channels before it — #2708's lesson was that one shared channel lets a hint of
one class silence a different class that had never been shown. Plugin version
bumped: a hook change clients cannot see did not ship (#1040).

The stub is autouse in conftest rather than added to forty-odd call sites: the
arm loads an embedding model, and every existing test that stubs the NOTES
search would otherwise pull a real model in through the one arm it had no way
to know about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:35:36 -04:00
bvandeusenandClaude Fable 5 8664d8ad14 feat(ledger): the consumer map surfaces — every css row carries used_by on list_shapes, flag="unused-css" (the map's negative space, surfaced never deleted), derive groups and the write-path family carry consumers, the derive line says "used by N template(s)", coverage payload unused_css + the standing block; docs, SKILL, plugin 0.1.44; the two step-2 tests expected 5 edges where fan-out makes 6 (milestone 302 step 3, #2936)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Failing after 44s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 31s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 14:03:35 -04:00
bvandeusenandClaude Fable 5 31383bcebe test(hooks): the two tests that read a refused connection as "nothing recorded" now answer through a sink, and the silence case removes the shape it had left behind (#2932)
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 16s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 11:05:42 -04:00
bvandeusenandClaude Fable 5 0ab94b2a00 fix(plugin): write-path hooks say when Scribe did not answer — once per outage, shared marker, record nudge withheld on an unanswered call; check_plugin allows exactly that line when unreachable; plugin 0.1.43 (#2932)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 13s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 40s
CI & Build / Python tests (push) Failing after 49s
CI & Build / Build & push image (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 10:59:54 -04:00
bvandeusenandClaude Fable 5 85111442a6 feat(ledger): CSS derive families are names, never bodies — name floor 2 for css, dup: grouping sym-only; derive line says "repeated name" and dismisses scoped-css; plugin 0.1.41 (note #2917)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 00:21:17 -04:00
bvandeusenandClaude Fable 5 590203a293 refactor(tests+frontend): one http_sink helper for the hook tests; apiErrorMessage replaces ten hand-rolled error-body parses; type X, import specifiers are not definitions (#2904, milestone 299 step 6)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m11s
CI & Build / Build & push image (push) Successful in 38s
tests/helpers.http_sink replaces three module-local _Sink handlers (the
write-path tests and the after-write test). ProjectView + SettingsView
parsed `(e as {body?:{error?}}).body?.error || fallback` by hand ten times
beside the apiErrorMessage canon (#2853) - all ten now call it. The
extractor (server + the hook awk mirror) no longer reads `import { type Foo }`
as a definition of Foo - that was the last "identical body" sym family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 15:07:23 -04:00
bvandeusenandClaude Fable 5 b88225eeb3 fix(hooks): after-write dedups its channel files; prior-art tests follow the extractor and skip list into scribe_defs.sh (#2901)
Run 4240: two pre-write hook tests pinned the skip case and scribe_defs()
inside scribe_prior_art.sh, which moved to the shared library; the after-write
test saw the same derive key appended once per changed file. Keep each token
once (sort -u after the appends) and point the pins at the library.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 13:38:06 -04:00
bvandeusenandClaude Fable 5 2324c15418 feat(write-path): the derive arm — the hint names a duplicate family (no canon) or a canon elsewhere for the shapes being written; exclude_derive channel (#2900, milestone 299 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 23s
shape_ledger.write_time_derive asks the ledger what it knows about each
named (kind, symbol): a derive-grouped family (identical body / same name
in N other files) -> "derive it now, do not add a copy"; a canonical row
at another path -> "canon #N at <path>, reuse". Judged rows at the path and
the canon own file stay silent. Rendered by _derive_line beside the
divergence line; keyed (group id / canon:<id>) on a third per-session dedup
channel in the hook (.derive.ids -> exclude_derive=).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 13:30:33 -04:00
bvandeusenandClaude Fable 5 92e38ff17b test(routes): the prior-art contract guards read the handler plus its _project_scope helper (area 5 follow-up)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 6s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 28s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 11:26:01 -04:00
bvandeusenandClaude Fable 5 bbee0d0db1 refactor(tests): one definition each for the copied fixtures and fakes (#2825, milestone 296 area 1)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 18s
The shape ledger showed the same test scaffolding defined over and over:
_bind_user x12 (byte-identical), _dispose_engine x10 in three wordings,
_no_supersession x3, _make_mock_session x7 in three subsets, a get-or-create
User helper x2 (+3 inlined), and fifteen hand-rolled MagicMock note factories
each re-explaining the same "an auto-MagicMock attribute is truthy" hazard
(note 2109).

Now: conftest.py carries _bind_user / _dispose_engine / _no_supersession as
opt-in fixtures (pytestmark = usefixtures(...) per module, so unit tests pay
nothing), and tests/helpers.py carries make_mock_session(), ensure_user() and
fake_note(**attrs) — the hazard documented once, real values on every
attribute the product reads. Call sites were rewritten by AST so titles with
dashes and commas survived; the three SimpleNamespace _note stand-ins that
only feed a single function stay local.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 11:03:48 -04:00
bvandeusenandClaude Fable 5 d74a244b3a feat(ledger): divergence readout — button B where button A is canon, shape history, and judged-shape recheck (#2793, milestone 294 step 7)
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
Every judgment now goes through one helper that remembers the fingerprint
judged (classified_sha) and writes a code_shape_events row; the sync writes
vanished / reappeared / drifted events and flags recheck_at when a body
moves under an instance/variant. The refresh flags diverges_from on shapes
new since the previous computation that sit where one canon dominates the
judged siblings of their directory+kind and were not proposed as that canon
(a first seed flags nothing); the write-path hint asks the same question
in-band for the shapes the hook names. list_shapes(flag=divergence|recheck),
shape_history(project_id, path, symbol) (read-only), coverage line/payload/
card carry divergent + recheck. Backup v8 carries the history. Plugin 0.1.36.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 23:13:39 -04:00
bvandeusenandClaude Fable 5 475f0857c9 feat(ledger): write-path stamping — a pulled canon the session then instantiates lands as a hook instance row (#2791, milestone 294 step 5)
CI & Build / Plugin hooks (push) Failing after 2s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Failing after 28s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 39s
The prior-art hook now names the shapes being written (shapes=kind:name —
every definition in the payload, or the one enclosing an Edit found by
walking the file upward) and the server stamps them as instance rows when
the session PULLED a snippet inside PULL_WINDOW that the payload references
by symbol or that the semantic arm scored for this very payload.
classified_by=hook, evidence in reason; never overrides a judgment or a
canonical row, overridable by classify_shapes. Offered-but-unopened stamps
nothing. Pulled-and-already-seen snippets stay in the semantic query as
evidence without re-entering the deduped menu. A brand-new shape gets a
provisional row the next sync confirms or vanishes. Read-scoped keys get
the hint, never the stamp. Plugin 0.1.34.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 20:19:45 -04:00
bvandeusenandClaude Fable 5 3162332a13 feat(prior-art): edit-time record-sync nudge — the sync class (#2708)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 24s
CI & Build / integration (push) Successful in 26s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 43s
A snippet recorded AT the exact file being edited is not a reuse
suggestion — it IS the record of the file being changed. The write-path
hint now renders those as their own SYNC class: 'snippet #N records this
file — updating the record is part of the edit (update_snippet /
verify_snippet)'. Nearby and semantic hits stay the reuse menu.

The two classes dedup on separate per-session channels (exclude_ids vs
exclude_sync_ids, .ids vs .sync.ids in the hook), so a reuse hint shown
early in a session can no longer silence the record-sync nudge when the
recorded file itself is edited later. Sync surfacing is measured under
its own note_usage source (write_path_sync) — its pull-through rate is
the scoreboard for whether edit-time sync actually happens, per decision
#2707 (no forge connection; records stay current in the session that has
the context). Plugin 0.1.31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 20:05:01 -04:00
bvandeusenandClaude Fable 5 8407368c0c fix(hooks): definition detector covers all code, not a language shortlist (#2682)
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 18s
CI & Build / Build & push image (push) Successful in 27s
ARM 1 extracted definitions with patterns for CSS/JS/TS/Python only —
the languages of the repo it was born in — so the local duplication
proof, and the #2664 record nudge gated on it, were structurally
unreachable in Go/Kotlin/Rust projects (Minstrel, FabledExchange):
precisely where recording was observed never to happen. One
modifier-strip plus a definition-keyword family (func/fun/fn/function/
def/sub, struct/trait/interface/enum/object/protocol/type, plus Go
method receivers) now covers them all; impl is excluded because several
impl blocks per type is normal Rust, and keyword-less declarations
(C/Java/Dart) are documented out of scope. Grep patterns mirror the
same forms so hits are definitions, never call sites. Parameterized
tests pin the coverage per language family. Plugin 0.1.30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:33:24 -04:00
bvandeusenandClaude Fable 5 4107b17727 fix(telemetry): usage readout grouped by a rebuilt CASE — group by the label instead (#2663 root cause)
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Build & push image (push) Successful in 39s
CI's new integration tests reproduced the outage on a clean database and
named the half: the writes land fine, and usage_for_notes fails on EVERY
call. The GROUP BY rebuilt the ambient case() expression, and asyncpg's
expanding IN-parameters give each instance its own bind names — so Postgres
sees a SELECT expression the GROUP BY doesn't cover and rejects the query
with a GroupingError, which the old code swallowed into zeros. One labelled
expression, grouped by its label. The deployed table has been accumulating
events all along; history appears as soon as this deploys.

Also: the two hook-execution tests now run with the real PATH and skip
when the hook's tools are absent (the restricted-PATH convention next door
is for silence contracts, where empty-for-the-wrong-reason still passes) —
and the unit lane installs jq so 'skip' never quietly becomes 'nowhere'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 21:49:48 -04:00
bvandeusenandClaude Fable 5 fb757fb4ba feat(reuse): recording gets a seam — the prior-art hook asks for create_snippet when duplication is proven and unrecorded (#2664)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 25s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 21s
CI & Build / Python tests (push) Failing after 31s
CI & Build / Build & push image (push) Skipped
Zero snippets were ever recorded outside sessions already thinking about
snippets: the read side had a real seam (the PreToolUse hook) and the record
side had a trailing clause of a floor bullet. The trigger moment — 'I just
wrote the second copy' — is mid-Write/Edit, so the nudge now rides the same
hook: when the local arm proves the definition exists elsewhere in the repo
AND Scribe returned no record of it, the context block asks for
create_snippet. Both gates or silence, so a brand-new helper and an
already-recorded one stay nudge-free.

Floor bullet promoted to name the trigger moments (extract, hoist, second
copy); guarded by test the same way the Systems reflex is. Plugin 0.1.29 so
the cache picks up the hook (#2209).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 21:45:32 -04:00
bvandeusenandClaude Opus 5 da2383b079 fix(retrieval): verify the reserved slot's kind instead of trusting the query
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 34s
CI caught six failures on f8522fb. Five were fixtures; one was a real
assumption.

THE REAL ONE: _reserve_slot_for_reuse trusted that a query filtered by
note_type could only return reuse kinds. It now checks _record_kind on the way
in. That slot exists FOR snippets and processes — one silently spent on
something else is worse than no slot at all, because the resulting line is
indistinguishable from one that earned its place on score.

THE FIXTURES, all the same shape: MagicMock notes with is_task left to
auto-create. It is truthy, and _record_kind reads task-ness FIRST — so every
mock snippet in three test modules was rendering as "task". Two of those
fixtures already carried a comment explaining this exact hazard about `.data`;
the same reasoning applies to `.is_task` and nobody had needed it until the
menu started naming kinds.

One assertion was genuinely stale rather than broken: test_write_path_trigger
pinned note_type == "snippet", which was the behaviour the widening replaced.
Updated to the new contract, including the task_kind="issue" filter that keeps
the open to-do list out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-31 23:01:31 -04:00
bvandeusenandClaude Opus 5 390846a3d5 fix(write-path): disclose cross-language prior art instead of hiding it
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 16s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Has been skipped
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
Closes #2244. Retrieval matches on concept, and concepts are language-agnostic:
asking about a TypeScript union-find scores 0.72-0.73 against a PYTHON snippet,
comfortably over the 0.68 bar. That is useful — a different-language solution
gives you the shape even when the code isn't reusable — but the menu line said
nothing about it, so the reader either dismissed a good structural reference or
pasted Python into a .ts file.

Worth noting this predates the concept-query change: raw TS code already matched
the Python snippet at 0.73, because the embedder reads identifiers and structure
semantically rather than syntactically. The fail state has been shipping quietly;
#2242 only made it an intended use rather than an accident.

- knowledge._note_to_item projects `language` from the data mirror, same shape as
  the existing verification projection — a plain column read, no body parsing.
- The semantic arm carries language through on the item it builds; it is the arm
  where these arise, since a snippet recorded AT the path you're editing is
  almost never in another language.
- _prior_art_line folds it into the marker: [similar 0.72 · python]. Together
  with the score rather than after the title, because the two jointly are the
  judgement being offered.
- One explanatory line is added to the menu, and only when something on it is
  actually tagged.

Two deliberate calls:

LABEL, DON'T FILTER. A stricter threshold for foreign-language hits would
suppress exactly the shape-borrowing this exists for. They were never the
problem; their being undisclosed was.

ONLY CLAIM A MISMATCH YOU CAN ESTABLISH. _foreign_language returns "" when either
side is unknown — unrecognised extension, or a snippet with no recorded language.
A wrong "· python" is worse than no tag. Same-language hits stay unlabelled, so
the common case keeps a clean line and the preamble stays off the menu entirely.
Operator-typed language names fold through an alias table first (py/python3 →
python, tsx → typescript, c++ → cpp); unrecognised names pass through lowercased,
which still makes an unknown-but-equal pair compare equal.

Trap found while building: _note() in the tests is a MagicMock, so `note.data`
auto-created a truthy mock that would have rendered its repr into a menu line.
Both test helpers now set data = None explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-30 10:31:56 -04:00
bvandeusenandClaude Opus 5 57781770c3 feat(write-path): query snippets by concept, not by raw code
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 20s
CI & Build / Python tests (push) Successful in 42s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Build & push image (push) Successful in 26s
Implements #2242, from the operator's question: would it make more sense to
search by the concept of the snippet than by the code itself?

It would, measurably. A snippet's embedded text is f"{title}\n{body}", and a
snippet's body is composed markdown — When to use / Signature / Location, then
the fenced code — so `when_to_use` appears TWICE in the vector and the document
is prose-forward. The semantic arm was interrogating it with raw code carrying
no prose at all. Measured on the deployed instance against snippet #2222:

  query built from            score   best unrelated   separation
  raw code body               0.743   0.630            0.11
  name + docstring            0.823   0.602            0.22
  hand-written concept prose  0.835   0.583            0.25

A 12-word description beats a near-verbatim reimplementation of the function,
and code-as-query RAISES the noise floor. It's also the cleanest explanation for
the fragment miss recorded on #2223: a short excerpt carries almost no prose to
match a document that is mostly prose.

So build the query from what the code says it's FOR — declarations plus the
first docstring / JSDoc / leading comment block — shaped as "name(params) — what
it does", mirroring a snippet's own title, which is the form that measured 0.823.

Server-side rather than in the hook: no manifest bump, so installed 0.1.20
plugins get this immediately; multi-language parsing in bash would be miserable;
and it's unit-testable here.

Two rules worth calling out, both measured rather than chosen:

- NO DOC, NO REWRITE. A bare identifier is not a concept and scored 0.671 vs the
  code body's 0.743. Separation from noise is identical either way (0.113), but
  the absolute drops under the 0.68 bar, so preferring a bare name would convert
  a comfortable hit into a miss. Undocumented code keeps the raw payload.
- The 48-char floor still judges the RAW payload, before the rewrite. A concept
  query is allowed to be shorter than the floor — that is the point, the best
  queries are short — but a sub-floor edit stays silent even with a docstring.
  Applying the floor after extraction would discard the best queries.

Regex, not a parser: this is on a PreToolUse critical path and an Edit's
new_string is rarely a valid module, so a miss must cost only a fallback. Every
unrecognised language (Vue SFC, config files) degrades to exactly the previous
behaviour.

Telemetry now logs the concept query rather than the code, since retrieval_logs
is what the threshold gets tuned from and the two aren't comparable.

0.68 is left alone: signal rises to 0.82 while noise FALLS to 0.58, so the bar
sits mid-gap instead of near the edge. To be re-measured against the deployed
instance rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-30 09:58:06 -04:00
bvandeusenandClaude Opus 5 1fb883b72f fix(write-path): give the semantic arm its own threshold + a payload floor
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 6s
CI & Build / TypeScript typecheck (push) Successful in 22s
CI & Build / integration (push) Successful in 20s
CI & Build / Python tests (push) Successful in 41s
CI & Build / Build & push image (push) Successful in 49s
Closes #2223. The write-path prior-art trigger's semantic arm shared
auto-inject's 0.55 threshold, which was tuned on prose. Code embeddings
sit on a much higher similarity floor — any two Python-shaped payloads
share keywords, indentation and structure — so 0.55 landed INSIDE the
noise band. Measured against the live instance:

  near-duplicate of a recorded helper   0.73-0.74   true positive
  unrelated colour math / Vue SFC / CSS 0.55-0.63   false positive
  `x = 1`                               0.58        false positive

6 of 8 probe payloads produced a nudge; 4 were noise. The margin gate
couldn't help — _AUTOINJECT_BAND is relative to the top hit, so with a
single hit it never engages.

Two gates are now the write-path arm's own:

- kb_writepath_threshold, default 0.68 — above every measured false
  positive, still 0.05 below both true positives. Auto-inject keeps
  0.55; it was tuned on prose and is not implicated. The comment this
  replaces explicitly reserved the split for when telemetry showed the
  surfaces wanted different values, so this is the change it described,
  not a reversal of it.
- WRITEPATH_MIN_CODE_CHARS = 48 non-whitespace chars, below which the
  semantic arm doesn't run at all. Whitespace is excluded so a deeply
  indented one-liner can't pass on padding. 48 sits under the smallest
  plausible reusable helper (~60) and well over a degenerate edit, so it
  errs toward keeping recall — precision is the threshold's job. This is
  the cheap half of the operator's #89 idea; the full length<->threshold
  curve stays open there, since they asked to brainstorm it rather than
  have a scale invented for them.

top_k stays shared — "how many titles at once" means the same thing on
both surfaces.

The existing tests were passing `code="x"` / `code="def f(): ..."` into
the semantic arm, i.e. exactly the payloads the floor now drops, so the
gate tests were never exercising a realistic payload. They now use a
REAL_CODE fixture, plus new coverage for the floor (trivial payload,
padding, place-arm unaffected, real helper passes) and a guard on the
constant itself.

Settings UI carries the new knob with the reasoning in its hint, and the
write-path checkbox no longer claims it shares the threshold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-29 23:26:59 -04:00
bvandeusen e0328f2b1c feat(plugin): write-path trigger — offer prior art before code is rewritten
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 27s
CI & Build / Python tests (push) Successful in 47s
CI & Build / Build & push image (push) Successful in 1m2s
The milestone headline. Auto-inject fires on the operator's prompt; the
moment reuse is actually lost is later, when the agent decides mid-task to
write a helper. A PreToolUse hook on Write|Edit now fires there.

Channel: `additionalContext` with NO permissionDecision, so the note reaches
Claude beside the tool result and the write is never blocked — a recall aid
must not be able to stop the operator's work. Plain stdout would have been
invisible to the model, and deny/ask would have made a nudge into a gate.

Two arms, different in kind:
- BY PLACE — a snippet recorded at this path (or its directory) is prior art
  by definition, not resemblance, so it is neither scored nor thresholded.
  This is what #2083's reverse lookup was built to answer.
- BY MEANING — semantic search restricted to snippets (new `note_type` filter
  on semantic_search_notes) over the code about to be written.
Place ranks first; the top-k cap spans both arms.

Gates carried over from milestone 93 verbatim: threshold, margin, session
dedup, titles-never-bodies. Own `source='write_path'` in retrieval_logs so
precision is tunable separately — the docstring records that the place arm
is unlogged and hands that to #2085.

Its own on/off in Settings but the SAME threshold/top-k: one "how loud may
Scribe be" knob is easier to reason about than two that drift, and splitting
them later is then a data-backed change rather than a guess.

Details worth keeping: the hook sends a REPO-RELATIVE path because that is
how locations are recorded; the git remote resolves to a project and is never
used as the location `repo` filter (different namespaces, would silently
match nothing); the endpoint stays a GET because a read-scoped API key cannot
POST and every other hook depends on that.

plugin.json 0.1.17 -> 0.1.18. Refs #2082, milestone #232.
2026-07-28 09:08:17 -04:00