Repository navigation
Releases: l0kifs/docir
Release list
v0.30.4 — two first commands in a fresh checkout both answer
🎯 The release
Two first commands in a fresh checkout both answer.
A fresh clone or git worktree has no index yet, so the first command builds one. When two
commands started at once — the ordinary case for an agent that issues its first two reads in
parallel — both found the index empty and both built it, and the slower one exited 1 with a raw
traceback: UNIQUE constraint failed: tags.key, or table alembic_version already exists when
they started within a fraction of a second. Over the daemon both clients answered, but a second
daemon died at startup with the same error, which only daemon.log showed.
🐛 Bug fixes
- The first build of an index happens once, however many commands race for it
(issue-308e5207c77b, GitHub #42). Creating the index tables and the first build now take the
database's write lock before they look, so a second command waits for the first and then finds
the index built. Opening an index that is already built takes no lock, so ordinary commands do
not queue behind a writer. - Large stores no longer give up while another command builds. The wait for that build is
up to five minutes rather than five seconds: on a 2,000-document store the first build takes
~20s, and four simultaneous first commands now all succeed where 0.30.3 failed three.
⬆️ Upgrading
- Nothing in a store changes and nothing needs rebuilding. Upgrade with
docir self upgrade. - The fix lives in the build that opens the store, so a machine still on an older release can
still lose the race. If one does, run the failed command again: the command that won built a
complete index, anddocir doctorconfirms it.
🔗 Full changelog
See CHANGELOG.md
v0.30.3 — an older docir keeps the agent files a newer one wrote
🎯 The release
An older docir keeps the agent files a newer one wrote, instead of quietly replacing them.
docir agent update regenerated every agent file from the running build's templates, whatever
the file's version stamp said. In a shared repository that meant a teammate one release behind
could replace the skill a newer build had written with older text — and docir self upgrade
did the same whenever its install step did not move the version. Both reported updated and
exited 0.
🐛 Bug fixes
-
A file a newer docir wrote is kept, not downgraded (issue-af3f335688f4). Its row says
keptand names the version that wrote it, a warning on stderr says what to do, and nothing
in the skill directory is written or deleted. This applies todocir agent install,
docir agent updateanddocir self upgrade. Upgrade docir and run the command again; only
when the older text is what you want — a repository pinned to an older release on purpose —
write it anyway:docir agent update --force
An absent or unparseable stamp is written over as before, and versions are ordered as PEP 440,
so0.10.0is newer than0.9.0.
⬆️ Upgrading
- The guard lives in the build that runs the command, so releases before this one still
overwrite. Upgrade every machine that refreshes a shared repository's agent files, then run
docir agent updateso the installed skill explains akeptrow. - If an older build already downgraded your skill,
git log -p .claude/skills/shows when;
docir agent updatefrom this release writes the current text back. - A uv tool installed with an exact version pin is one
docir self upgradecannot move — it
says so and quotes the installer. Reinstall it the way the installer's hint names, then run
docir self upgrade --no-package.
🔗 Full changelog
See CHANGELOG.md
v0.30.2 — check does less work, and the daemon sees your .gitignore
🎯 The release
docir check does ~40% less work, every command starts ~0.1s sooner, and the daemon sees a
.gitignore edit.
A warm read was mostly the client starting Python, and check was the slowest command agents
run often. Both were measured before anything changed, every speed-up leaves the output
byte-identical, and the one behaviour change is a fix.
⚡ Faster
Measured on docir's own store (266 documents, 1,947 vectors, 124 governed globs), interleaved
against 0.30.1's source:
| 0.30.1 | 0.30.2 | |
|---|---|---|
docir version (process start, p50) |
0.585s | 0.487s |
warm docir context, daemon side |
95ms | 44ms |
warm docir check, daemon side |
789ms | 477ms |
docir --no-daemon check, whole process |
1.91s | 1.40s |
- Every command starts sooner. The client imported the docs watcher and the site builder
for every command; they now load only indocir daemon serveanddocir build. 704 modules
at startup become 570. contextscores its vectors ~8x faster. The cosine scan runs onmath.sumprod: ~60ms
becomes ~7ms over 1,947 vectors, anddocir lint --deepdrops from 2.09s to 1.12s. No new
dependency.checkwalks each code glob once and reads the corpus once. A**glob used to walk a
file once per directory level above it, and four file checks each parsed every document.
A governed directory holdingnode_modulesortarget/is no longer walked file by file.
🐛 Bug fixes
- An edit to
.gitignorereachescheckthrough the daemon. Since 0.28.0 the daemon read
the ignore files once, at start, so after an edit it kept fingerprintingcode:globs under
the old rules — nocode-driftedwheredocir --no-daemon checkreported one — until it was
replaced. The rules are now read afresh for every command, at ~10ms of the speed-up above.
📏 Measured and rejected
- numpy for the vector scan — converting the decoded vectors costs more than
sumprod's
whole scan. - One
.gitignoreindex per pattern — +205-251ms percheck; one per command is ~10ms.
⬆️ Upgrading
- Nothing to run for the speed-ups, and the daemon is replaced on the first command after the
upgrade. The packaged skill is unchanged;docir agent updateonly re-stamps its version. - If you edited a
.gitignorewhile a daemon on 0.28.0-0.30.1 was running, a--verified
or a new--codeglob written through it may hold a digest the current rules disagree
with.docir checkreports it ascode-changedorcode-drifted; read the document
against the code, thendocir update adr-0001 --verified.
🔗 Full changelog
See CHANGELOG.md
v0.30.1 — a body edit lands where you named it, or is refused
🎯 The release
A body edit lands where you named it, or is refused — it is never dropped.
Two docir update calls reported success on an edit they had not made. Both exited 0 and
printed a document, so an agent had no way to tell from the reply that its text was lost or
misplaced; only reading the body back showed it.
🐛 Bug fixes
-
--append-sectionon a heading the body already has adds under it (GitHub #37). It
wrote a second## Contextat the end on every call, and since every section operation
resolves a heading to its first occurrence, the appended text was out of reach of
--sectionand--replace-section. Now the existing heading is extended and never written
again:docir update adr-0001 --append-section "Context" --body "One more constraint."
Prose lands at the end of the heading's own text, before any sub-heading of its; text
opening with a sub-heading becomes its last sub-section. Text carrying a heading at the
section's level or above is refused — append that heading as its own section. A heading the
body lacks still becomes a new section at the end, as before. -
docir updaterefuses a body no edit mode would write (GitHub #36).--body,
--body-fileor--stdinwithout--append-section,--replace-sectionor
--replace-bodyprinted the unchanged document and exited 0; over MCP,bodybeside
replace_bodywas discarded the same way. All of these now exit 2 naming the modes, and
nothing in the call is applied — a metadata flag beside the stray body does not half-land.
⬆️ Upgrading
- Run
docir agent updateso the installed skill describes both behaviours. - A script that passed a body on every
update, mode or not, now fails where it was losing
the text. Blank text is still accepted. - A document the old append already doubled is listed by
docir lint --deepas
ambiguous-heading. Merge the two copies fromdocir get adr-0001and write the result
back withdocir update adr-0001 --replace-body --force --stdin:--remove-sectiondeletes
the first copy, which is the one holding the original text.
🔗 Full changelog
See CHANGELOG.md
v0.30.0 — an audit is docir check, and check says when it could not look
🎯 The release
An audit is docir check — and check now says when it could not look.
Asked to audit a store, an agent with docir installed either never loaded the skill — its
description named implementing, recording, searching and migrating, not checking — or loaded
it, ran docir check, and then re-derived every finding with grep and Python anyway. That
cost as much as auditing without docir at all.
The skill says the findings are the answer
The installed skill now loads for checking the docs, and tells the agent that check applies
each rule to every file, which of a dangling finding's two ids is the document, and what a
clean check does not mean.
Measured with benchmarks/agent_tokens.py on this repository's store with five planted
defects — a dangling edge, a duplicate id, a depends_on cycle, an overdue review, a file that
does not parse: 12 of 12 audit runs loaded the skill and named exactly the planted defects, at
a median of $0.090–0.116 per audit against $0.23–0.38 for an agent without docir
(claude-sonnet-5, 93 to 259 documents, 2026-09-28). Take it with:
docir agent updateA clean check now means something
With no git repository above the store, there is no tree to resolve a code: glob against, so
unmatched-code, code-unwatched, code-changed and code-drifted all went silent — and an
empty code section read exactly like a tree where nothing had moved. A store whose documents
declare code: now gets one code-unchecked warning saying so. A store that declares no code,
like the ordinary global one, says nothing.
🎯 Also in this release
- The
duplicatefinding says what to do with it.docir lint --deepnames two unlinked
documents that are the same document written twice; the skill, the docstring and the MCP
tool now say the answer is an edge, not a delete. The skill also tells an agent to run
docir contextover a description beforedocir add, and to record a disagreement it finds
by reading as acontradictsedge. - Measured and rejected: detecting contradictions (adr-3c5eb1ff3800). The embedder scores
subject, not stance — a claim and its own negation score up to 0.983 — so no threshold both
reaches this corpus's recorded contradictions and stays readable.
🐛 Bug fixes
Each of these was docir reporting success on something it had not done.
- A repeated list flag keeps every value.
docir add --type decision --title T --code "a/**" --code "b/**"wrote onlyb/**, and said nothing. The seven write-side list flags —add --tags/--related/--code,update --set-tags/--set-related/--set-codeandinit --profiles—
now repeat the way every read-side one already did; each occurrence is still comma-split. - A file that lists one tag, glob or edge twice is read once. A repeated tag failed every
docir reindexwith a rawIntegrityError; a repeated glob made--replace-bodyrefuse the
document as changed on disk, permanently.docir checknow names such a file as
repeated-entry, anddocir check --fixrewrites it without the repeat and without moving
updated. docir self upgradeno longer calls a stalled upgrade "already the newest build". An
installer can exit 0 having changed nothing — auv toolreceipt pinned to an exact version,
a pip held back by a constraint file. The version is now read back after the install; when
it did not move, docir says so and quotes what the installer printed, which is where the
cause is.
⬆️ Upgrading
- Run
docir agent updateso the installed skill loads for audits. - A wrapper that passed a list flag twice and relied on the last one winning now gets both.
- If
docir checkreportsrepeated-entry,docir check --fixrepairs it.
🔗 Full changelog
See CHANGELOG.md
v0.29.0 — a newer docir tells your agent, once
🎯 The release
A newer docir tells your agent, once — and never tells it something it cannot do.
The release check has existed since 0.12.0 and was off behind an environment variable, which
is a setting in one person's shell. The reader it was built for is an agent, and it never saw
it. This release moves the decision into the store, where a team takes it once.
A store now answers the question
docir init writes a committed config.yaml beside docs-schema.yaml:
update_check: trueCreating a store is the act that opts in — deliberate, taken once, and inherited by everyone
who clones the repo. A machine that never created a store and never set the variable still
never contacts PyPI. Existing stores gain the file from docir self upgrade, which appends to
it the way it appends to .gitignore: a key already present is an answer somebody gave, so a
store that set false keeps it.
DOCIR_UPDATE_CHECK=0 turns it off for one shell, =1 on. A run with CI set never checks —
an agent that follows an upgrade instruction inside a job makes that job's docir version depend
on the day it ran.
It says nothing it cannot back
The notice used to name docir self upgrade to every installation. That command's package step
declines for two of the six kinds docir tells apart: an ephemeral uvx run has nothing to
upgrade, and a project install is docir as a dependency of the tree you are working in — what
an editable checkout and every lockfile-managed project detect as.
- a
projectinstall is told nothing - an install with no upgrade command carries its own explanation instead —
uvx docir@latest,
or your lockfile - an install docir owns is told to run
docir self upgradebetween tasks rather than during
one, because the command replaces the running process, respawns the daemon and rebuilds the
index
It is announced at most once per release per day per store. docir self status is still the
unthrottled answer for a reader who is asking rather than being told:
docir self status --refreshMCP hears it too
Over MCP the notice arrives in the server's instructions, at the handshake — the one place an
agent with no stderr will read it, at the one cadence that does not spend the context budget
that body-less skeletons exist to protect. --json is unchanged: docir query, docir search
and docir context emit a bare array, and an envelope around them would break every consumer
to deliver a courtesy.
🎯 Also in this release
docir_doctorgives an MCP client the environment it is running in — the daemon, the
peers a read is skipping, the model in force, the thread cap.DOCIR_EMBED_THREADScaps how many cores the embedding model may take (GitHub #23).docir check --against mainreports id collisions before the merge (GitHub #22).
🐛 Bug fixes
- The established file keeps a duplicated id, decided by git history (GitHub #30).
- An id more than one file claims is refused,
--forceincluded (GitHub #29). - A governed code glob is never silently unwatched (GitHub #26).
🔗 Full changelog
See CHANGELOG.md
v0.28.0 — a daemon you can do without, and a build that is not a change
docir runs where it used to only nearly run, and stops reporting drift you did not cause.
🐛 The three reported defects
- A daemon that cannot be started runs the command in process (#15). The daemon keeps the
model warm and serializes writes; it decides nothing, and every command it answers
--no-daemonanswers too. Inside a read-only agent sandbox the client had nowhere to fall
back to, so every read ended in a Python traceback. It now falls back with one warning on
stderr and stdout untouched. Reproducing it turned up the half that mattered more: the MCP
server built its own executor beside the CLI's, so a CLI-only fix would have left the crash
exactly where an adopter meets it. - A
code:glob hashes what the repository tracks, not what it builds (#20). Ignored files
were fingerprinted, so one compile drifted everysrc/**-style glob at once in any repository
that builds beside its sources —bin/,obj/,target/,dist/,build/— and the
finding was indistinguishable from a real one. The matcher now reads the repository's own
.gitignorefiles, and only those: not.git/info/exclude, not your global excludes, both
per-machine. It was live here too, throughbenchmarks/.coverage. docir self upgradebrings the store's.gitignoreup to the running build (#21).
release-check.jsonwas written into every store and ignored nowhere, and the file itself was
written once bydocir initand never again — sofeedback/never reached a store created
before it. Entries are appended under a comment naming the version, never rewritten over.
🔧 Also fixed
docir doctoranddocir daemon statussurvive a home with nowhere to listen — the two
commands you run to diagnose the first failure.- The embedding model moved to
~/.docir/models. A temp directory is where the system puts what
it may delete, so the 64 MB download was re-paid on every sweep. - A cached peer reader expires when that peer moves, so a repaired peer stops being skipped.
- A daemon that loaded a different
docs-schema.yamlis replaced. docir query --code '.github/workflows/**'finds its documents.docir self upgradecounts the corpus instead of reciting all 56 findings.
📝 The writing skill says a document is the current state
Agents append, because --append-section destroys nothing and so never refuses — and a
document becomes a log of its own life. The body grows against its type's ceiling, and the
superseded half keeps being retrieved, because the index has no idea a later paragraph
overruled an earlier one. Rule 3: git holds what was true before, so something that changed is
edited rather than answered beside. A date in a heading is the signal. Terminal states are
exempt.
⬆️ Upgrading
- Expect one
code-driftedper pattern whose match set shrank. Read the document and
docir update <id> --verified. Where the defect was live, that is every governed document,
and the re-baseline is the fix arriving. - Until everyone upgrades,
code-driftedis a per-build opinion; nothing refuses anything. - The model is re-downloaded once. It is the last one a sweep can force.
docir self upgradetops up the store's.gitignoreand names what it added.
🔗 Full Changelog
See CHANGELOG.md
v0.27.0 — the glob that watches from the moment you write it
🎯 The thesis
A code: glob used to be a promise nothing kept. It named the code a document governs, and
docir check said nothing when that code moved — because the drift digest was written by
docir update --verified and by nothing else. In docir's own store that was all 99
documents naming the code they govern, and the check could fire on none of them.
0.27.0 makes the glob watch from the write that declares it, and makes a store say which docir
it needs before a build that cannot read it fails on the wrong thing.
🎯 New Features
- A
code:glob is watched from the write that declares it.docir checkreports
code-driftedwhen the files behind a pattern stop being the ones the document was pointed
at — no review step to reach first.docir update <id> --verifiedupgrades the same watch to
code-changed, the stronger claim that somebody read the two against each other. Both are
warnings: code landing ahead of its prose is ordinary, not damage. - A store records the docir it needs.
store_format:is read before the schema resolves, so
an older build stops with the version it needs instead of a parse error about a key nobody
removed.docir check --fixwrites the line and keeps the file's comments. docir doctor | jq '.compat'carries the store-format numbers to compare against another
machine's docir, and every surface this build will stop accepting with the date it stops — so
"is this urgent" is answerable without asking. Over MCP, askdocir_deprecations.
🐛 Bug Fixes
- A
**glob no longer fingerprints the package's bytecode. 19 of the 39 files one glob
matched here were.pyc, so a digest moved when no line was edited and differed by
interpreter version. - A store that will not open no longer takes the MCP server down. The tools still list and
every call returns the reason, instead of the client seeing a closed connection. - A daemon that dies at startup says why. The readiness timeout quotes what the daemon wrote
rather than reporting the wait as if it were the diagnosis.
⬆️ Upgrade notes
Run docir check --fix once. It files a baseline for every code: glob that has none, so
drift is watched from that run onward, and records store_format: if your schema needs it. Both
report one action per document.
Expect docir check to report code-drifted on documents that were silent before. That is the
feature: a glob nobody had verified was watching nothing. They clear by reading the document
against the code and stamping --verified — nothing mechanical can, by design.
If a teammate stays on 0.26.0, their first command refuses the migrated index (they delete
index.db* and reindex, once), and their writes drop keys their build does not know. Run docir check --fix after; it refiles what was dropped.
🔗 Full Changelog
See CHANGELOG.md
v0.26.0 — the link written in prose is a link docir reads
A body cites another document two ways, and docir read only one. related: frontmatter was
validated on write, reported by dangling and drawn on the graph; a [[...]] written
mid-sentence was parsed by nothing — not the renderer, not a check, not the index. It published
as literal brackets, and a retitle broke every inbound one in silence. This release makes the
prose link a link docir reads: it resolves, docir build renders it, and docir check names
the ones that point at nothing.
Upgrade notes
- No migration. The index schema is unchanged, so
docir doctorreports only
stale-index-builduntil the firstdocir reindexafter upgrading.docir self upgrade
runs it and refreshes the generated instruction files, whose maintenance reference now
documentsunresolved-link. - A store that already writes
[[...]]sees its broken ones on the nextdocir check.
They were broken before; this is the first build that says so. Each is a warning, so
docir check --strictstill passes, anddocir check --fixleaves them — a target nobody
can name is not a repair without a guess.
The link written in prose is a link docir reads
A target may be a document id, a filename stem, a title slug or the title itself; a .md
suffix and a leading directory are stripped, [[target|label]] keeps the label and
[[target#heading]] links the anchor. Resolution reads every document, inactive and archived
included: a link to a resolved issue is a working link, and "not in the default docir query"
is never a reason to call one broken. An ambiguous target resolves to neither and names its
candidates.
The filename stem is carried as data rather than derived, because docir update --set-title
keeps the filename — so a retitled document's stem is the slug of a title it no longer has,
and the links written against it are exactly the ones that must keep working.
docir build renders a resolved link as the target's current title, so a retitle cannot
leave a link displaying a name no document has. An unresolved one stays the literal [[...]],
like a dangling edge on the same page.
docir build
unresolved-link, the prose half of dangling
docir check reports a [[...]] that resolves to nothing as unresolved-link, one finding
per (document, target). It is Tier 1 where unresolved-mention is Tier 2 because the two are
not the same defect: an id named in a sentence is a citation, while [[...]] is link syntax
with no second reading. The one false positive available is a body demonstrating the syntax,
and the scan skips code spans and fences. Prose links still feed no graph — orphan reads
related: alone — and nothing repairs one, so docir check --fix leaves it.
docir check
Verified by injecting each bug the guards claim to catch, and exercised against this
repository's own store through the daemon, the CLI, MCP and docir build; 0.25.0 reads a
store this build wrote and this build reads one 0.25.0 wrote, both clean.
Underneath: six classes split by reason to change
DocumentService, MaintenanceService, GraphChecker, cli/app.py, the markdown file
store and publishing/infra/rendering.py were each split on the seam their own section
comments had drawn, and build_mcp_server went from 646 lines to 22. No api.py signature
and no CONTRACT.md moved. Every surface was held byte-identical rather than asserted: the
whole Typer tree, tools/list, every finding docir check reports against the released
0.25.0, all 422 files docir build produces from this store, and ranking under 300 randomised
differential trials. What the audit kept, and why DocumentService is not split further, is
on record as adr-a1754eb79fe7.
Full changelog: CHANGELOG.md
v0.25.0 — an upgrade that finishes, and stops redoing work nobody asked for
docir self upgrade could not finish on a large corpus, and most of what took so long was work
it never needed to do. Two halves of one failure. The client bounded the daemon's reply at a
flat 300s, so the command that always makes docir's most expensive request was bounded by a
number chosen for an ordinary one. And that request re-embedded every vector in the store on
every release — including the releases that changed neither the model nor the chunking.
Upgrade notes
- The reply timeout no longer bounds how long a command may take. The daemon sends a
keepalive every few seconds while it works, and each frame re-arms the socket, so
DOCIR_REQUEST_TIMEOUTnow answers "has the daemon died?". There is no corpus size it has to
be raised for; if you raised it, put it back. - Migration
0012addsembeddings.input_digestand upgrades an existing index in place.
The first rebuild after upgrading recomputes every vector once, because a row written by an
older build carries no record of what produced it. Every rebuild after that recomputes only
what moved. - An index at schema
0012is refused by docir 0.24.0 and earlier, by name, with the
rebuild that fixes it. The index is derived and gitignored, so a teammate on an older build
deletes it and reindexes —docir doctorreports it asindex-from-newer-build. Upgrading
everyone who shares a store is the shorter path. - Run
docir reindexafter upgrading, ordocir self upgrade, which does it for you.
The upgrade that timed out while the daemon was still working
docir self upgrade runs reindex --resync, and after the package step that is always the full
pass — the step is what made the build stamp unequal. Its cost is the size of the corpus. The
client gave up at 300s while the daemon completed the rebuild and committed it, which left an
error, a rebuilt store, instruction files that were never refreshed, and no check report. Past
roughly 1,500 documents the command could not succeed at all.
The daemon now sends a keepalive frame while a request runs and the client discards them, so the
budget measures silence rather than work. A wedged or killed daemon still fails, and now says so
in those terms instead of advising a bigger number.
docir self upgrade
docir daemon status
Verified against this repository's own corpus over both transports: 146s of rebuild completed
under an 8s reply budget.
Vectors know what they were computed from
A full rebuild re-saves every document, which queues every one for embedding, and the drain
recomputed all of them — 1,547 vectors and 146s against 205 documents, on a release that had
changed nothing the model reads.
Each row now records a digest of what produced it: the model id, the document's embedding text,
and every chunk it would be split into. The drain compares it and skips a document whose inputs
are unchanged. The same upgrade against the same corpus now costs 2.6s, and a single
hand-edited file still reports exactly one re-embed.
docir reindex
docir embed --flush
A chunking change still forces the full recompute, by itself: different splitting is a different
set of chunks and so a different digest, with no version constant for anyone to remember to
bump. The skip also requires the stored chunk count to match, because chunks live in their own
table and are dropped by their own calls — a matching digest alone would have made docir reindex stop being the repair for a wiped chunk set.
docir reindex therefore reports 0 re-embedded for a corpus nobody has edited. The document
count is unchanged: a rebuild still re-reads every file.
Full changelog
See CHANGELOG.md.