Releases: ReidenXerx/bearing
Release list
1.0.12 — an uninstall that stops hiding, and a warning that stopped naming what it forbade
1.0.11 was tagged but never published to npm, so its changes ship here. If you are coming from
1.0.10, this release contains both.
Fixed — a stealth uninstall left bearing wired in, and hid that it had
git status came back clean, so the repo looked untouched. It was not: eleven guards were still
registered in .claude/settings.local.json, every one of them pointing at a hook script the same
uninstall had just deleted — a failed spawn on every session start, prompt and tool call.
mergeClaudeSettings writes to settings.local.json under stealth and settings.json otherwise;
removeClaudeSettings only ever knew the second. The visible path was fine, which is why no
existing test saw it — they all install visibly.
Two things made it worse than a leftover file:
- The concealment survived too.
removeExclude()existed and was imported intokit.mjs, and
was never called. So the leftovers stayed hidden andgit statusreported clean — the repo
looked uninstalled precisely because the hiding mechanism outlived the thing it hid. .bearing/contract.mdwas never recorded. 21KB of generated contract, written at install and
absent frommanifest.files, so uninstall could not know it owned it (NS-22).
All three fixed, with the negative control: a user's own hooks, permissions and .git/info/exclude
entries survive untouched.
Fixed — the block named npx gitnexus analyze in order to forbid it
Reported with a screenshot. The block correctly said node scripts/bearing-agent.mjs refresh, and
the agent ran npx gitnexus analyze anyway — because bearing told it to. The message ended "Run
yourself — never ask the user to run npx gitnexus analyze", and the only concrete command in that
sentence is the one it meant to prohibit, so it reads as the instruction. Following it lands on
the raw indexer and reintroduces the npx invocation the command resolver exists to remove. Naming a
command in order to forbid it is naming it; the message now names none.
The same screenshot showed the agent passing --skip-agents-md by hand, which bearing should do
itself under stealth. analyze writes its stats block into AGENTS.md / CLAUDE.md and the
stabilizer strips it after — but in between, the repo is dirty, so anything reading git status
in that window sees bearing having modified tracked files. Not writing it beats writing and
reverting. Passed on every stealth tier; the stabilizer stays as the net for an indexer run bearing
did not launch.
1.0.10 — minions: fan out to gather, and keep the thinking
Added — Minions, a fifth module: fan out to gather
The README carries a generated diagram for it like every other module — the fan-out is only half
the picture, so it shows what comes BACK (citations, not opinions) and where the conclusion is
drawn.
Your agent can already spawn subagents. What it does not know is when it should — so it grinds
through forty files serially, or samples five and generalises. That judgment is the module; the
fan-out is plumbing.
Fan out when the work is bounded, verifiable, independent and wide (3+ units): every call site
of a symbol, every file still on the old API, every migration site, every route to audit against one
rule. Don't when the judgment is the work, when the answer only survives verbatim, or when the
unit needs context the subagent was never in.
Each subagent carries the project's north-stars and pinned persona, and returns a fixed shape:
FOUND src/fees.ts:88 — const fee = gross * RATE
CHECKED rg "\* RATE" src/ --type ts
MISSED dynamic dispatch in src/plugins/ — could not resolve
Minions gather; your agent concludes — they do minimal or zero reasoning. A subagent returning a
verdict puts a cheaper model's summary between the evidence and your decision, which is the drift
this product exists to prevent. And the CHECKED/FOUND split is load-bearing: CHECKED filled
with FOUND empty means it looked and there was nothing; both empty means it never understood the
task. In prose those are the same sentence — the same error as reading a graph zero as absence.
Claude Code only, since it is the only runtime that can spawn subagents with a model choice — the
README says so rather than implying parity.
Added — the fan-out trigger is now enforced, and citations are checkable
Four follow-ups that turn minions from instructions into something with feedback.
- A nudge when you are grinding. The trigger lived only in the always-on contract, which means
it fired when the agent happened to recall it. A PostToolUse hook now notices 8 distinct gather
targets in a row with no delegation and suggests fanning out — once per session, advisory, never a
block (NS-5). Distinct targets, not calls: re-reading one file while editing it is not grinding,
and a nudge that fires during every edit loop gets ignored forever.minionFanoutThreshold: 0
disables it. node .bearing/lib/verify-citations.mjs src/a.ts:88prints what is actually on each cited
line and exits non-zero if any do not resolve. "Spot-check one per minion" was advice; a
fabricatedfile:lineis the one failure the return shape cannot catch by itself. Deliberately
not an npm script — those are all owned by the GitNexus module, so a minions-only install could
not run it.- Fan-outs and grind-nudges are counted in the scorecard, so the module can be measured rather
than assumed — the same reason the gates keep a tally instead of asserting they help. - Subagents inherit graph-first. A minion grepping for call sites in a repo with a graph is
doing the exact thing the gates exist to redirect, one level down where no gate can see it.
Also fixed: the context-pressure nudge still named the old shared .bearing/.task-core.md, so it
sent the agent to write a core its own recovery would not read.
Added — minions run on a middle tier, and wanting a smarter one is a smell
sonnet by default, overridable per machine via minionModel in .bearing/hooks.local.json. If
the tier is unavailable the fan-out runs anyway — a costlier minion is a nuisance, a skipped unit is
a hole in the answer.
The tier is not a cost setting, it is a consequence: a middle model is sufficient precisely
because minions do no reasoning (NS-24). Which makes it a diagnostic. If you want a smarter
minion, you delegated judgment — the fix is the split, not the model. Same when a unit keeps
returning MISSED: do that unit yourself rather than re-running it on a bigger model.
Changed — one spawn harness, shared by microscope and minions
Both modules send work to anchored subagents, and the mechanics are identical: the same pinned
persona from .bearing/domain.json, the same north-star subset, parallel where the runtime allows
it, and the duty to say what went unchecked. That is now authored once and rendered into both
skills, with a test that fails if a copy goes stale.
The return contract stays separate, deliberately — the two are opposite on the axis that
matters. A microscope lens must reason; opinions are the entire point of it. A minion must not
(NS-24). Unifying those would either silence the lenses or let the minions editorialise.
The fan-out threshold is now 3 units, not 5 — at three they already run concurrently, so the
round-trip is paid once rather than three times.
Fixed — domain inference read the wrong things, and a missing persona stayed quiet
Found by pinning personas on real repos, and all three failures pointed the same way: a
screen-capture app and a patient-facing health platform were both branded developer-tooling.
- Dependencies are not identity.
package.json's dependency list counted as the repo
describing itself, so ajwtdependency scored as a strong identity signal — overriding the
package's own description. Almost every web app depends on an auth library; almost none are
identity products. Name, description and keywords only. \bhealth\bdoes not match "Healthcare". The word boundary fails on the trailing care, so
a package naming its own domain scored zero for it. A repo naming its domain should be the
strongest signal there is.- The analyzer's block is not the repo's voice. bearing's own contract was stripped before
inference, but the stats blockanalyzewrites intoCLAUDE.md— "indexed by GitNexus… use the
MCP tools" — was not, and that alone suggested developer-tooling for a claims platform.
And an unresolved domain is now raised where it will be seen: the installer prints the
consequence ("reviews here will be generic") rather than one warn line, and the always-on
contract carries the ask so the agent meets it every session — phrased as a job it can usually
answer from the code, asked once, taking no for an answer.
Fixed — "would be committed" sent you to a .gitignore that was already correct
Found updating a real repo installed back at 1.0.6. The machine-local check reported
.bearing/.bearing-session-primed.flag as committable, but the ignore rule was present and
correct — the file had been COMMITTED before that rule existed, and git never ignores a tracked
file however good the rule is.
The check now separates the two causes and names the exit for the one you cannot fix by editing a
file: already COMMITTED, so the ignore rule cannot help — run git rm --cached <path> (NS-6).
1.0.9 — the agent stops fearing a window it isn't in, and a broken index stops blocking commits
Fixed — the agent thought every session was a 200k one
A 1M session carrying 300k tokens read as 152% full, so the agent hedged about running out and
wrote task-cores from the first hour — permanently, since it never got less full. The window was a
hardcoded 200,000.
It cannot simply be looked up: the transcript records no window, and claude-opus-5 is the same
model id on a 200k and a 1M session. So it is corrected by evidence instead — a session cannot have
carried more tokens than it can hold, so usage above the assumed window disproves the assumption.
The observation is rounded up to a real window rather than trusted exactly, since usage is sampled
at the last assistant turn and the true ceiling is higher than whatever was seen.
Revises upward only: too small is the failure being fixed, too large merely delays a warning.
An explicit contextWindowTokens in .bearing/hooks.local.json is your own statement of fact and
still wins.
Fixed — the pre-commit hook blocked commits when the INDEX was broken
Reported from a real repo: bearing:full-pdg ran unguarded under set -e, so any indexer failure
blocked every commit. Theirs ended "graph write collapsed — 200,722 relationships produced, 64,983
readable", reproducibly, and the team committed with --no-verify for days.
Blocking a commit because an index could not be built fails the developer for something that is
not their fault and that they cannot fix from there (NS-5) — and --no-verify teaches people to
skip the hook permanently, which costs far more than one stale index. bearing's CI has always been
report-not-gate for exactly this reason; the commit hook, where a block is more disruptive, was the
opposite.
The two halves are now split (NS-8): fail open for the developer — the commit proceeds — and
fail closed for the graph — the index is marked failed, so the session brief tells the agent
Index is STALE instead of letting it answer from a broken one. The warning names how to fix it
(bearing:agent-refresh), how to report it (bearing:fallback), and how to get the old behaviour
(BEARING_PRECOMMIT=block, mirroring GITNEXUS_CI_MODE=block).
1.0.8 — an uninstall that actually leaves, and a task-core per chat
Fixed — uninstall did not leave the repo as it found it
A shakedown of install / update / uninstall across every runtime and module combination. The
install and update paths held up — idempotent over repeated runs, recorded choices preserved,
feature downgrade removing what it should. Uninstall was the weak one, and every defect below needs
a SECOND install to appear, which is why a suite that installs once never saw them.
- The Node floor stayed in the user's
package.jsonforever. Install adds
engines.node >= 22.9.0because bearing's own scripts need it; uninstall never took it back. Left
behind it is enforced by npm underengine-strict, by Yarn always, and by CI — so a project on
Node 20 fails to install because of a tool it removed. Now recorded at install and removed at
uninstall, and a floor the user set themselves is never touched in either direction. - Uninstall restored bearing's own Cursor config. A backup answers "was there a file of the
user's here before bearing?", and only a first install can observe that — by the second run
.cursor/hooks.jsonexists because we wrote it. So update backed up our own file and uninstall
faithfully restored it, leaving Cursor registering hooks whose scripts the same uninstall had just
deleted: a failed spawn on every session start, prompt and tool call, in a repo the user believes
is clean. The earlier install's answer now wins, per adapter so a runtime added later still backs
up a genuinely user-owned file. Uninstall also strips a leftover registration written by an older
version, and only when the entry is the one we write — a user's owngitnexusMCP server survives. - Empty
.zed/and.cursor/shells were left behind. Install creates these in repos that
never had them; uninstall wrote back{"context_servers":{},"agent":{"profiles":{}}}and stopped.
Both are now removed once nothing of the user's is left in them — and a settings file holding
their theme, keymap or own servers is left exactly as it is. - An intel-only contract pointed at a section that isn't there. The north-stars text
distinguished itself from "the graph-first North star above", which in a repo without the GitNexus
module is a pointer to a section the filter had just removed (NS-13). Contract tags now apply to a
paragraph as well as a section. - A bad target printed a stack trace. "Not a git repository" is a fact the user needs, not a
crash to read; the stack is oneBEARING_DEBUG=1away.
Known and accepted: installing rewrites package.json as 2-space JSON, so a repo that minified or
tab-indented it sees a whitespace-only diff that uninstall cannot undo.
Fixed — the task-core was one file per REPO, not per chat
.bearing/.task-core.md was a single path. The moment two agent sessions ran in the same
repository — a second editor window is enough, and three is routine — they overwrote each other's
save-state. The failure is worse than losing the file: on recovery a session reads whatever the
last writer left, so it reconstructs from another chat's task with full confidence. That is
precisely the drift a task-core exists to prevent, manufactured by the task-core itself.
- One file per chat, at
.bearing/task-cores/<chat-id>.md. The key comes from the transcript
path every hook already receives; its basename is the session id and it stays stable across
compaction, which is exactly when the core must be found again. - The path is now unguessable, so the session brief states it — on a fresh start as well as on
recovery. Before, the agent could name the one documented file from memory; without this it
could not write a core proactively at a milestone and would only learn its path once compaction
hit, which is too late. - A pre-existing single-file core is still read, so upgrading mid-task loses nothing.
- Old chats' cores are pruned after 30 days, never the current chat's however old it looks.
.bearing/task-cores/is gitignored and covered by the post-install check — the previous rule
matched a file, not a folder, so every chat's save-state would have been committed. Uninstall
removes the empty directory but keeps any core still in it, the same way it keeps north-stars.
Fixed — the contract promised commands the install did not have
Found by dogfooding: bearing's own repo runs an intel-only install, and npm run bearing:northstars
— the command its own CLAUDE.md advertises — does not exist there.
Every npm script is owned by the GitNexus module, so an intel-only install has none of them. The
north-stars section was advertising a command you only get by installing a different module, in
the one file every agent reads as authoritative (NS-13, NS-20). The contract now points at the
skill and the file path, both of which are always installed.
- The generated-file note shipped into user repos said "edit there, run
npm run gen:contract" — a
path and a command that exist only in bearing's own repo. It now tells the user what they actually
need to know: edits to that block are replaced on the next update. - The post-install check that catches dangling commands scanned only executable files, on the
reasoning that docs mention commands illustratively. True in general, but the contract is an
instruction, so those three files are now held to what is installed — including when
package.jsonhas no scripts at all, which the original scan skipped as uninteresting and was in
fact the broken case. - README lists those commands under "With the GitNexus module".
Added — bearing update tells you what changed
Nobody visits a changelog; everybody reads the terminal they just typed into. An update is the one
moment you are asking "what did this just do to my repo?", and bearing already records the version
you were on, so it can answer:
What's new since 1.0.4
1.0.5 — the compound-command notice was silent on the shape it exists for
1.0.6 — one MCP server for the whole machine, instead of one per client
1.0.7 — a domain expert on every review, and an installer that checks its own claims
Full notes: https://github.com/ReidenXerx/bearing/releases
Titles only — 1.0.7's section alone is 13k characters, and burying the next steps under it would
make the useful part unreadable. It stays silent rather than guess: a fresh install, an unknown
previous version, no packaged changelog, or nothing new all print nothing, and an update never
fails because release notes could not be read.
CHANGELOG.md now ships in the npm package, so this works from an install as well as a checkout.
Added — every release is on GitHub
The repo had zero tags and zero releases, so the changelog was visible only to someone who thought
to open the file. All eight — v1.0.0 through v1.0.7 — are now published, each tagged at the
commit that actually shipped it, with its changelog section as the body. npm run release:notes -- --list shows what is releasable; it refuses Unreleased and the pre-rename 1.2.0.
The README's changelog link is now absolute, since npm does not reliably rewrite relative paths
(NS-17), and points at the releases page too.
1.0.7 — a domain expert on every review, and an installer that checks its own claims
The kit was renamed gitnexus-agent-kit → bearing, but three identifiers kept the old name
because renaming them touches files in your repository. Two were merely stale. One was a bug.
Fixed
- An intel-only install no longer creates
.gitnexus/. The install manifest lived at
.gitnexus/agent-kit-manifest.json, and writing it created the graph tool's index directory in
repos that had explicitly declined the GitNexus module — an index directory for an indexer that
was never installed. NS-13 names four channels through which enforcement must not leak; this was
a fifth. The manifest now lives at.bearing/manifest.json, and the fourgitnexusentries in
the managed.gitignoreblock are gated on the module too..bearing/.gitnexus-*stays
ungated: the core session-primer writes it, and.gitnexus-northstar-counter.jsonbelongs
to the north-stars module rather than the graph. - The managed
AGENTS.md/CLAUDE.mdblock is<!-- bearing:BEGIN -->. Matching only a new
marker would have left the old block in place in every installed repo and appended a second one
beside it — the exact failureGITIGNORE_MARKERS_LEGACYalready documents. Both adapters now
match the current marker or any legacy one, replace the first in place, and drop any duplicate. - The gitignore migration pointed at a command that does not exist. It rewrote
(safe to remove via gn-kit uninstall)into(safe to remove via gn-agent-kit uninstall)—
one dead binary name for another. Both now collapse tobearing-uninstall(NS-6). - Renamed the remaining user-visible strings: the
bearing verificationbanner, the
bearing installedhealth detail, and a sync script that told the user to rungn-agent-kit.
Fixed — uninstall now leaves the repo as it found it
Six leaks, all pre-dating this release. After install; update; uninstall a repo was left holding
.bearing/hooks.json, an empty .claude/ with a {} settings file, empty .claude/skills/ and
.agents/skills/ directories, and a .gitignore it never had. It now comes back byte-identical.
- An update disowned the file the install created.
.bearing/hooks.jsonis seeded only when
absent, because it is team-shared config the user edits — but skipping the copy also dropped it
from the manifest's file list, so uninstall no longer knew the kit had put it there. It is now
re-claimed on update, and only when a previous manifest says the kit wrote it: a hooks.json
that existed before the first install was never ours and is still never touched. .claude/settings.jsonwas left as{}once our hooks were stripped out. It now follows
the rule.mcp.jsonin the same adapter already used — remove the file when what remains is
empty, keep it when the user has anything else in it.- The skill link directories were never removed, only their contents, and
.claude/was
missing from uninstall's prune list entirely. Both are rmdir-only, so a directory holding
anything of the user's survives. - A
.gitignorethe kit created was left behind empty. Whether the repo had one is now
recorded in the manifest at install time, because by the second run the file exists because we
made it — indistinguishable, after the fact, from the user's own (NS-1). - Uninstall stripped the final newline from a
.gitignoreit was otherwise leaving alone,
which was enough to show the file as modified in the user's diff.
Added — the installer now checks its own claims
Every defect below was found by an agent inspecting a real install by hand. A person running
npx bearing would have been told about none of them — and three would have printed success
while broken. The existing verifier could not have caught a single one: it asks does this file
exist, and every failure had the right files in the right places with the wrong content.
lib/postcheck.mjs asserts post-conditions against the disk at the end of every install and
update. Each check exists because a real defect shipped through the gap it covers:
| check | the defect it would have caught |
|---|---|
scripts_binary |
all 16 npm scripts reverted to npx gitnexus@latest after step 7 |
mcp_entries |
setup overwrote .cursor/mcp.json; the Zed adapter hardcoded npx |
mcp_http_live |
the repo pointed at a port where the LaunchAgent had died on exit 127 |
local_state_ignored |
task-core, session flags and install backups became committable |
agent_docs |
a marker rename appended a second contract block |
no_legacy |
two manifests left side by side, free to disagree |
declined_clean |
.gitnexus/ created in a repo that declined the graph module |
files_present |
a recorded file missing from disk |
Three deliberate properties:
- Not behind
--skip-verify. That flag exists to skip the slow index build, and every
automated path in this repo passes it — which is precisely how these reached a real machine.
The checks read the disk we just wrote and cost milliseconds. - In
lib/, not the bundle.scripts/bearing-verify.mjsis owned by the gitnexus feature and
so is its fallback, so an intel-only install had no verification at all. The configuration least
exercised by the author was also the least checked. - A failure changes the headline and the exit code. The summary reads "finished with N FAILED
checks" rather than "complete", and the process exits non-zero. Environmental problems (a server
that is not running) print a fix instead of asking for a bug report.
Recorded as NS-20 and NS-21. The negative-case test immediately earned its keep: the first draft
of scripts_binary compared with String.includes, and "npx gitnexus@latest".includes("gitnexus")
is true — a check that could never fail, which is the exact trap NS-9 describes.
Fixed — npx gitnexus@latest came back after every install
1.0.6 added a recorded gitnexusCmd so a repo could pin the binary it runs. The manifest recorded
it correctly and kit.mjs wrote it correctly — and then step 7 undid all of it, because three
shipped components rebuilt their commands from the bare default. Found by installing into a real
repo and reading what actually landed: manifest said gitnexus, all 16 npm scripts said @latest.
scripts/bearing-teaching/merge-package-scripts.mjsis run bybearing-setup.shafter the
installer has written the scripts, and rebuilt every one of them fromnpx gitnexus@latest. The
existing regression test passed--no-setup, so the fixture skipped exactly the step that broke
it (NS-9). It now asks.bearing/lib/gitnexus-cmd.mjs, and so does--snippet.bearing-setup.shoverwrote.cursor/mcp.jsonwith a hardcodednpx -y gitnexus@latest mcp
stdio entry, reverting both recorded choices — a repo pointed at a shared http server went
back to spawning one server per client, recreating the pile-up http exists to prevent. It now
builds the entry from the newmcpEntryFor()resolver. Its ownGITNEXUS_CLIwas pinned the
same way and now resolves too.- The Zed adapter never honoured either choice — alone among the three, it hardcoded the npx
entry. Because Zed project settings win over user settings, that committed entry superseded a
correctly configured global one: observed on a real machine as Zed runninganalyzefrom
~/Library/Application Support/Zed/node/cache/_npxagainst the same index a bearing refresh was
writing. Zed now writes the resolved binary. It stays on stdio deliberately even for an http
repo — Zed's remote-context-server shape is unverified, and guessing a schema in someone's real
editor config risks a server that will not start at all, which is worse than one that works
locally. The installer now says so in its next steps instead of leaving it to be discovered. .bearing/lib/detect-api-router.mjsspawnednpx gitnexus@latest cypherdirectly, with
gitnexusSpawnsitting unused beside it — so it queried the published analyzer's graph while
everything else used the installed one.
Fixed — the CLI silently did nothing through a symlink
lib/kit.mjs decided whether it was the entry point by comparing import.meta.url against
process.argv[1] unresolved. Any symlink on the way in made those differ, so the CLI fell through
and exited 0 having done nothing. On macOS this is the ordinary case, not an exotic one: /tmp
is a link to /private/tmp, so node /tmp/checkout/lib/kit.mjs install … looked like a
successful install that installed nothing. Both sides are now resolved with realpath.
Fixed — the macOS shared MCP server never actually started
The launchd path shipped in 1.0.6 marked UNVERIFIED. Running it on a Mac showed the warning was
right, and in the predicted place.
- The LaunchAgent exited 127 in a restart loop. An absolute
ProgramArgumentspath is not
enough: gitnexus is an "env node" shebang script, and launchd starts with a minimal PATH that has
no version-manager bin dir, soenvcould not findnode.servicePathEnv()now puts the
binary's own directory first — where nvm, volta, fnm and nodenv all keep the matchingnode—
and the systemd unit had the identical latent bug. installServicereportedok: true, "listening on 127.0.0.1:39100"for that dead agent,
because it only checked thatlaunchctl bootstraphad loaded the definition. Loading a
service and running a server are different events. The caller's fallback-to-stdio path was
therefore unreachable — it would have written an http entry pointing at nothing, which fails
every graph call. It now confirms the port answers before claiming success.
Verified on macOS 27: agent runs, survives kill -9 via KeepAlive, and is listening again ~1s
later. The Task Scheduler path remains unverified.
Compatibility
- Every manifest reader consults the old paths. The manifest is an inst...
1.0.6 — one MCP server for the whole machine, instead of one per client
MCP stdio spawns one child process per client, by protocol design. Every editor window and
every agent session therefore gets its own GitNexus server — seven were observed on one machine,
all watching the same index, all auto-refreshing when HEAD moved, and all queueing behind a single
index lock with a 600 s timeout. That contention blocked real work.
GitNexus already ships the fix: gitnexus mcp --http (v1.6.9) is one long-running server that
resolves repositories per request, so a single process serves every repo on the machine.
Added
- The installer asks how the MCP server should run, rather than deciding for you. The http
option installs a background service on your machine, and that is not something to arrange
behind your back. Only asked when the GitNexus module is actually selected. - The choice is recorded and re-applied on every update. bearing still always writes the MCP
entry — that is what keeps it predictable — but it writes what you chose instead of a hardcoded
default. Non-interactive:--mcp http|stdio|<port>|<url>, orBEARING_MCP. - Optional service setup: a systemd user unit on Linux, a LaunchAgent on macOS, a scheduled
task on Windows. No root anywhere, loopback only, and the removal command is printed with it.
Fixed
bearing updatesilently reverted a hand-configured MCP entry. A repo deliberately pointed
at a shared server, or at a locally built gitnexus, had that overwritten with
npx gitnexus@latestat the next update — undoing a whole daemon setup without a word and
recreating the pile-up it was installed to fix.- The README documented two commands that 404.
npx bearing-update <repo>makes npx resolve a
package by that name, which does not exist. The subcommand form (npx bearing update <repo>)
is the one that works through npx.
Known limitations
- Only the systemd path has been executed. The launchd and Task Scheduler paths are written
from documented behaviour and are untested — the file says so, a successful install on those
platforms says so, and the manualgitnexus mcp --httpcommand is printed either way. The rule
that broke the first systemd draft (these supervisors do not inherit your shell's PATH, so the
binary must be named absolutely) is applied and tested on all three. - Task Scheduler has no equivalent of
Restart=on-failure, so on Windows the server stays down
until next logon if it dies mid-session. - A service definition bearing did not write is never touched, and a failed service install falls
back to stdio rather than leaving you with a config pointing at a dead port.
1.0.5 — the compound-command notice was silent on the shape it exists for
1.0.4 added a notice telling the agent that a blocked shell command was blocked WHOLE, so
python3 edit.py && grep ... could not be read as "the edit landed, only the grep was blocked".
It fired on &&, || and ;.
The incident it was written for has none of those. It was a heredoc followed by a search on
the next line — a python3 heredoc that rewrote several call sites, then a grep beneath it.
Bash rejects the whole line, so the rewrites never ran, and the notice stayed silent because a
newline is not an operator. A newline separates steps exactly as ; does. It now counts, so the
notice covers the shape that actually costs silent edits.
Backslash line-continuations are excluded — foo \ then --bar is one step, and warning there
would report lost work when none was. That exclusion also handles CRLF, where the byte before the
newline is the carriage return rather than the backslash; without it the exclusion failed silently
on Windows.
Over-warning is the deliberate direction: the notice fires only on a deny, where "nothing ran"
is true by construction. A missing notice costs silently-lost work; a redundant one costs a line.
Added
lib/classify.test.mjs— behavioural coverage for the shipped guard core, which previously had
none. Pins the compound-notice shapes and the scoped-grep allow with its paired deny, since an
allow with no paired deny widens silently into "greps are fine".
1.0.4 — dogfooding, a broken uninstall, two dead gates, and field feedback
Installing bearing into its own repo for the first time, then auditing the paths dogfooding
doesn't reach, then acting on a report from an agent using the kit in a live project.
Anyone on 1.0.3 should upgrade — it cannot uninstall.
Fixed — critical
uninstallcrashed partway through. It threw whenever no backup was recorded (the normal
case) and, being the first adapter in the cleanup loop, took the rest with it: hooks stayed
registered against deleted files, the MCP server stayed configured, and the manifest survived so
the repo still looked installed.uninstalland module-deselection deleted files they had overwritten. Install stashes a
colliding.githooks/pre-commitor.vscode/settings.jsonbeside itself; removing ours without
restoring theirs left a hole with the content stranded in a.bearing-backup. Both now put the
original back.- Deselecting a module removed nothing. Re-installing with fewer features left every file,
hook, MCP entry and npm script in place — so turning GitNexus off left its gates enforcing. - The large-source-read gate had never fired once. It referenced an unbound
config; the
ReferenceErrorwas swallowed by its own fail-open catch and reported as "0 lines", so no file
was ever large enough to gate.
Fixed — the tool blocking legitimate work
- A stale index was a total lockout. One commit of drift and the agent could not run
ls, tail
a log, read a.csv, or run tests until a full reindex. Staleness now gates only what a stale
graph would have answered. Globwas inverted.**/*— the broadest sweep there is — was allowed, whilesrc/order.js
was denied and told to usequerywith a concept, which cannot find a file you named by path.- Scoped searches were denied and sent to a tool that can't answer them: a grep in one named
file, a search intests/, and counting occurrences, all redirected tocypher ACCESSES. - A blocked shell command implied the rest of it ran. A denied Bash call blocks the whole
line, but the message named only the offending segment — sopython3 edit.py && grep …read as
"the edit landed". Sequenced commands now say outright that nothing executed.
Added — the kit distrusts the graph, and itself
- Unreliable
impactverdicts are flagged. When the pre-edit gate grades a changerisk: LOW
but resolved no callers — or only test files — the agent is told to treat it as unknown blast
radius and confirm classically. It warns rather than blocks: re-runningimpactreturns the same
empty answer. - "A graph ZERO is not evidence of absence" is now part of the always-on contract. Positives
are strong evidence; zeros mean unknown and must be confirmed before any "dead code" call. - The kit audits its own enforcement.
bearing:scorecardand the session brief report whether
the gates are earning their keep, and name the evidence that distinguishes "gates misfiring" from
"gates working on a grep-happy agent".
Fixed — first-run experience
npx bearingadvertised the wrong product. Six strings still listed three runtimes after
Codex shipped, including the runtime picker's "All" option — so choosing All looked like it
excluded Codex.- The banner box stopped closing once the greeting grew, and the installer asked "quick or full
index?" in installs with no indexer. update-allclaimed "Index built" for repos with no GitNexus module, and reported a failed
reindex as the whole update failing.
1.0.3 — 20 defects fixed after an adversarial review
Two independent reviewers audited the install/migration core and the runtime hooks, reproducing
every finding end-to-end. Anyone on 1.0.x should upgrade — several of these made the tool
unusable or destructive.
Fixed — critical
npx bearingdied immediately. The interactive installer — the command the README leads with
— threw aReferenceErroron its first line.- Any install without the
gitnexusmodule crashed, after writing the manifest, leaving a
half-installed repo. - The feature filter applied to FILES only.
settings.jsonstill registered every hook,
package.jsonstill got every script,.mcp.jsonstill wired GitNexus — so a filtered install
spawned a missing module on every tool call. uninstalldeleted your north-stars andhooks.local.json(untracked by the kit's own
gitignore, so unrecoverable). It now removes only what it installed and reports what it kept.
Fixed — the tool blocking legitimate work
- Repos living under
~/srcor~/go/srchad every file treated as source: every large Read
denied and every Edit gated, repo-wide. Classification is now repo-relative. - Piping into
rg/ag/ackwas denied —npm run build 2>&1 | rg errorblocked, with a Cypher
query offered as the fix. TODO/FIXMEgreps were denied as symbols, redirected to a lookup that cannot resolve.- A failed refresh locked the session permanently and re-locked it every session; the escape
hatch existed but was unreachable. - A repo with no commits denied
ls,cat, Read, Grep and Edit. bearing updatetripped its own drift gate, blocking graph queries right after updating.- Deny messages now name the two ways out (
bearing:fallback,mode: guide).
Fixed — protecting your files
- Install overwrote
.vscode/settings.jsonand.githooks/pre-commitwithout a backup, and
uninstall then deleted them. Pre-existing files are now saved to<file>.bearing-backup. - The pre-install backup was re-taken from the already-modified file on every update, so
uninstall "restored" kit artifacts. - Team-tuned
.bearing/hooks.jsonwas reverted on every update (e.g.mode: guide→ blocking). - A
.gitignorerule appended with>>was absorbed into the managed block and deleted on the
next update. update --featureswas silently ignored; migration failures were rendered as successes.
Fixed — performance and precision
- The read guard read entire files to compare one line count (398ms / 230MB on 54MB); now a bounded
scan (104ms → 2ms on 9.4MB). - Context-pressure tail cap 8MB → 32MB — past it the estimator reports "unknown", which reads as
"not full" exactly when the window is filling. git … commitwas a substring match, denyinggit rev-parse HEAD^{commit}and friends.
75 tests, up from 69 — every fix has a test that fails when the fix is reverted.
1.0.2 — proper diagrams
Changed
- README diagrams are now generated SVG (
npm run gen:diagrams) rather than ASCII: rendered
boxes, colour-coded outcomes, readable on light and dark. Served over absolute raw URLs so they
display on npm and GitHub alike, with no external renderer and no dependency on npm's
relative-link rewriting. 1.0.1 shipped the ASCII interim — this is the version worth looking at.