the expensive failure in AI-assisted coding isn't a bad line of code — linters and tests catch those. it's an agent confidently re-implementing something your team already tried and killed. that's been the selvedge pitch from the start: an append-only store where the agent logs its own reasoning at change time, so "we tried that and it broke revocation" survives the session that learned it.
v0.3.10 solved delivery — the memory now arrives at session start instead of waiting to be asked ("Delivery, Not Storage" [arXiv:2607.20972] measured why that matters: zero voluntary memory operations across 114 turns against a pre-seeded store, while deterministic injection landed every time).
v0.3.11 is about the two halves of the sentence "rejected paths, retained." first: making the rejection itself first-class. second: making retained something you can check instead of something you take my word for.
rejections are now stated outcomes, not inferences
until now, selvedge could tell you a path was abandoned mostly by reading tea leaves: a remove event near an add, a proximity window, a heuristic confidence score. useful, but it's inference — and inference is exactly what this tool exists to replace with testimony.
three changes make abandonment a stated fact:
change_type="reject". the counterpart to revert, for paths never taken. revert says "we tried this and rolled it back." reject says "we considered this and decided against it" — no code was ever written, and the decision is still worth a permanent record, because the next session's agent will consider it too. no new MCP tool; it's a value on the existing log_change.
prior_attempts grows an exact tier. an attempt closed by an explicit reject or revert event now reports confidence: "exact" — the outcome is stated in the log, not inferred from proximity. the old heuristic drops to tiebreaker for attempts without a stated outcome. (if you filter on confidence, accept exact alongside proximity_high — rows move.)
expires_when finally does something. a rejection stored without the condition that would invalidate it is data that silently rots. the column shipped dormant in v0.3.8; v0.3.11 ships the evaluator, with a deliberately closed grammar — four shapes, validated at write time, everything else rejected rather than stored:
library:django>=5.0 # revisit when a named dependency reaches a version
entity:users.email:changes # revisit when that entity next changes
date:2027-01-15 # revisit on a date
manual:security-review # never auto-fires; a label for humans
selvedge stale evaluates these locally — installed package metadata, the event log itself, and the clock. no network, no LLM, and no cleverness: a library: condition whose package isn't locally observable comes back as manual_review, not a guess, and a pre-release never counts as its final release. a decision that knows when it should die is a decision you can still trust in a year.
capture-time nudges close the loop: log a reject or revert with neither stale_when nor expires_when and the validator suggests recording the invalidating condition. it warns, never rejects — same posture as the secret-shape warnings.
the log can now prove itself
here's the uncomfortable question under the whole product: selvedge's differentiator is that rejected decisions stay on the record. append-only. permanent. queryable. and until this release, that was a promise about our own conduct — nothing in the store could show you a row was the row the agent wrote.
v0.3.11 adds a tamper-evident hash chain. every logged event gets a SHA-256 chain record in a sidecar table, written in the same transaction, each record binding the previous one and its own sequence number. selvedge verify gains two checks: chain_intact recomputes the whole thing and fails hard — naming the exact sequence number — when a chained row was edited, deleted, or reordered out-of-band. chain_coverage warns (never fails) about rows that predate the chain: unchained, not invalid.
the design problem worth a paragraph, because it generalizes past selvedge: some fields are late-bound. the commit SHA does not exist when the event is logged — the event describes the work that produces the commit, and a post-commit hook stamps it in afterward. a naive "hash the whole row" chain calls your own git hook an attacker. selvedge's cut: git_commit sits outside the protected core, declared out loud in the attestation manifest — tolerable because a commit hash is independently checkable against git, which is itself a hash chain maintained by other software. everything else — reasoning, diff, entity path, change type, the supersede links — is chained. legitimate mutations don't break anything: migrate-paths appends an amend record, and the destructive-gated prune appends a tombstone accounting for what it deleted. a gated prune verifies clean; a silent sqlite3 "UPDATE events SET reasoning=..." does not.
and the honest scope, which is in the module docstring and belongs in this post too: this detects casual and accidental modification, and produces an independently verifiable export. it is not proof against a motivated local attacker. there's no key. someone who controls the file can recompute every digest. what the chain buys you is that nobody edits the record casually — no script, no well-meant cleanup, no buggy future code path mutates a decision without the next selvedge verify naming the exact row.
we ate the dogfood on day one: selvedge's own store crossed its chain genesis with this release, and the first chained rows are the decisions behind v0.3.11 — including a real change_type="reject" for the retroactive chain seal command we decided not to ship, with the reasoning attached.
the smaller items
-
selvedge supersedegains-d/--diff,--revisit-after, and--expires-when(#31) — parity withselvedge log, through the same size-bound and secret-shape checks. - an id-less supersede no longer re-opens every earlier revert on the path (#30) — the fallback now mirrors the auto-link rule and re-opens at most the one removal it would have linked to at write time.
- both were disclosed as known-and-unfixed in the v0.3.10 post; both are closed now.
- hook contracts, pinned: the PreCompact reminder is byte-identical across repeat fires, goes quiet once you log, and now distinguishes "edited with no log" from "log exists but was truncated" — different problems, different fixes. the SessionStart digest's selection order is a documented contract with a seeded regression fixture for the supersede-mid-render race.
numbers: tests 984 → 1114, coverage 89.4%. the chain checks are mutation-verified — delete the threading checks and the suite goes red. the resident MCP tool-surface cost moved this release (4445 tokens core, 2.2% of a 200k window) because the rejection example and the expires_when grammar now live in the tool docstrings agents actually see; that's the adoption bet, called out in the changelog.
upgrading from 0.3.10
pip install -U selvedge. genesis is automatic on your first write; there's no migration and no new config keys. one expected surprise: selvedge verify will warn about your pre-existing rows (chain_coverage) — they're unchained, not invalid, and the chain deliberately makes no claim about rows that predate it. CI stays green by default.
try it
pip install selvedge
cd your-project
selvedge setup # detects claude code / cursor / copilot
- site: selvedge.sh
- repo: github.com/masondelan/selvedge
- release notes: v0.3.11
- the chain design doc, including everything it deliberately doesn't protect: docs/design/tamper-evidence-proposal.md
open source, MIT, local-first, zero LLM calls in the core. the thing i most want field reports on this time: log a real rejection with an expires_when, wait for it to fire, and tell me whether the revisit that surfaced was worth your attention.
This article was originally published by DEV Community and written by Mason Delan.
Read original article on DEV Community