Skip to content

Releases: dat999zx/knowl

v5.22.1

Choose a tag to compare

@github-actions github-actions released this 07 Sep 04:55
cf92eb1

The skill run banner no longer corrupts the MCP transport. runSkillPackage wrote its banner with console.log, and it has two callers that disagree about what stdout is: on the CLI it is the operator's terminal, under knowl serve it is the JSON-RPC frame stream. knowl_skill_run calls the same function inside a stdio MCP server, so the banner was interleaved into the protocol and the client failed to parse the response to a call whose skill had actually run — an action taken, reported as a transport error. It goes to stderr now, the choice knowl serve already makes for its own startup banner.

Creating the global store no longer rebinds the process. ensureGlobalStore bootstrapped ~/.knowl/global.db through initDbPath, which assigns the module-level context every unscoped store operation resolves through. On the CLI that is invisible because the process exits; under knowl serve the process outlives the call, so one knowl_store with namespace: 'global' rebound the ambient database and every later project write in that session went to the wrong file while reporting success. Bootstrap runs through withDbPath instead.

A caller-chosen revision reaches git as an operand. listChangedFilesSince and listRenamedPathsSince interpolated a commit into an argv option position, and the caller is not always the engine — knowl_drift exposes since as an MCP tool argument accepting any string, so a prompt-injected agent chooses it. spawnSync runs without a shell, so this was never command injection; it was git-option injection, which is enough, because git diff --output=<path> exits 0 and writes the diff over the named file. The separator is --end-of-options and deliberately not --, which to git diff means "pathspecs follow" and would silently demote the range to a path.

--local excludes in the store that holds the atom, on both surfaces. The global branch of knowl store printed "Marked local. It will not be published." and recorded nothing, so the promise was made by the message alone. knowl_store had the same gap in a different shape: it ran the exclusion outside the withDbPath scope that wrote the atom, so for every non-project namespace the atom landed in one database and its cloud_excluded row in another. The namespace store's publisher reads its own exclusion table, finds nothing, and stages an atom the caller was told would never be published — and the global namespace is not exempt from that seam, since maybeAutoStage skips only session and the machine store can itself be cloud-connected.

v5.22.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 16:56
632b340

OpenClaw in-process plugin integration. OpenClaw is supported via an in-process plugin package (@dat999zx/knowl/plugin and integrations/openclaw) rather than shell hooks. Running inside the OpenClaw gateway evaluates the write gate at before_tool_call in sub-millisecond time (~0.68ms vs ~118ms subprocess) with an explicit matcher filtering write tools (exec, apply_patch, spawn_agent). The write gate carries an internal 5-second deadline and swallows engine errors to uphold OpenClaw's fail-closed host contract without blocking user writes on memory degradation.

Prompt recall is delivered at before_prompt_build returning { prependContext: card } with the fixed orientation card, ensuring prompt prose never becomes a search query. Impact cards are injected before model replay via api.registerAgentToolResultMiddleware directly into result.content (avoiding details which OpenClaw strips before compaction). That middleware matches every tool; only the impact-card lookup is gated on the write tools that can produce one.

Mid-turn cards reach plugin hosts. Hermes and OpenClaw both returned undefined from midTurnContext, and host-lifecycle.ts gates the entire mid-turn slot on that value — so change cards, destructive-command lessons, fleet cards, skill nudges, turn-capture prompts and the drift reminder were all silently dropped on two hosts. Both now park the engine's card in their observer hook (post_tool_call / after_tool_call) and deliver it from the hook that rewrites the tool result. The card arrives one tool call late, which is affordable only because every card in that slot is advisory; the write gate keeps its synchronous fail-closed path. The result middleware now matches every tool rather than only the writers: the drift reminder counts consecutive non-Knowl calls of any kind, so a write-only matcher dropped it during exactly the read-and-shell runs it exists to interrupt. midTurnDeliveryVerified is now true for Hermes: driving the real plugin hooks against the real engine in a real project, the drift reminder arrived on tool call 11 of 14 — the tick DEFAULT_DRIFT_REMINDER_EVERY = 12 predicts. It stays false for OpenClaw, whose delivery has not been observed.

Namespace reads now reach the store that writes reach. The global store is addressed by its known path, but every namespace list was built from project config — and knowl init writes no memory.global block. So in every repository Knowl creates, an atom written with namespace: 'global' was unreadable by the surface that wrote it: knowl query and knowl_query keyword search both missed it, and every id-addressed operation failed on an id search had just printed — knowl supersede and knowl reviewed with "Knowledge item not found", knowl_query --id reporting it did not exist, knowl_timeline answering []. Reported as "Knowl is bad at superseding". withItemNamespace now resolves an id in whichever namespace holds it and runs the caller's entire operation there, write included; both keyword search paths union the global store's known path.

knowl init openclaw merges the plugin entry into openclaw.json (project-scoped or ~/.openclaw/openclaw.json), writing both required permission gates (allowConversationAccess and allowPromptInjection) and explicitly setting timeouts.before_tool_call: 5000 while preserving all surrounding user configuration. It also copies the plugin into ~/.openclaw/knowl-plugin, the way knowl init hermes has always copied its own — previously the command wrote config enabling a plugin OpenClaw had never been told about, reported success, and left a dead install that verify() agreed was configured because it only read the config file. The printed instructions name the two remaining steps and why each is mandatory: the dependency install needs --install-links, because OpenClaw's safety scan refuses a plugin whose node_modules symlink outside the install root and a plain npm install <path> produces exactly that; the registration needs --force (the directory is outside ClawHub trust metadata) and --accept-capabilities (the plugin declares tool-result middleware).

v5.21.1

Choose a tag to compare

@github-actions github-actions released this 05 Sep 04:54
dc53f3e

A project can be held open per consumer, without the process-global handle deciding for
everyone.
initDb writes to one module-level context and getDb/getClient read it, so two
projects open at once in one process silently share the last one opened: reproduced by opening A
then B and issuing a write through the caller that held A, which landed in B's file — A's store
empty, B's holding A's row, no error and no log line. closeDb is the other half, because it
releases the whole pool, so the first consumer to finish tore down every other one's connection
and the survivors got Database has not been initialized from a call they had made correctly.

No shipped consumer is affected, and this is hardening rather than a bugfix. Every one of
them opens exactly one project per process — the CLI is one-shot, and the MCP server binds a
project at startup and hops namespaces with withDbPath, which has been scoped since 3.0.1. The
subprocess boundary has been hiding the defect; the in-process library export planned for the
OpenClaw plugin removes it, because a long-running gateway holds several workspaces open in one
address space.

openProjectScope(root) returns a handle whose run(body) executes inside the same
AsyncLocalStorage the namespace hop already uses, so ambient reads resolve to that project
rather than to whoever opened last, and whose release() maps to releaseClient — one database,
leaving the pool alone. withProjectScope(root, body) is the open-run-release form. Two handles
on one project are refcounted, and a database the process-wide context is also using is never
released by a scope. Nothing assigns the global handle, so initDb/closeDb and every existing
call site behave exactly as before.

A Hermes session with Knowl selected as the memory provider gets its own card back. Selecting
Knowl in Settings > Memory & Context replaced the orientation card with keyword search over the
user's literal sentence: pre_llm_call returned nothing when memory.provider was knowl, and
the provider's prefetch ran knowl query <the prompt> --limit 5 on every turn instead. Recall
is the hook's job again on every host, whatever the dropdown says. system_prompt_block went with
it — register already publishes the rules section, and both halves loaded on such a session, so
the rules were in the system prompt twice.

v5.21.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 12:55
decb425

The code index reads Python and Go. The Tree-sitter index behind symbol:// locators, the
per-symbol read set and certain-tier impact detection covered .ts, .tsx, .js and .jsx
only; a .py or .go file was skipped without a word, so a session that read one recorded a
single file:// row and a comment-only edit reported it as moved. Both languages now index:
Python def/class definitions (decorated or not), class members as Class.member, module-level
assignments and both import forms; Go functions, methods qualified by receiver type as
Type.Method, type specs and aliases under a new type kind, const/var specs and every
import path. A Python or Go function's signature stops at the end of its header — a Python
block begins at its first statement, so the comment lines above it are cut too — and a body edit
leaves the hash alone while a parameter or decorator change moves it; a struct or interface is cut
at its keyword like a class at its brace, so a field comment does not move it either. __pycache__,
.venv, venv and vendor join the directories the walk never enters — a first-party directory
by one of those names loses its rows on the next pass, and symbol evidence citing them reads stale
from then on — and go.sum, poetry.lock, uv.lock and Pipfile.lock join the churn paths
drift ignores. Two more prebuilt grammars, no install-time compilation on any platform CI runs.

A Hermes session whose folder is not a Knowl project now says so, once. Every lifecycle
event returned early and silently in that case, so the plugin loaded, hermes plugins doctor knowl reported two tools and seven hooks, and nothing recorded or recalled anything -- a state
indistinguishable from a healthy integration. The common way in is that knowl init hermes is
machine-wide and one-time, while knowl init is per repository, so opening a repo that never had
the second is easy.

The note names the folder and points at knowl init, and it is said once per session rather
than once per event -- seven events a turn would make it noise. It is withheld entirely from
someone with no machine-wide store, where an unsolicited "run knowl init" would be an advert
rather than a diagnosis.

Two smaller corrections fell out of the same path. The card's heading claimed "no project open
for this session" even when a folder was open, and its advice was "open a repository as this
session's folder" -- which someone who already had one open reads as a broken diagnosis. Those
two situations now get different wording, because they need opposite remedies. Closes #250.
The machine-wide store can sync to a cloud workspace, and knowl cloud finds it on its own.
Personal defaults were local forever: everything under src/cloud/ takes a project root and reads
that project's pointer, so ~/.knowl/global.db had no route to a workspace and a new laptop
started empty.

It needed no server change, no schema change and no new gate, because the machine store was
already almost a project. loadConfig has always substituted ~/.knowl/config.json when handed
the machine home; global.db already carries the cloud ledger tables; publishing stopped
consulting the git gate on 2026-08-13, so a directory that is not a checkout can still push; and
connect --repo already existed for a project with no git remote. The one thing missing was the
matching substitution in initDb, which resolved ~/.knowl/.knowl/knowl.db — nobody's store, and
an error on open. With that pair aligned, a root is again the only thing a cloud command needs.

  • --global on connect, stage, unstage, push, pull, status and autopush, the
    same word knowl init --global already uses: act on the machine, not on this directory.
  • Outside a repository the machine store is used automatically, with a line on stderr saying
    so. The inference is narrow on purpose: only when there is no project above the directory at
    all. A project whose config will not parse is an error about that project, never quietly
    answered from personal defaults — the same distinction the MCP server draws, and kept as its own
    named predicate for the same reason: findProjectRoot raises exactly one error type, so a
    widened guard cannot be caught end to end. Verified by mutation, where broadening it passed
    every behavioural test.
  • Auto-staging works there too, and needed no code of its own: with the database seam fixed, a
    write to the machine store resolves the machine config, finds its pointer and queues the atom
    exactly as a project write does.

send, receive and retract are unchanged. The first two are a person-to-person transfer
rather than workspace sync; the third acts on one already-published id.

Choosing the workspace is unchanged -- --workspace <id>, or the same picker when you belong to
more than one -- and the machine store publishes on the same terms as a project: connect writes
a pointer and sends nothing, push asks first unless given --yes. It connects under the name
personal, because with no git remote the identity would otherwise fall through to the directory
name .knowl: unreadable in a listing, and identical for every person, so two people connecting
their machine stores to one workspace would collide. --repo overrides it.

Global skills: reusable playbooks with project bindings. A skill can now live once on the machine (~/.knowl/skills/<name>/) as a reusable playbook, while each repository provides its own commands and paths via project bindings in .knowl/config.json. A playbook and a binding are two keys: neither runs anything alone.

  • Layering and Shadowing: Project skills shadow global skills of the same name. knowl skill list identifies whether each skill is project or global.
  • requires block: Manifests (skill.yaml or skill.json) declare inputs, capabilities (process, network, write, publish, delete), and fail-closed preconditions (clean_worktree, on_branch:<name>, command_exists:<bin>).
  • Strict interpolation: Only ${inputs.*} is substituted; shell expansions, environment variables, or missing inputs fail closed before running. A shell entrypoint cannot interpolate at all — see below.
  • Approval and planted-package protection: knowl skill approve <name> --global records trust in ~/.knowl/skill-trust.json. Capabilities with external effects require explicit confirmation. A repository shipping both a local skill and a binding cannot self-approve.
  • Visible run banner: Every run displays a banner with the fully resolved command, working directory, declared capabilities, and verified preconditions. Capabilities are declarations, not a sandbox.
  • Pinning and provenance: Bindings can pin a version; manifests track origin provenance.

A shell skill entrypoint can no longer interpolate ${inputs.*}. A shell entrypoint is a
command string, so a bound value was spliced into it as syntax rather than as a value:
deploy ${inputs.target} with target bound to staging; curl x | sh is two commands. The rule
already existed one branch earlier -- a shell entrypoint refuses runtime arguments because "no
quoting is correct for both cmd.exe and POSIX shells" -- and interpolation, which arrived with
global playbooks, carried the same hazard with a sharper edge: a binding comes from a project's
config, so a repository could decide what an already-approved global playbook runs, and approval
would stop covering everything that determines the command.

Both safe routes remain, and the refusal names them. Every bound input is now exported as
KNOWL_SKILL_INPUT_<NAME>, which the command reads rather than the shell parsing it; or use a
script entrypoint, whose arguments are passed as an array and never reach a shell. Script
entrypoints are unaffected, which is what the documented example already used.

v5.20.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 08:18
622ac48

A global memory layer, and the layered read that makes it reachable. Knowl has had four
namespaces since long before this release -- session, project, organization, global -- with
precedence, round-robin interleaving and per-row embedding identity all implemented. None of it
could be read: the layered reader ran only when vector search was off, and vector search is the
default, so a configured global or organization namespace was written to and never queried.

The reader now spans namespaces under vector search. Each one is searched with its own
embedding identity, resolved from its own config root -- the project's for session and
project, the Knowl home for the standalone stores -- because searchKnowledgeEmbeddings filters
on that fingerprint and scoring a 768-dimension query against 384-dimension rows is meaningless.
A namespace whose profile cannot be served is skipped and named, never silently dropped.

What that unlocks:

  • ~/.knowl/global.db, a machine-wide store for what is true of you rather than of a
    repository: preferences, machine quirks, conventions that hold everywhere. A file beside the
    machine home rather than a project at it, because knowl init at ~ would put a store on top
    of models/, cache/, repos.json and credentials.json.
  • knowl link global [--off], per project and reversible. Project answers still outrank
    global ones, so linking never changes what a repository says about itself.
  • knowl store --namespace global, where every --path must be absolute: a relative path
    names nothing in a store that spans repositories. Those paths are provenance for a reader and
    are not indexed -- impact detection, drift and evidence staleness stay project-only, and the
    write says so rather than looking wired up.
  • Sessions with no project at all -- knowl outside a repository, or a Hermes Desktop window
    with no folder open -- resolve to global alone, which is the difference between having memory
    there and having none. Only when there is genuinely no project: a repository whose config is
    malformed is an error, never quietly answered from someone's personal defaults.
  • knowl init --global runs anywhere, setting up the machine-wide store and any hosts
    named beside it without writing anything into the current directory — which is how a
    machine-wide host such as Hermes is wired in one command. Plain knowl init still
    initializes the directory you run it in.

Hermes Agent is driven by a plugin now, and it reaches Hermes Desktop. 5.19.0 wired Hermes up through the hooks.<event> shell commands its config.yaml accepts. Those are terminal-only: the serve backend Hermes Desktop launches takes a fast path that never calls register_from_config, so not one of them is registered there (upstream hermes-agent#69825), and hermes hooks doctor reports them healthy regardless because it reads the config file rather than the live registry. Python plugins load from agent/agent_init.py, which every path builds an agent through. So knowl init hermes now installs integrations/hermes/knowl/ into the Hermes plugins directory, enables it in config.yaml beside the MCP entry, and removes the shell hooks the previous version wrote — registering both would send every event twice. The plugin sends exactly what a shell hook would have sent, and adds three things a subprocess cannot: the project resolves from Hermes' per-session working directory instead of the backend's, the memory rules ride in the system prompt, and a file write gets a same-turn impact card appended to its result. Restart Hermes after knowl init hermes to load it.

The plugin also registers knowl_query and knowl_store as Hermes tools of its own, and that is not duplication of the MCP server — it is the only correct channel on Desktop. knowl serve resolves the project from its own process directory, and Hermes Desktop runs one server for every project from a directory that is not any of them, so its mcp__knowl__* tools report No Knowl project found while the store is healthy. Pinning mcp_servers.knowl.cwd would fix one repository and silently answer from it in all the others, so init does not set it. The plugin's two tools run in the session's own directory instead, which is right however many projects are open; everything they do not cover is a knowl <command> away in the agent's terminal.

The Hermes bootstrap card was being thrown away, on every host path. The profile registered on_session_start, so the engine bound the session and spent the bootstrap card on an event whose return value Hermes discards — and the first real turn then arrived on a session the engine had already seen, with nothing to say. Measured: a fresh session whose first event is pre_llm_call gets a 3,030-character card; the same session preceded by on_session_start gets an empty answer on both. That event is no longer registered, so the first turn binds the session and carries the card.

Knowl can be Hermes' memory provider, not only its plugin. Hermes has a first-class slot for
a memory backend -- Settings > Memory & Context > Memory Provider, beside Mem0, Honcho and the
rest -- and it scans $HERMES_HOME/plugins/ for candidates, which is exactly where
knowl init hermes already installs. Knowl now appears in that dropdown with no extra step and no
second install path; selecting it is optional and additive.

The two surfaces are complementary rather than alternatives, because neither reaches what the
other does. A MemoryProvider gets no tool-level event at all, so the write gate and the
same-turn impact card can only be hooks. The hooks have no compaction event and can only append
to the user message, so three things can only be the provider:

  • Recall in the system prompt, where instructions belong, instead of appended to the message.
  • Hermes' recall indicator, which reports what was injected without depending on the model to
    mention it.
  • A checkpoint before compaction. Hermes fires no hook before it compresses a conversation,
    so without this a session's knowledge is summarised away before capture ever sees it. The
    engine normalizes it to the same checkpoint event PreCompact maps to elsewhere.

One directory serves both, which needs two things to stay true. Hermes imports it twice -- the
plugin manager for the hooks, plugins/memory for the provider -- and the collector it passes
the second time forwards register_hook to a real plugin context, so the module name is what
decides which half registers; registering both would fire every event twice. And plugin.yaml
must keep its explicit kind: standalone: without it Hermes sniffs MemoryProvider out of the
source, reclassifies the plugin as exclusive, and skips it entirely -- silently taking every
hook with it. Both are covered by tests.

When Knowl is the selected provider the pre_llm_call hook stops injecting its card, so recall is
never delivered twice. It still fires, because that is what binds the session and carries capture.

The Hermes plugin's tests now run in CI. They are Python, so npm test never reached them and
neither did anything else -- 31 tests sat in the repo running nowhere, including the ones
asserting the hook payload shapes Desktop depends on. They run on the ubuntu lint job's
preinstalled python via npm run test:plugin, stdlib unittest only, no pip install.

In the same job, audit:prod now retries when the registry is unreachable. npm audit exits 1
both when it finds vulnerabilities and when it cannot reach the audit endpoint at all, and on
release day npm returned 503 Service Unavailable on four runs while flapping -- one succeeded
in between -- burning seven minutes each time on npm's own internal retries. Only the second case
is retried: a real finding still fails on the first attempt, and an outage that never clears still
fails the build. The gate is not weakened, it just stops going red when the service is flaky.

Antigravity recorded nothing, four independent times over, and knowl fleet had never listed
one of its sessions.
The payload is protojson: every key camelCase, the session
conversationId, the root workspacePaths, the tool one toolCall: {name, args} object. The
stdin allowlist carried none of those names, so every event reached the normalizer empty and threw
IncompleteHostHookPayloadError -- which the hook entry swallows in silence. No row, no log, no
symptom, identical to a host nobody had configured. Three of the five registered events were also
written in a shape Antigravity parses and ignores: only PreToolUse and PostToolUse take the
{matcher, hooks} wrapper, and PreInvocation -- the one event that starts a session there -- is
a bare handler list. The tool names were wrong too: the real writes are replace_file_content,
multi_replace_file_content and write_to_file, the read is view_file, and the shell is
run_command. All of it is now read off the installed bundle and five real transcripts rather
than quoted from documentation, and docs/hosts.md says so. Payload remapping is one new profile
member, normalizePayload, applied once before anything reads a field.

Codex lost two thirds of its shell commands, found while verifying the above. isShellEvent
delegated to the shared helper, which knows bash and shell -- but across this machine's codex
sessions the tool is called shell_command 14,329 times against shell 2,059.

knowl cloud push can drain a queue again. Two independent faults could each leave staged
knowledge unsendable indefinitely.

--yes passed the strict snapshot check, which refuses when the queue merely GREW since the
snapshot was taken. That check exists to protect what a human read at the prompt, and --yes
shows no prompt — so with auto-staging on, any agent writing beside the push (including the
session ...

Read more

v5.19.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 16:23
189d168

Hermes Agent at Claude Code parity: knowl init hermes.

Hermes Agent is configured through the shell hooks in its own config.yaml, which take Claude Code's wire format: knowl init hermes writes mcp_servers.knowl and one hooks.<event> entry each for on_session_start, pre_llm_call, pre_tool_call (matched to write_file|patch), post_tool_call, pre_verify, on_session_end and on_session_finalize. pre_llm_call carries the turn card, pre_tool_call blocks a refused write on exit 2, and pre_verify — which fires before an edit turn finishes and accepts Claude's {"decision": "block"} — carries the capture nudge, so Hermes reaches every capability. Init edits config.yaml as a YAML document (comments survive) and never runs hermes itself, whose own mutators rewrite the file without comments and can stop on an interactive prompt. Hermes asks for consent once per hook at the terminal on first use; gateway and Hermes Desktop runs need that approval or hooks_auto_accept: true. Home is HERMES_HOME, else %LOCALAPPDATA%\hermes on Windows, else ~/.hermes.

User-owned YAML config files (config.yaml) are merged as documents: every comment survives (comment blocks may be re-indented to sit with their key), the file's line-ending convention is kept, and a file that fails to parse is reported and left untouched. yaml is now a direct dependency.

Antigravity is two products reading two MCP files, and knowl init antigravity now writes both. The IDE's "View raw config" opens ~/.gemini/antigravity/mcp_config.json; the agy CLI reads ~/.gemini/config/mcp_config.json, which Gemini CLI's migration often leaves at 0 bytes. The adapter had pointed at the IDE file alone, so the CLI's /mcp stayed empty. An empty JSON config is now read as "no servers" rather than a parse error, and a JSON MCP target takes a list of files: detect reports configured only when every file holds the entry, and configure merges into each.

v5.18.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 05:01
512a2b8

Hooks can run on the MCP server the host already holds open, and file evidence can finally
go stale.

File evidence can go stale, which the README has promised since it was written.
isEvidenceStale compares a file's current hash against the one the evidence recorded, and no
shipped writer ever recorded one: affectedPaths became file evidence with contentHash NULL, the
one module that hashed files has no importer, and the MCP schema has no field for a caller to
supply one. Symbol evidence went stale; file evidence could not. The obvious fix — hash every cited
path — would have turned an agent's unverified assertion about a file into a staleness claim about
it, and that gate was real. The read-set is now the gate: a cited path some session provably
opened (work_read_sets holds a file:// or symbol:// row for it, captured from the tool
stream and never from the agent's own report) is hashed from disk at write time; a path merely
declared stays unhashed and never reports stale. One disk read per observed path per write, on the
write path only. session-evidence.ts is untouched and still unwired; whether to delete it is a
separate call (#225).

"Two independent confirmations" now means two days, and the items that had already earned it
are promoted.
The comment above VERIFY_THRESHOLD promised independent confirmations; the query
counted rows, so two knowl_feedback calls in one turn — one agent, one item, one source — promoted
an item to verified. Measured on the project's own store, every item the row count would have
promoted was a burst inside a single session: four useful events in seven minutes on one, two four
minutes apart on another. The feedback path now counts distinct days, the unit the observed-use path
already uses, and neither clears the bar. The second half is the one that matters more: promotion
was edge-triggered only, run at the instant a feedback row was written and never again, so an item
whose confirmations crossed the bar before knowl_feedback was wired to standing stayed asserted
forever — the store holds one with three useful events against a threshold of two. Session start
now re-evaluates the feedback predicate over every asserted item, capped and audited the way the
observed-use pass is, so the backlog drains instead of stranding (#223).

Hooks can run on the server the host already holds open, instead of as a process per event.
Every Knowl hook has been a command hook: a fresh knowl agent-hook process per event, measured
at ~230ms of Node startup each and paid twice per tool call, serialized against the agent's own
work because the host waits on the pre-tool hook. Over 102 real Claude Code sessions that is 31s
of startup at the median session and 190s at the 90th percentile — while the MCP server sat there
with the database open and the embedding model loaded. Claude Code 2.1.257 and Codex 0.148 can run
a hook as a call to a tool on a connected MCP server, so hooks.transport: mcp now writes the
mid-session events as mcp_tool hooks calling knowl_hook and registers that tool; SessionStart
stays a process because both hosts say it fires before servers finish connecting. Opt-in and
command by default, because moving costs a catalog entry — MCP has no hidden-tool concept, and a
34th tool against a surface already measured at ~10.5K tokens is paid only by the repositories that
asked for it. The payload travels as ${field} templates the host fills in, is rebuilt on the
server whichever way the host rendered each one, and then goes through the same allowlist the
stdin path applies, so nothing reaches the handler by this route that the other would have dropped.
Calling the tool while the transport is command is refused rather than run, so a client holding
a stale tool list cannot capture every event twice. A write the gate refuses still says why on the
server's stderr, which is where the host writes its MCP log — the same second copy the process
path prints, and worth more here, because a block whose verdict this new transport got subtly
wrong would otherwise be completely silent (#224).

v5.17.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 03:54
b8b2e60

The agent sessions running on one machine stop being invisible to each other, and an agent can
ask what its own branch broke.

A push no longer fails anonymously on an over-long field. The cloud contract caps title,
source and conflictKey at 500 characters; the local store caps nothing, so a research atom
with a long citation list sat staged until push, where the server's zod rejection came back as
Too big: expected string to have <=500 characters with no path and no id — one line per
offender, and no way to tell which two atoms of a hundred were meant short of SQL over
cloud_published. The caps are now checked before the request, and the failure names the atom,
the field and both lengths. Not enforced at write time on purpose: knowl store is local-first
and works with no cloud account, so a machine that will never push does not answer to the
server's limits (#217).

knowl doctor stops prescribing a fix that breaks publishing. A repo embedding differently
from its workspace drew a WARN saying the two sets of items were "invisible to each other", with
the remedy "align search.vector, then reindex". #191 made the mechanism false — each peer is
searched under its own profile and scored against its own range and floor — and the remedy was
worse than the warning: a cloud-connected repo's atoms must stay on the server's serving
profile, so aligning to the workspace would break every knowl cloud push. It is now an OK line
that names the per-profile mode. workspace add and workspace join still refuse a mismatch,
but say why they actually refuse: a workspace holds one profile so every repo shares one
semantic range, which is a policy choice rather than an invisibility claim (#216).

The sessions on one machine can see each other, on every host. Claude Code keeps a registry of
its live sessions and lets one message another, but records nothing about what each is doing —
and every other host records nothing at all — so two sessions hit the same failure and both start
fixing it, and a third changes the hook every one of them is standing on. Knowl now keeps the
other half in one machine-level file, ~/.knowl/fleet.db: what each session was asked, what it
wrote this turn, its last error, and which problem it has claimed by editing files after seeing
it. Every host with Knowl hooks is in the fleet and they see each other — a Codex session appears
on a Claude session's roster and the reverse — with liveness read from the host's own registry
where it publishes one and from recency where it does not. Only the sessions the host's messaging
can actually reach are offered as something to SendMessage; the rest are listed, marked, and
raised with the user instead. knowl fleet lists it from any terminal, inside a project or not,
and the knowl_fleet MCP tool lists it to an agent — registered unless fleet.enabled is
false. Two of the four switches ship active, and the line between them is what a surface can
cost you: fleet.enabled because the roster prints nothing at all when a session is alone, and
fleet.cards (enforce) because a card is advice on a channel the agent is already reading and
never a refusal. The two that would cost you something ship quiet — fleet.digest (off) spends
lines on every turn, and fleet.nudge (shadow) withholds a stop, so it records what it would
have said until you arm it. knowl posture maximal turns the digest on and arms the nudge.

An agent can ask what its own branch broke. knowl pr --since has always answered "which
stored knowledge does this diff invalidate", and it was CLI-only — so the actor most able to act
on the answer, the one that just wrote the branch, had no way to ask. knowl_drift is the same
check as an MCP tool: it takes a base ref, previews by default, and apply marks the matches as
needing review. It deliberately does not tell the team, which the CLI does — publishing a
retirement is visible to every member of a workspace, and sending stays the user's to run, the
same line knowl_cloud already draws. The automatic session-start check is a different question
and is unchanged: that one asks what drifted while you were away, this one asks what the work you
just did made false, and the diff that answers it does not exist until the branch does.

v5.16.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 05:02
2f97ae5

Knowl can be installed from the official MCP registry, and change impact stops being blind to
half of how agents actually read code.

Knowl is publishable to registry.modelcontextprotocol.io. server.json is the manifest, and
the part worth reading twice is that the registry does not take a publisher's word for ownership:
it fetches the npm metadata for exactly the version the manifest names and rejects the publish
unless that tarball carries an mcpName matching the server name. 5.15.0 is on npm without that
field, so the registry would have refused it — this release is the first that carries it, which is
what makes the publish possible at all. mcpName and the server name now have to stay equal
across two files forever, so npm run check:versions gained server.json as a fourth file rather
than the promise getting a gate of its own.

The read set sees a file opened with cat, not only with Read. The read tools were Read
and NotebookRead; a file opened through the shell arrived as a command string with no paths at
all and hit neither the read branch nor the write branch. That is not an edge case — a host
granted shell access instructs its agent to prefer cat, head and sed -n over the file tools,
and a session working that way recorded no reads whatsoever, silently, in exactly the sessions
doing the most reading.

What the parser refuses is most of the design, because a read-set row asserts that a session saw
some text and the certain tier spends that assertion by interrupting the agent and refusing its
write. So grep, rg, find, ls and wc are declined — they return matches or names, not
contents — along with git show <ref>:<path> (the agent saw a ref's text; the hash recorded would
be the working tree's), an in-place sed -i, any segment carrying a redirect, and any token the
shell would still have expanded. A read piped into anything that reports on the text rather than
passing it on (cat f | grep x, cat f | wc -l) is declined for the same reason its direct form
is. Shell reads are recorded at file granularity rather than per symbol: the shell says which
file was opened and never how much of it, and expanding a slice into one row per symbol would
assert beliefs about signatures that never reached the agent.

The write gate's own precision is printed somewhere. The measurement had existed since the
shadow gate did, computing exactly the number the bar in front of enforcing it is written against
— and nothing imported it. Shadow mode was faithfully recording every refusal an enforcing gate
would have issued into a table whose verdict no command could read, which is the same defect as
not measuring at all: a score nobody can see cannot promote the thing it measures, and cannot
retire it either.

🛡️  WRITE GATE (shadow)
  Refusals withheld:     60
  Adjudicated:           48 of 60
  Precision:             87.5% (6 false positive(s))
  Bar to enforce:        ≥95% over ≥40 adjudicated — not cleared

The bar is printed beside the number on purpose: a precision figure alone invites "87% sounds
fine". Both halves fail differently and are reported separately, because 100% over three findings
is not evidence and this block has to say so rather than look like a pass. Nothing has been
adjudicated yet reads as not yet measured, never 0.0% or 100% — no evidence is not a perfect
score.

The staleness marker names what to open. A row whose cited files moved used to report a
condition and leave the reader to diff affectedPaths against the working tree to find the
target. Measurement on exactly that situation — a served claim whose source had moved, with the
link present and reachable — found agents opened the source in roughly one turn in five and acted
on the superseded value in about three quarters of the rest, and a content-free freshness cue did
not move those numbers. What moved them was an instruction naming the target, on the path the
reader was already on. So the sentence leads with the verb and the filenames and the count follows
it, capped at three names plus and N more so an atom citing thirty paths still reads as one line.

Fixed

  • A correction elsewhere was promoting the atom it made doubtful. The session card's three
    knowledge slots were ordered by updated_at, which moves on supersession, archival, visibility
    promotion and a freshness flip — 72% of items on a 950-item store carry one newer than their
    valid_from. Marking a sibling needs_review stamps that column, so correcting one atom
    promoted unrelated atoms onto the next session's card for having just been flagged as
    doubtful
    , evicting whatever that session had actually learned. Housekeeping could take all
    three slots. The card now orders by the open assertion's valid_from, which moves on
    restatement and on nothing else; an item with no open assertion still falls back to updated_at.
  • A fork was handed a second copy of what it already inherited. A fork is the one subagent
    that is not context-poor — it inherits the parent's whole conversation, system prompt and tool
    definitions, so the parent's own session card and the workflow rules were already in front of it
    when the subagent bootstrap fired. It now gets neither. Only the context is skipped: a fork's
    reads, writes and tool events stay attributed exactly as any other subagent's.
  • A conflict-repair test asserted an order it had never established. Both rows were created by
    back-to-back calls, ISO timestamps are millisecond-granular, and on a fast machine the repair
    fell through to its own id tiebreak — so "newest wins" was decided by which random hex sorted
    first. Failed 9 times in 12 under a forced tie. The production sort is unchanged; it is
    deterministic by construction, which is what its comment always claimed.

Documentation

  • docs/reference.md carries the staleness marker's new wording, which its previous text quoted
    verbatim and would otherwise have contradicted, and documents what the shell-read parser
    recognises and — at greater length — what it declines.
  • The README feature list gains the shadow write gate's precision surface, alongside the recall
    gap and the un-restated claims report.

Chores

  • GitHub Actions group bumped (codeql-action 4.37.8 → 4.37.9, ai-plugin-scanner-action
    1.2.533 → 1.2.551) and dev dependencies (eslint 10.8.0 → 10.9.1, typescript-eslint
    8.66 → 8.68).

v5.15.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 16:40
1d29fd3

Knowl can see who it is failing, and stops cutting the card that tells them.

A subagent in a linked workspace received no skills and no knowledge at all. Both paths that
build the session card rendered it at full width and let the caller slice the finished string, so
anything charged against the budget first pushed the cut backwards through a section boundary. On a
four-repo workspace the repo list alone is 1,034 characters against a subagent's 853-character
budget: the child got a header, a repo list severed mid-entry, and nothing else. Skills are the half
that cannot be recovered — recent knowledge can be found by querying, but a peer repo's shared skill
is findable only by an agent who already knows it exists, which is exactly the agent who does not.
The card is now composed to the budget rather than sliced down to it, so the formatter's own
clamps apply instead of a blind cut. The parent path had the milder form of the same bug: with all
three warning producers at their ceiling, 1,310 characters of warning cut the knowledge section off
entirely.

The cap is no longer optional. An omitted cap used to select the slicing path, which meant three
further callers — the knowl agent lifecycle session-start CLI, session-start on a host that
shares its binding, and turn-start before a session binding exists — were still cutting cards
delivered to live agents. There is now no way to ask for a sliced card.

A subagent's recall is no longer pooled into its parent's. A subagent shares its parent's
session id, so every child's recall landed on the parent's row and the one population most worth
looking at was the one the measurement could not isolate. knowl status now splits the ratio:

  Retrieved when held — main thread: 62% (128 held)
                        subagents:   31% (44 held)

Printed only when both sides have observations, so a comparison is never made against a population
of one. Additive column, no backfill and none possible — an observation already written cannot be
re-attributed, because the identity was never on it.

The knowledge no drift check can reach is now dated. Drift watches files; roughly half the store
cites none. A new knowl status block reports how long since anyone last restated those claims, by
category, and names the ones furthest past their own category's cadence. Report only — nothing flips
freshness, because for prose there is no evidence a claim became false, only the absence of anyone
reaffirming it.

It ranks rather than flags, and that is what makes it shippable today: a cutoff cannot be calibrated
on a store younger than the cadence it is measuring, but an ordering needs no cutoff and sharpens on
its own. Ranking on plain age was tried and measured degenerate — a store is seeded in one batch, so
an age-ranked list is that seed with every row tied at the store's own age. The ratio to a category
median asks the useful question instead: is this claim unusual for its kind.

Fixed

  • The Claude Code plugin registered no MCP server at all.

Documentation

  • docs/reference.md covers both new knowl status blocks, and the README feature list gains
    the un-restated claims report and the recall gap's main-thread/subagent split.
  • Parallel agents in git worktrees share the main checkout's store.