Skip to content

Releases: l0kifs/docir

v0.30.4 — two first commands in a fresh checkout both answer

Choose a tag to compare

@l0kifs l0kifs released this 21 Oct 11:29

🎯 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, and docir doctor confirms it.

🔗 Full changelog

See CHANGELOG.md

v0.30.3 — an older docir keeps the agent files a newer one wrote

Choose a tag to compare

@l0kifs l0kifs released this 17 Oct 14:18

🎯 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
    kept and 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 to docir agent install,
    docir agent update and docir 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,
    so 0.10.0 is newer than 0.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 update so the installed skill explains a kept row.
  • If an older build already downgraded your skill, git log -p .claude/skills/ shows when;
    docir agent update from this release writes the current text back.
  • A uv tool installed with an exact version pin is one docir self upgrade cannot 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

Choose a tag to compare

@l0kifs l0kifs released this 17 Oct 12:58

🎯 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 in docir daemon serve and docir build. 704 modules
    at startup become 570.
  • context scores its vectors ~8x faster. The cosine scan runs on math.sumprod: ~60ms
    becomes ~7ms over 1,947 vectors, and docir lint --deep drops from 2.09s to 1.12s. No new
    dependency.
  • check walks 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 holding node_modules or target/ is no longer walked file by file.

🐛 Bug fixes

  • An edit to .gitignore reaches check through the daemon. Since 0.28.0 the daemon read
    the ignore files once, at start, so after an edit it kept fingerprinting code: globs under
    the old rules — no code-drifted where docir --no-daemon check reported 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 .gitignore index per pattern — +205-251ms per check; 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 update only re-stamps its version.
  • If you edited a .gitignore while a daemon on 0.28.0-0.30.1 was running, a --verified
    or a new --code glob written through it may hold a digest the current rules disagree
    with. docir check reports it as code-changed or code-drifted; read the document
    against the code, then docir update adr-0001 --verified.

🔗 Full changelog

See CHANGELOG.md

v0.30.1 — a body edit lands where you named it, or is refused

Choose a tag to compare

@l0kifs l0kifs released this 16 Oct 12:36

🎯 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-section on a heading the body already has adds under it (GitHub #37). It
    wrote a second ## Context at 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
    --section and --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 update refuses a body no edit mode would write (GitHub #36). --body,
    --body-file or --stdin without --append-section, --replace-section or
    --replace-body printed the unchanged document and exited 0; over MCP, body beside
    replace_body was 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 update so 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 --deep as
    ambiguous-heading. Merge the two copies from docir get adr-0001 and write the result
    back with docir update adr-0001 --replace-body --force --stdin: --remove-section deletes
    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

Choose a tag to compare

@l0kifs l0kifs released this 28 Sep 09:25

🎯 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 update

A 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 duplicate finding says what to do with it. docir lint --deep names 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 context over a description before docir add, and to record a disagreement it finds
    by reading as a contradicts edge.
  • 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 only b/**, and said nothing. The seven write-side list flags — add --tags/--related/--code, update --set-tags/--set-related/--set-code and init --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 reindex with a raw IntegrityError; a repeated glob made --replace-body refuse the
    document as changed on disk, permanently. docir check now names such a file as
    repeated-entry, and docir check --fix rewrites it without the repeat and without moving
    updated.
  • docir self upgrade no longer calls a stalled upgrade "already the newest build". An
    installer can exit 0 having changed nothing — a uv tool receipt 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 update so 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 check reports repeated-entry, docir check --fix repairs it.

🔗 Full changelog

See CHANGELOG.md

v0.29.0 — a newer docir tells your agent, once

Choose a tag to compare

@l0kifs l0kifs released this 23 Sep 16:17

🎯 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: true

Creating 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 project install 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 upgrade between 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 --refresh

MCP 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_doctor gives 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_THREADS caps how many cores the embedding model may take (GitHub #23).
  • docir check --against main reports 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, --force included (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

Choose a tag to compare

@l0kifs l0kifs released this 18 Sep 12:40

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-daemon answers 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 every src/**-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
    .gitignore files, and only those: not .git/info/exclude, not your global excludes, both
    per-machine. It was live here too, through benchmarks/.coverage.
  • docir self upgrade brings the store's .gitignore up to the running build (#21).
    release-check.json was written into every store and ignored nowhere, and the file itself was
    written once by docir init and never again — so feedback/ never reached a store created
    before it. Entries are appended under a comment naming the version, never rewritten over.

🔧 Also fixed

  • docir doctor and docir daemon status survive 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.yaml is replaced.
  • docir query --code '.github/workflows/**' finds its documents.
  • docir self upgrade counts 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-drifted per 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-drifted is a per-build opinion; nothing refuses anything.
  • The model is re-downloaded once. It is the last one a sweep can force.
  • docir self upgrade tops up the store's .gitignore and names what it added.

🔗 Full Changelog

See CHANGELOG.md

v0.27.0 — the glob that watches from the moment you write it

Choose a tag to compare

@l0kifs l0kifs released this 17 Sep 14:38

🎯 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 check reports
    code-drifted when the files behind a pattern stop being the ones the document was pointed
    at — no review step to reach first. docir update <id> --verified upgrades 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 --fix writes 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, ask docir_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

Choose a tag to compare

@l0kifs l0kifs released this 12 Sep 14:01

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 doctor reports only
    stale-index-build until the first docir reindex after upgrading. docir self upgrade
    runs it and refreshes the generated instruction files, whose maintenance reference now
    documents unresolved-link.
  • A store that already writes [[...]] sees its broken ones on the next docir check.
    They were broken before; this is the first build that says so. Each is a warning, so
    docir check --strict still passes, and docir check --fix leaves 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

Choose a tag to compare

@l0kifs l0kifs released this 06 Sep 10:38

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_TIMEOUT now answers "has the daemon died?". There is no corpus size it has to
    be raised for; if you raised it, put it back.
  • Migration 0012 adds embeddings.input_digest and 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 0012 is 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 doctor reports it as index-from-newer-build. Upgrading
    everyone who shares a store is the shorter path.
  • Run docir reindex after upgrading, or docir 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.