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.
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.
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.
selvedge supersede
-d/--diff
, --revisit-after
, and --expires-when
(#31) — parity with selvedge log
, through the same size-bound and secret-shape checks.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.
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.
pip install selvedge
cd your-project
selvedge setup # detects claude code / cursor / copilot
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.