Commit Graph
203 Commits
Author SHA1 Message Date
Martin Vogel 5e6fa657ee Merge pull request #2517 from DeusData/fix/uninstall-keeps-indexes-v2
fix(cli): keep project indexes by default during uninstall
2026-10-04 16:24:09 +02:00
Martin Vogel b5649e9823 Merge pull request #2468 from DeusData/fix/issue-1167-1180-client-detect
fix(cli): client detection for OpenCode, Hermes, Claude (#1167, #1180)
2026-10-04 16:23:04 +02:00
Martin Vogel 8c12de9b16 Merge pull request #2467 from DeusData/fix/issue-2228-toml-foreign-tables
fix(install): keep foreign tables between the managed TOML markers (#2228)
2026-10-04 16:22:57 +02:00
Martin Vogel c4e1ad4c87 Merge pull request #2448 from DeusData/fix/hook-augment-log-path
fix(hook-augment): log under the cache dir, never at a cut-off path
2026-10-04 16:22:11 +02:00
Martin Vogel f6fe11dde9 Merge pull request #2446 from DeusData/fix/issue-1690-skill-project-name
fix(mcp): resolve a directory project arg to its owning project (#1690)
2026-10-04 16:21:59 +02:00
Martin Vogel db1dcf4600 Merge pull request #2333 from DeusData/fix/issue-1948
feat(watcher): opt-in polling of non-git project roots (#1948)
2026-10-04 16:21:40 +02:00
Martin Vogel ba6307f037 fix(cli): keep project indexes by default during uninstall
Require --delete-indexes for automatic or noninteractive index removal.
Keep the separate terminal prompt with a default of retaining indexes,
and preserve dry-run and guarded activation ordering.

Refs #2200

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 19:11:36 +02:00
Martin Vogel d13c0ecb73 test(cli): mark Claude Code installations in hook fixtures
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-03 00:21:00 +02:00
Martin Vogel 8292e3e807 test: size hook log temp buffers for Windows paths
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-02 20:51:31 +02:00
Martin Vogel abf00764a7 fix(mcp): resolve a directory project arg to its owning project (#1690)
An agent in a checkout whose directory name differs from the repository
name (git clone .../repo_name.git some_name) could not find the indexed
project. The installed skill only said "list_projects - check if project
is indexed", so Codex and Copilot guessed the project from the repo name,
got "project not found" and stopped using the graph. Passing the working
directory did not help whenever it was not exactly the indexed root: a
path was only ever turned into the path-DERIVED name, which misses a
checkout indexed under an explicit name (the stored identity, see #1660)
and every directory below an indexed root.

Fix:
- mcp: a path-shaped project keeps the derived name when an index exists
  under it (unchanged fast path); otherwise it resolves to the indexed
  project whose stored root_path owns the directory. The deepest owning
  root wins; two projects owning that root stay unresolved, so the
  not-found error lists every project instead of guessing. Reuses the
  list_projects identity reader and the canonical-path containment helpers.
- mcp: the project-not-found hint says how to pick the project (the name
  whose root_path contains the working directory, or that absolute
  directory; never a repo or folder name), so an agent that guessed can
  recover.
- cli: the SKILL.md every skill client installs (Codex, Copilot and the
  rest), the shared agent instructions and the Aider CLI instructions say
  the same thing.

Proof (dev binary, synthetic HOME; repo_name cloned as some_name and
indexed by path; a second checkout other_dir indexed with name=my-alias):
  before: project=<other_dir>        -> project not found
          project=<some_name>/pkg    -> project not found
  after:  project=<other_dir>        -> my-alias (index_status agrees)
          project=<some_name>/pkg    -> the some_name project
  unchanged: the checkout root path and the explicit name resolve; the
  unindexed sibling some_name_x stays not found; a guessed "repo_name"
  stays not found and now carries the how-to-pick hint.

Tests:
- mcp tool_project_path_resolves_owning_project_issue1690: an explicitly
  named root and its subdirectory resolve; normal naming and similar
  names (some_name2, some_name_x) are not confused; two owners of one
  root are never guessed; a guessed repo name gets the hint. RED 3x
  before, green after, RED again when the production change is reverted
  (the hint check also goes RED on its own revert).
- cli cli_guidance_resolves_project_by_root_path_issue1690: pins the
  instruction in the skill, agent-instructions and Aider variants.

Fixes #1690

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-30 23:14:32 +02:00
Martin Vogel c06ceb1638 feat(watcher): opt-in polling of non-git project roots (#1948)
A project indexed from a directory that is not a git repository was never
refreshed after its first index: init_baseline classified it is_git=false
and poll_project/check_changes returned early for it forever, so the graph
silently went stale. The comment in init_baseline even claimed such folders
were "polled as a plain directory", which was never true.

Add an opt-in config key, watch_non_git (default false, read once at daemon
startup like watcher_enabled). When on, a non-git root is polled on the
existing adaptive cadence (cbm_watcher_poll_interval_ms) by a tree
signature: the indexer's own cbm_discover walk (same skip lists, .gitignore
stack and .cbmignore), sorted by path and folded over (relative path, size,
mtime). A changed signature takes the existing reindex path with the same
commit-after-success discipline as the #937 dirty signature. The baseline
leaves the signature "unknown", so the first poll reindexes once
(at-least-once, covering edits made while the daemon was down).

Runaway guard: only files discovery would index enter the signature, so
cbm's own .codebase-memory output (built-in skip) and the cache directory
(pruned by absolute path) can never make a finished reindex look like a
new change; a scratch folder under an unrelated dirty repository is polled
on its own files and never inherits the ancestor's dirty state.

cbm_file_info_t gains mtime_ns, filled from the stat discovery already
performs, so the signature needs no second stat per file and stays
UTF-8-correct on Windows (discovery's wide stat).

Docs: README and docs/CONFIGURATION.md now say non-git roots are not
watched by default and document watch_non_git. The staleness signal in
index_status is left to #1561's freshness object. Refs #1948.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-30 22:27:19 +02:00
Martin Vogel e81fd499f8 fix(install): adopt the Codex MCP table after Codex rewrites it (#1720)
Codex rewrites its [mcp_servers] tables whenever it edits MCP servers
itself (`codex mcp add`, its MCP settings, wrappers such as Orca that
drive it) and drops comment decor on the way. From our managed block it
keeps the table but loses the begin marker, omits `args = []`, and leaves
the end marker as an orphan below whichever table follows. Reproduced
with codex-cli 0.153.4: after `codex mcp add orca -- /bin/echo hi` the
file holds an unmarked

  [mcp_servers.codebase-memory-mcp]
  command = ".../codebase-memory-mcp"
  env_vars = ["CBM_CACHE_DIR", "CBM_RUNTIME_DIR"]

and a stray `# <<< codebase-memory-mcp MCP <<<` under [mcp_servers.orca].

Every later install then failed with op=mcp_install on a table cbm itself
had written, twice over: cbm_toml_remove_legacy_table refused the orphan
marker as malformed, and its owned-shape check predated env_vars (#1562,
#1664), so even without the orphan the table counted as foreign.

cbm_toml_remove_legacy_table now drops a lone orphaned marker line in the
same atomic write (we are the only author of those markers, #1558; only
that line is removed, nothing is guessed) and treats env_vars naming only
CBM_CACHE_DIR / CBM_RUNTIME_DIR as part of the owned shape. A table with
any other key or forwarded name is still foreign and left byte-identical,
and a foreign table keeps the file untouched, orphan included. The scan is
split into toml_strip_legacy_table + toml_legacy_classify_assignment so
the entry point reads the file, heals, strips and writes once.

The same helper backs the Grok Build install, which gains the same
self-heal.

Tests: cli_upsert_codex_mcp_adopts_codex_rewritten_table uses the exact
bytes Codex produced; config_toml_legacy_remove_adopts_owned_env_vars_only
pins the env_vars boundary (owned names adopted, any other name foreign).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-30 22:27:19 +02:00
Martin Vogel 0f52d30c29 Merge pull request #2412 from DeusData/fix/activation-unknown-daemon-is-foreign
fix(cli): never stop a daemon whose cache is not confirmed as ours
2026-09-30 22:21:59 +02:00
Martin Vogel 48acb559e6 Merge pull request #2405 from DeusData/fix/internal-db-enumeration
fix(cli): never treat internal cache stores as project indexes
2026-09-30 22:21:11 +02:00
Martin Vogel 368f33f9e0 Merge pull request #2353 from DeusData/fix/issue-2200
test(cli): pin that update -y never deletes project indexes (#2200)
2026-09-30 22:20:18 +02:00
Martin Vogel aa53278b29 Merge pull request #2318 from DeusData/fix/issue-1133
fix(cross-repo): fail explicitly when zero target projects resolve (#1133)
2026-09-30 22:17:47 +02:00
Martin Vogel ed76cf8e88 fix(cli): never stop a daemon whose cache is not confirmed as ours
Why: the activation rendezvous is per OS account, so an install or update
run under another HOME / CBM_CACHE_DIR (a sandbox, a second profile) meets
the account's live daemon at the same endpoint. The daemon's
activation-shutdown handler verifies only the requester's build, never its
cache, so the CLI's scope decision is the only thing that tells "my daemon"
from "someone else's". That decision drained the cohort unless the active
cache was positively DIFFERENT: a busy session group (another activation or
a session start held it, so the active identity was never read) and an
unreadable active cache both fell through to "ours", printed "Stopping
active CBM sessions" and shut the daemon and every session behind it down.

Fix (user decision 2026-09-28, "unknown = foreign, keep it"): only a
positive match may drain. cli_activation_resolve_scope returns
CLI_ACTIVATION_SCOPE_ACTIVE only for an OK admission (exact identity, cache
included, or an empty group) or for a conflict whose cache fingerprint
equals this activation's. BUSY and a blank active cache now return the new
CLI_ACTIVATION_SCOPE_UNCONFIRMED, handled like a foreign cohort: no lock is
taken, no drain request is sent, the mutation proceeds, and the audit log
records "quiesce_skipped: active cohort ownership unconfirmed (...)". A
read-only status probe names the daemon that was left (pid, version) and
the notice says how to stop it: close its sessions, then run
`codebase-memory-mcp daemon stop`. The confirmed paths are unchanged: a
normal self-upgrade still drains its own daemon, a clearly foreign cache is
still left alone.

Proof (product binaries; private CBM_RUNTIME_DIR; an own permanent test
daemon started from the same build under a host HOME/cache; a sandbox
HOME/cache on the same runtime dir; the cohort maintenance gate held by a
second process, i.e. a busy session group):
- origin/main: "Stopping active CBM sessions and operations for
  install...", the test daemon was stopped (daemon status: not running),
  and the install still failed (rc=1 after 23 s, "could not be stopped
  safely").
- this change: "Leaving the running CBM daemon untouched (pid 76713,
  version dev): this install could not confirm that it serves the cache
  this install targets (the session group was busy, so its cache identity
  could not be read)." The test daemon kept running with the same pid and
  the install completed (rc=0, 10 s).
A lifetime record whose cache field is zeroed is refused (UNSAFE) by both
builds with the daemon kept; that path never drained and is unchanged.

Tests (cli suite; fixture daemon in a private runtime dir; sandbox HOME):
- cli_install_foreign_home_unreadable_cohort_cache_keeps_host_daemon
  (the new seam cbm_cli_set_activation_scope_cache_unreadable_for_test
  blanks the observed active cache fingerprint)
- cli_install_foreign_home_busy_cohort_keeps_host_daemon (the maintenance
  gate is held through the product's own lock-directory handle)
Both were RED 3x on origin/main for the right reason (the host daemon was
drained: ASSERT(host_serving)), are green with the fix, and turn RED again
when the fix is reverted. Controls stay green:
cli_install_into_host_namespace_still_drains_host_cohort (confirmed same
cache: drained, as before) and
cli_install_{skip_binary,binary}_into_foreign_home_never_drains_host_cohort
(clearly different cache: kept, as before).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 20:24:48 +02:00
Martin Vogel 2278498371 Merge pull request #2393 from DeusData/fix/issue-2264
fix(install): migrate v0.10.8 OpenCode agent profiles on upgrade (#2264)
2026-09-28 19:45:27 +02:00
Martin Vogel 7122298421 fix(hook-augment): log under the cache dir, never at a cut-off path
Why: arming the hook-augment deadline opens its breadcrumb log with
O_CREAT. The path came from raw getenv("HOME") as
"%s/.cache/codebase-memory-mcp/logs" in 1 KiB buffers with no length
check. A HOME near 1 KiB therefore created a file at the cut-off name
(".../logs/hook-augme") on every hook call, and a longer one created a
cut-off directory. The hook also ignored CBM_CACHE_DIR, which every
other cbm component honours for its cache and logs. The index skip log
had the same defect in its CBM_INDEX_LOG override: an override longer
than 1 KiB was copied in unchecked and opened "wb", which created or
truncated the file at its cut-off prefix and reported that name as the
run's logfile.

Fix: the breadcrumb log now resolves through cbm_resolve_cache_dir()
(CBM_CACHE_DIR, else HOME, else USERPROFILE, plus the default) to
<cache_dir>/logs/hook-augment-timeouts.log. Every snprintf into the
1 KiB buffers is checked, including the one for the CBM_HOOK_TIMEOUT_LOG
override. A path that does not fit means no breadcrumb, and the logs
directory is created only for a path that fits. The hook still exits 0
(it never fails the agent's hook call) and allocates nothing new. The
CBM_INDEX_LOG override gets the same check: an override that does not
fit writes no logfile and warns index.logfile_path_too_long, and the
index still completes. With a normal HOME the log stays where it was.

Proof (dev binary, synthetic roots, isolated CBM_RUNTIME_DIR, no-op
Bash payload on stdin; "created" = find over the synthetic root minus
the pre-made HOME/cache chains):
- 980-byte HOME. Before: exit 0, plus three directories and a file at
  the cut-off name <HOME>/.cache/codebase-memory-mcp/logs/hook-augme
  (1023 bytes). After: exit 0, nothing created.
- CBM_CACHE_DIR=<root>/cache. Before: the log landed under HOME.
  After: <root>/cache/logs/hook-augment-timeouts.log.
- 1000-byte CBM_CACHE_DIR. After: exit 0, nothing created.
- Normal HOME: <HOME>/.cache/codebase-memory-mcp/logs/
  hook-augment-timeouts.log before and after.
- 1096-byte CBM_INDEX_LOG via `cli index_repository`. Before: a file
  at the 1023-byte cut-off name, reported as the logfile. After: status
  indexed, skipped_count 1, no logfile, nothing written.

Tests: RED 3x on the unfixed code for the reasons above, green with
the fix, RED again with the fix reverted:
- cli: cli_hook_augment_log_long_home_creates_no_cut_path,
  cli_hook_augment_log_honours_cache_dir, plus the positive control
  cli_hook_augment_log_default_location_unchanged (green before and
  after).
- index_resilience: index_long_log_override_writes_no_cut_path.
The fixtures sit under link-resolved temp roots: the kernel bounds the
expanded path, so under macOS's /tmp -> /private/tmp link a near-1 KiB
path can be neither created nor probed, and the bug stays hidden.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 19:34:10 +02:00
Martin Vogel 2b329fe75d fix(cli): client detection for OpenCode, Hermes, Claude (#1167, #1180)
Three detection defects kept `codebase-memory-mcp install` from
configuring agents people run, and made it configure one they never
installed.

OpenCode without a config file or a CLI on PATH (#1167, #1180).
OpenCode was detected only from its global config file or directory,
OPENCODE_CONFIG_DIR, or an `opencode` found on PATH. It now also counts
as installed when its data directory exists: ~/.local/share/opencode on
every OS, %USERPROFILE%\.local\share\opencode on Windows
(opencode.ai/docs/troubleshooting, "Storage"; the same page names it as
the global config location of older installs). The OpenCode CLI and the
Desktop app both create it -- the Desktop's bundled opencode-cli server
uses the same XDG paths (anomalyco/opencode packages/core/src/global.ts
creates the config and data dirs at startup;
packages/desktop/src/main/sidecar.ts only redirects XDG_STATE_HOME) --
so detection no longer depends on PATH. The install target
~/.config/opencode/opencode.json(c) -- %USERPROFILE%\.config\opencode
on Windows per the same docs page -- is unchanged and right for CLI and
Desktop alike. A PATH longer than 4 KB, which hides every CLI from
find_in_path(), is handled separately by PR #2381.

Hermes home on Windows (#1180). Native Windows Hermes -- the install.ps1
CLI, the MSIX package and the Desktop app -- defaults HERMES_HOME to
%LOCALAPPDATA%\hermes and falls back to a legacy ~/.hermes only while
that directory is absent (NousResearch/hermes-agent
website/docs/user-guide/windows-native.md, "Data layout";
apps/desktop/electron/data-paths.mjs, resolveDesktopHermesHome). cbm
probed and wrote ~/.hermes only, so a Desktop install was never detected
and a Hermes found on PATH got its config.yaml and skill in a home it
never reads. On Windows the home now resolves to
<home>/AppData/Local/hermes unless only ~/.hermes exists, using the
home-derived AppData convention of the existing Zed, goose, Kilo and VS
Code probes; detection, install and uninstall all follow it. HERMES_HOME
still wins, and macOS/Linux keep ~/.hermes.

Claude Code from a bare ~/.claude (#1180). Any ~/.claude directory
counted as a Claude Code install, so a user who only had the folder got
hooks, skills and MCP entries for a client they never installed.
Detection now needs what Claude Code itself leaves behind: settings.json
in the config dir, .claude.json in the home (in CLAUDE_CONFIG_DIR when
set), or the claude CLI.

Proof on macOS against synthetic HOME/cache/runtime dirs with a minimal
PATH (`install --dry-run --skip-binary`, then a config-only install):
  bare ~/.claude                        main: Claude-Code  fix: none
  ~/.local/share/opencode only          main: none         fix: OpenCode
In the OpenCode case the fixed install writes the codebase-memory-mcp
entry to ~/.config/opencode/opencode.json; main writes nothing. Controls
(a launched Desktop layout, a real Claude user) are detected by both.
The Windows Hermes home is covered by a platform-parameterised resolver
test that runs on every host plus a _WIN32-bound end-to-end detection
and plan test.

Tests (cli suite, 335 passed): seven new tests; five fail on main three
times out of three (the _WIN32-bound Hermes test and the look-alike
control are green on macOS by design). Reverting the Claude+Hermes half
and the OpenCode data-dir probe separately turns exactly their own tests
red. Nine existing Claude fixtures that used a bare ~/.claude as the
install marker now write .claude.json; two Hermes tests and smoke-test
8x expect %LOCALAPPDATA%\hermes on Windows.

Refs #1167
Fixes #1180
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 02:42:24 +02:00
Martin Vogel 98e58acb8d fix(install): keep foreign tables between the managed TOML markers (#2228)
cbm_toml_upsert_managed_block and cbm_toml_remove_managed_block treated
everything between the "# >>> codebase-memory-mcp MCP >>>" markers as
ours: install replaced the whole span with the fresh block, uninstall
deleted it, and the conflict check skipped that same span. When our block
ended ~/.codex/config.toml, Codex Desktop appended its own tables below it
while the closing marker stayed the last line, so [mcp_servers.node_repl],
[desktop] and the [marketplaces.*] tables sat inside the markers. The
0.10.4 -> 0.11.0 upgrade deleted all of them, together with the
startup_timeout_sec the user had added to our own table.

The span is now read as TOML sections, each classified against the tables
the block declares:
- owned: a declared table (the k-th [[element]] of a declared array). The
  keys the block writes are replaced; every other key, comment and blank
  line in it stays in the table.
- attached: a sub-table of a declared table, such as
  [mcp_servers.codebase-memory-mcp.env]. It is part of our server entry
  and stays with it; outside the markers it would collide with the next
  install.
- foreign: any other table, moved byte for byte and in order to just after
  the closing marker, so from then on the markers bracket only our entry
  and later appends land outside them.
- lead: lines above the span's first table, moved above the opening
  marker, where they keep the table they belong to.

Uninstall of the Codex, Grok and Kimi blocks goes through the new
cbm_toml_remove_managed_block_owned with the header each block declares:
it removes our table with its keys and sub-tables and leaves everything
else in place. cbm_toml_remove_managed_block keeps the historical
whole-span contract.

Content whose ownership cannot be decided fails closed with the file
untouched: a second copy of a declared table, an undeclared extra
[[element]] of a declared array, a child of a declared array element, an
unparsable line in an owned table, a user key colliding with one the block
writes, a move that would hand the keys below the block to another table,
and a rewritten file whose tables conflict with the block.

Preserving was chosen over failing closed on every foreign table because
Codex keeps appending below a closing marker that ends the file. A
refusing installer would block every later install and uninstall until
the user hand-edits the TOML, and a hand edit is itself the likeliest way
to lose those tables. Moving whole sections keeps every byte and every
key's table, and the shapes that cannot be attributed still refuse.

Without foreign content the output is byte-identical to the old rewrite.

Fixes #2228

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 02:02:07 +02:00
Martin Vogel 66d9c2be88 Merge pull request #2304 from pcristin/codex/fix-cli-fixture-lifetime
test(cli): keep fixture host until released
2026-09-28 00:08:17 +02:00
Martin Vogel 460153204b fix(cli): never treat internal cache stores as project indexes
The cache directory holds two internal SQLite stores next to the project
indexes: the user's settings (_config.db) and the cross-repo store
(_cross_repo.db). cbm_list_indexes, cbm_remove_indexes and
count_db_indexes matched every "*.db" there. As a result:

- install --reset-indexes and uninstall (when index deletion is confirmed)
  deleted the user's config (every `config set` value silently reverted
  to its default) and the cross-repo store along with the indexes;
- the "Found N index(es)" listing and count over-reported by up to two.

pass_cross_repo.c already excluded both stores by exact filename, but in
its own copy of the check. Centralise the rule in foundation/str_util:
cbm_is_internal_cache_db() (exact names, so a real project whose name
starts with "_" or contains "config" is still an index) and
cbm_is_project_index_db(). Every enumeration of the cache directory's
.db files (list, count, remove, cross-repo target scan) now calls it, and
cbm_config_open() builds its path from the same CBM_CONFIG_DB_FILENAME
constant.

Proof on a real isolated cache (one indexed Go repo, `config set
auto_index true`, a _cross_repo.db present):
  before: "Found 3 index(es)", "Removed 3 index(es)", cache left empty,
          `config get auto_index` -> false
  after:  "Found 1 index(es)", "Removed 1 index(es)", _config.db and
          _cross_repo.db kept, `config get auto_index` -> true

Test: cli::cli_index_enumeration_skips_internal_cache_dbs (RED on main,
green with the fix, RED again on revert).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 00:02:09 +02:00
Martin Vogel dc735c01fb fix(install): migrate v0.10.8 OpenCode agent profiles on upgrade (#2264)
Upgrading from v0.10.8 to v0.11.0 on a machine with OpenCode reported all
three OpenCode subagent profiles as "preserved modified profile" plus an
op=agent_install error each, and the activation stopped ("one or more agent
configurations failed; the published/current executable was kept"), although
nobody had edited those files.

Root cause: #1933 (v0.11.0) added the tool_search / tool_search_regex
permissions to the rendered OpenCode profile. Install and uninstall recognise
a profile as ours only when it matches the current rendering or one of the
known released renderings (the other access mode, the v0.9.1-rc.1 Codex
shape, the pre-tier Verify file). The v0.10.8 OpenCode shape was not in that
set, so cbm_text_migrate_owned_document classified the untouched v0.10.8
files as user-modified. Reproduced end to end: install with the v0.10.8
release binary, then install with v0.11.0/main, into an isolated HOME; the
three profiles are 1729/1880/1910 bytes, the same sizes as in the report.

cbm_render_graph_profile_opencode_v0108() renders the pre-#1933 shape
(both access modes), and install/uninstall now list it as a released
rendering, so those files are upgraded in place and uninstall removes them
as owned. A profile the user actually edited is still preserved. The
released-list assembly that install and uninstall duplicated moves into one
helper, and the two legacy renderers share one implementation (cli.c drops
three raw free sites; the memory-core baseline is tightened 173 -> 170).

The Codex and OpenCode op=mcp_install errors in the same report do not
reproduce from a pristine v0.10.8 config and depend on the reporter's own
config.toml / opencode.json; they are not addressed here.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 00:01:58 +02:00
Pcristin 262d7ca765 Merge branch 'main' into codex/fix-cli-fixture-lifetime 2026-09-27 00:45:26 +03:00
Hubert Tarnacki bc960d87d6 test(cli): pin JSON array literal handling for array-typed arguments
`codebase-memory-mcp cli <tool> --flag value` builds the tool's JSON
arguments from typed flags. Array-typed parameters such as
`semantic_query` used to accept only repeated flags; the JSON spelling
shown in the MCP schema (`--semantic-query '["a","b"]'`) was stored as a
single opaque string element (#2150). `cli_add_typed` now expands such a
literal; this pins the edge cases of that contract so it cannot regress:

- a well-formed array of strings contributes its elements
- an explicit empty array `[]` contributes nothing (not the keyword "[]")
- a malformed literal stays one literal string, without an error
- non-string items are copied as typed values, not stringified
- literals and repeated flags mix, accumulating in argument order
- repeated literals accumulate

Signed-off-by: Hubert Tarnacki <hubert.tarnacki@gmail.com>
2026-09-26 09:26:00 +02:00
Martin Vogel c64ea20fea test(cli): pin that update -y never deletes project indexes (#2200)
On 0.9.0, `update -y` in a non-interactive shell auto-confirmed "Delete
these indexes and continue with update?", removed every project index,
and only then failed at the ui/standard variant chooser: no new binary
and no indexes. The behaviour is already gone on main: 9ecabe0a moved
self-update out of the product, so a release `update` only hands off to
install.sh; install.sh runs `install -y --force`, which keeps existing
indexes unless the user passes the explicit --reset-indexes (#607); and
the variant chooser is gone, with --ui/--standard accepted as no-ops
(#1544). The only coverage of the hand-off was a Windows-only test that
checks the binary, not the indexes.

Add a portable regression test that drives the exact release dispatch
(activation seam off) with `update -y` and `update -y --standard`
against an isolated HOME/CBM_CACHE_DIR holding three .db indexes, and
asserts exit 0 with every index byte-for-byte intact. A generic -y must
never be read as consent to delete indexes. Re-injecting the 0.9.0
prompt-then-delete step into the hand-off turns it RED (kept == 0).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-25 22:24:01 +02:00
Martin Vogel 6d508350c7 fix(cross-repo): fail explicitly when zero target projects resolve (#1133)
A cross-repo-intelligence run whose targets resolved to nothing but the
source project -- a self-only target list, or ["*"] in a store that holds
only the source -- returned status "success" with projects_scanned:0.
That output is indistinguishable from "these services share no routes",
which is exactly the silent-zero result #1133 reports. Worse, the run had
already passed the pre-write validation and went on to delete the source's
existing CROSS_* edges, with no target to rebuild them from.

Root cause: cbm_cross_repo_match_cancellable validated that every resolved
target exists but never checked that at least one resolved target differs
from the source; the self entry was only skipped later, inside the match
loop, after delete_cross_edges had run.

Count the non-self targets during validation and fail before any write
when there are none, with a new result flag (no_targets) so the MCP/CLI
frontend reports a specific error ("resolved zero target projects ...
existing cross-repo edges were left unchanged") and exits non-zero.

The other halves of #1133 are already resolved on main: the
--target-projects '["*"]' flag form has parsed as a JSON array since
1a342553 (guarded here by an index_repository-specific CLI test), and
cross-repo mode has run in-process ahead of the index supervisor since
0e00ef57, so a cross-repo request can no longer surface the contained
worker-crash envelope with a 0-byte log (worker-log survival: 87717b0a).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-25 18:37:16 +02:00
Pcristin 6f91a16fc2 test(cli): keep fixture host until released
Signed-off-by: Pcristin <xxxokzxxx@protonmail.com>
2026-09-24 20:38:20 +00:00
Martin Vogel 92cafd1ec8 Merge branch 'main' into fix/hook-augment-worktree-tests 2026-09-24 07:39:35 +02:00
Lowpower 2f557f5398 style(cli): clang-format CBM_CLI_USAGE and help-text test
Signed-off-by: Lowpower <3133734+Lowpower@users.noreply.github.com>
2026-09-21 23:47:50 +00:00
Lowpower aaa72fbe60 feat(cli): point top-level cli --help at --format json
CLI_USAGE described outer --json but did not mention tool-level
--format json. Add a help-only pointer that tools which accept format
support --format tree|json, and distinguish payload JSON from the full
MCP envelope. No new flags, env vars, or output-default changes.

Fixes #2102

Signed-off-by: Lowpower <3133734+Lowpower@users.noreply.github.com>
2026-09-21 23:47:50 +00:00
Martin Vogel 1a3425536b fix(graph): quality round after the v0.11.0 probe — deterministic resolution, findability, coverage, names
Nine findings from the 2026-09-16 graph-quality probe over the eleven bench corpora, each with a
test that fails without its fix and a before/after measurement on the real corpus.

Deterministic resolution (O9). Two indexes of one kernel tree differed in 5,821 IMPORTS and 632
CALLS edges. Three order/load dependencies carried it: the `#include` target was the first by-name
hit whose path ends with the include path — a bucket in worker merge order (include/linux/device.h
vs tools/virtio/linux/device.h); the registry broke suffix_match score ties by bucket position; the
LSP budget-share rule measured wall time. Include targets now prefer the File node, the least nested
path, then the lexicographic path; registry ties prefer the least nested QN, then the smaller QN; the
budget share is measured in thread CPU time like the budget itself. Two kernel indexes are now
identical (8,529,723 nodes, 1,974,940 CALLS, zero churn), as are two elasticsearch indexes.

Findability. search_graph's BM25 path honours the `label` filter and ranks an exact-name definition
above test methods that repeat the token (15/15 probe queries).

Coverage. The parse-coverage gap tree subtracts definitions the walker recovered inside an error
region (multi-line only); kernel/sched/core.c reports 476 flagged lines instead of 7,674.

Precision. Config-language callers (Makefile, CMake, YAML) no longer bind unique_name into code
(kernel Makefile -> C CALLS 195 -> 0). JS-family token names ({}, 1, (x: T) => U) are not
definitions; computed, quoted and escaped member keys stay (TypeScript 204 junk -> 0, 1,782 real
names kept). Names are cut at their first line (elasticsearch YAML 8,306 -> 0); a comment inside an
argument list is not an argument, so no Route node is named by a Java block comment; the wildcard
route `/*` is still a route. HTML/Vue/Svelte/Astro embedded member calls stay unbound.

Recall. A Python call through a self- or cls-rooted attribute keeps a specific unique_name member
target (Django +751, elasticsearch +3,288 CALLS, no new callee names).

Robustness. Edge properties never carry a split UTF-8 sequence (the 120-byte expression cut and the
JSON escaper both trim a partial code point). CLI array flags accept a JSON-array literal. Cypher
seeds a single-hop pattern from its selective end (kernel `MATCH (a)-[:CALLS]->(b) WHERE b.name =
'_printk'`: execution limit -> 7.9 s).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-21 20:29:27 +02:00
Martin Vogel 6115e5785d Merge branch 'main' into fix/hook-augment-worktree-tests 2026-09-21 07:39:58 +02:00
Martin Vogel 9da4d66df2 Merge pull request #2115 from DeusData/fix/activation-guard-home-scope
fix(cli): scope the activation guard to the install-target HOME so a sandbox install can't drain the host daemon
2026-09-12 19:20:41 +02:00
Martin Vogel fa99ff64eb test(cli): stop a negative probe from spending 15s proving silence
The macos-15-intel leg killed the cli suite at its 900s wall clock while
reporting 7537 passed, 0 failed. Two separate things were wrong.

The suite was misclassified. cli spends 497s of the 900s default on macos-14,
the FASTEST macOS runner, while macos-15-intel in the same matrix is 2.4-3.6x
slower on comparable suites (daemon_runtime 842s vs 349s, stack_overflow_b
277s vs 76s). 497s at that ratio cannot fit. daemon_runtime, at 842s on that
same runner, passes only because it was already in SLOW_SUITES. cli belongs
there too -- it is deterministic and simply large, which is the only thing that
list is allowed to mean.

The drain test was also waiting on a clock instead of on the system. Profiling
the suite per test (315 tests, 300s wall / 166s cpu) put
cli_install_into_host_namespace_still_drains_host_cohort at 42s wall for 19s of
cpu -- 23s idle, the largest single idle block, where the next worst was 11s.
The existing cli_install_force_quiesces_active_cohort_before_replacing_binary
drains a cohort too and idles 0.5s, which is what pointed at the difference.

The cost was the host_serving probe. Its generous budget exists for the
POSITIVE question -- is this daemon still up? -- where a slow reply on a loaded
runner must not be misread as drained; that fixed a real flake and is kept.
Asked in the negative it inverts: no reply is coming, so the full 15s is spent
establishing silence and ASSERT_FALSE is decided by the timeout expiring rather
than by the daemon's behaviour. The drain is already proven positively in that
test -- install returns 0 only after the activation completed, and the host
child exits CLI_SCOPE_HOST_DRAINED -- so the probe only has to confirm it.

Also shortens the drained child's teardown budget, which retried service_free /
lease_release / manager_free against 10s on a path where the activation had
already torn the service down. Behaviour at the deadline is unchanged; it is
reached sooner when nothing is wedged. Measured at only -3s on its own, kept
because it is correct and free.

Local, same machine, ASan+UBSan: the drain test 42.0s -> 25.8s (-38%), the
suite 300s -> 279s, 315 passed before and after -- no test removed, no
assertion weakened. PR #2188, which runs the same leg without these two tests,
passed macos-15-intel in 37m34s, so the suite was not already over budget on
main.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-12 18:01:46 +02:00
Martin Vogel b790be3d15 Merge pull request #2110 from DeusData/fix/1954-uninstall-symlinked-config
fix(cli): don't let an agent-config failure abort uninstall; follow user-owned symlinked configs (#1954)
2026-09-12 04:19:56 +02:00
Martin Vogel 78a6d19f1e Merge branch 'main' into fix/activation-guard-home-scope 2026-09-11 23:26:36 +02:00
Martin Vogel 33534653f1 Merge branch 'main' into fix/1954-uninstall-symlinked-config 2026-09-11 23:26:33 +02:00
Martin Vogel ca36316c6b Merge branch 'main' into fix/2054-orphan-sidecars 2026-09-11 23:26:28 +02:00
Martin Vogel 713764bd0b Merge pull request #2116 from DeusData/fix/713-1826-nongit-guard-openhands
fix: bound non-git auto_index (#713) + complete OpenHands settings/profile registration (#1826)
2026-09-11 22:44:19 +02:00
Martin Vogel cc6743e9f3 Merge pull request #2060 from TheStranjer/fix/follow-own-symlinked-agent-roots
fix(cli): follow agent-root symlinks owned by the invoking user
2026-09-09 23:37:45 +02:00
Martin VogelandClaude Fable 5.1 4fa11d1ab0 fix(cli): remove orphan SQLite sidecars on index deletion (#2054)
cbm_remove_indexes unlinked each *.db and *.db.tmp but never the SQLite
sidecars (<db>-wal/-shm/-journal), so `cbm update`, `cbm uninstall`, and
`install --reset-indexes` (the four callers of the single deletion
chokepoint) left orphan -wal/-shm files behind. Call the existing
wide-path-safe cbm_remove_db_sidecars() helper on both the live and staged
db paths after the .db unlink; it is idempotent and ENOENT-tolerant, so an
orphan -wal that outlived its .db is still cleaned. Sidecars are not
indexes, so the removed-count is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-09 21:06:27 +02:00
Martin VogelandClaude Fable 5.1 92cb3b9a4b fix(test): bound the cli activation-guard scope fixture's host waits (#2115)
The four namespace-scope tests fork a real host daemon and, in the parent,
waited on it with two UNBOUNDED calls: read() for the child's one-byte
readiness signal in cli_scope_fixture_start, and waitpid(host, 0) in
cli_scope_fixture_finish. If the forked child stalls before it can signal —
a fork-time sanitizer allocator stall is the classic cause, the same hazard
the earlier posix_spawn work addressed — the parent blocks forever and the
WHOLE cli suite hangs to the CI wall-clock kill (the observed rc=124 at 900 s
on the ubuntu-24.04-arm gcc shard, which passed on every other #2115 shard).

Attribution: the production drain path is already bounded (DRAIN 15 s, CONTROL
2 s) and a foreign-namespace install takes the non-blocking scope probe and
skips draining entirely, so this is not a production deadlock — it is a
test-fixture determinism gap, amplified by ASan on a loaded arm runner.

Fix, test-only:
- cli_scope_wait_ready(): poll the ready pipe to a generous, bounded deadline
  (90 s, comfortably past the child's own 45 s cohort-admission deadline plus
  service start), so a wedged child yields a clean ASSERT_TRUE(ready) failure
  instead of an unbounded read().
- cli_scope_reap_host(): reap with WNOHANG to a bound (60 s), then SIGKILL and
  reap, so a wedged child is force-killed rather than hanging finish(); a
  killed child leaves host_exit == -1, which fails the caller's assertion
  cleanly. This also prevents a stuck child from holding cohort locks that
  would poison later tests.
- cli_scope_host_serving(): raise the status probe deadline from 5 s to 15 s so
  a slow-but-alive daemon on a loaded runner is not misread as "drained" (the
  flaky ASSERT(host_serving) failure this fixture showed); it still fails
  cleanly when the daemon is genuinely gone or reports stopping.

None of these decides a healthy test — the child signals in well under a
second on a healthy runner; the bounds only convert an otherwise unbounded
hang into a deterministic pass/fail.

The scoping contract still binds: with cli_activation_resolve_scope forced to
always return ACTIVE, the three "must-not-drain" tests go RED at
ASSERT(host_serving) and the host-namespace "must-drain" test stays GREEN.
cli suite: 313 passed locally (macOS, twice); 30-round pre-fix and 8-round
post-fix stress of the four tests were clean; a fault-injected never-ready
child fails all four cleanly in ~37 s with no suite hang.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-09 00:56:55 +02:00
Martin VogelandClaude Fable 5.1 a3c24a71b4 test(cli): guard POSIX-only fd-capture helpers under _WIN32 (#2110)
cli_fd_capture_begin/end use POSIX fd redirection (dup/dup2/tmpfile) and
are called only by the uninstall tests, which are themselves #ifndef
_WIN32. Defined at top level, the helpers were compiled but unused on
Windows, tripping -Werror,-Wunused-function on the test-windows CLANG64
leg. Wrap the struct and both helpers in #ifndef _WIN32 to match their
callers. Test-only; no production change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 23:00:59 +02:00
Martin VogelandClaude Fable 5.1 8135be28a7 fix(cli): remove the install dir from the Windows user PATH on uninstall (#2117)
On Windows, install registers the install directory in the persistent
HKCU\Environment\Path, but uninstall never removed it — so every
install/uninstall cycle left its entry behind and the user PATH grew
without bound (a real test host reached ~8000 chars, emptying the
effective PATH and breaking tooling).

The reported non-ASCII "mojibake" is historical, not a current-code bug.
Both install paths already write the value through the UTF-16 registry
API: install.ps1 (shipped in each release archive) runs `cbm install`,
which calls RegSetValueExW(L"Path", ...) — git history shows no ANSI
(RegSetValueExA / SetEnvironmentVariableA) predecessor — and
setup-windows.ps1 writes via .NET [Environment]::SetEnvironmentVariable
(Unicode) with an ASCII-only install dir. The only path that handles a
non-ASCII directory is the wide-correct C path, so current code cannot
produce a garbled entry; the garbage the reporter saw was left by an
older binary. No code change is needed for the encoding itself.

The real defect is the missing uninstall-side removal. This adds
cli_remove_windows_user_path(), the counterpart to
cli_ensure_windows_user_path(): it opens the same key, walks the
';'-separated segments, and drops exactly the ones equal to the install
dir using the same case- and trailing-separator-insensitive comparison
the install `present` scan uses, so it removes precisely what install
would have de-duplicated. Every other segment is preserved
byte-for-byte, the value kind (REG_SZ / REG_EXPAND_SZ) is preserved, and
a registry error is a warning rather than a hard failure so a user is
never blocked from finishing an uninstall over a cosmetic PATH edit.
Removing the entry on uninstall also stops accumulation on reinstall: a
garbled historical entry defeats the install de-dup and gets re-added,
so taking our own entry back out keeps the value bounded.

Testing:
- tests/test_cli.c: a hermetic Windows unit test drives the append and
  remove logic against a GUID-scoped scratch key via the existing
  CBM_TEST_WINDOWS_USER_PATH_RUN_ID seam, never the developer's live
  HKCU\Environment\Path. A non-ASCII install dir doubles as a mojibake
  guard (a broken UTF-8->UTF-16 round-trip would fail the match and leak
  a duplicate). It asserts append, idempotent reinstall, exact removal
  from the tail and the middle, idempotent removal, and byte-for-byte
  survival of unrelated entries.
- tests/test_smoke_fixture_contract.sh: a macOS-runnable source contract
  that fails when the removal is absent and passes with it, proving the
  wiring binds (verified RED-on-revert).

The Windows-only registry code is #ifdef _WIN32; its compile and the
behavioral unit test are verified by CI's Windows leg (the local Windows
VM is down). Pre-existing mojibaked entries from old binaries are a
separate one-time cleanup, out of scope here, and setup-windows.ps1's
own lack of an uninstall mode is noted as a possible follow-up.

Fixes #2117

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 22:08:39 +02:00
Martin VogelandClaude Fable 5.1 3a4160b461 fix(cli): complete the OpenHands integration (settings.json mcp_config + agent-profile refs) (#1826)
install wrote only part of the OpenHands config. Register the server globally
under ~/.openhands/settings.json -> mcp_config {transport:stdio, command, enabled}
(owned-entry, unrelated content preserved), and add "codebase-memory-mcp" to
each ~/.openhands/agent-profiles/<p>.json mcp_server_refs array (null -> [it],
existing -> appended once). uninstall removes exactly what it added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 20:38:22 +02:00
Martin VogelandClaude Fable 5.1 013e1f7ec7 fix(cli): scope the activation guard to the install-target HOME (amendment)
The rendezvous directory the CLI's activation guard talks to is per OS
account (service.h), not per HOME/CBM_CACHE_DIR: every daemon owned by
one user meets at the same endpoint, and only the cache fingerprint
carried in the cohort identity separates one HOME/CBM_CACHE_DIR
namespace from another. `cli_activation_production_reserve` used
`cbm_version_cohort_reserve_for_mutation`, which treats any lifetime-
lock holder at that shared endpoint as the daemon this activation must
drain, with no cache-fingerprint awareness at all. An `install` run
against a second HOME (a sandbox, a second profile, `--skip-binary`
into an alternate CBM_CACHE_DIR) therefore reached the LIVE host
daemon at the shared endpoint and drained it — disconnecting every one
of the host's real MCP clients — even though nothing the sandboxed
install touched belonged to the host's cache namespace.

The fix adds `cli_activation_resolve_scope`, a non-blocking probe via
the already cache-fingerprint-aware `cbm_version_cohort_acquire`, run
before the blind drain path. It distinguishes three cases from the
resolved target of THIS activation:
  - nothing to quiesce at all: a `--skip-binary` install that also
    resets no index publishes nothing, so no session anywhere needs to
    stop (`quiesce_required` on `cli_activation_guard_scoped`, plumbed
    from `cbm_cmd_install`'s own binary/index-reset decision);
  - the active cohort's cache fingerprint doesn't match this
    activation's target cache: a foreign namespace, left untouched;
  - same cache fingerprint (or no active cohort): this activation
    really does replace that daemon, so the existing drain/quiesce
    path runs unchanged, including the barrier wait when another
    activation already holds maintenance.

Verified against a real forked daemon (a live runtime service with one
committed client) standing in for the host: an install into a foreign
HOME/CBM_CACHE_DIR (`--skip-binary` and a full binary install) leaves
that daemon serving with its client still attached, while an install
that targets the host's own namespace still drains it and the
activation audit still names the client it disconnected.

New/updated tests in tests/test_cli.c (cli suite):
- cli_install_skip_binary_into_foreign_home_never_drains_host_cohort
- cli_install_binary_into_foreign_home_never_drains_host_cohort
- cli_install_into_host_namespace_still_drains_host_cohort (regression)
- cli_install_skip_binary_unchanged_in_host_namespace_quiesces_nothing
- cli_activation_quiesce_does_not_wait_on_bootstrap_startup updated to
  carry a real cache fingerprint for its participant, now that scope
  is fingerprint-gated

src/mcp/index_supervisor.{c,h}: CBM_CLI_ENABLE_TEST_API-gated setter
so the fixture's forked daemon can carry the same build fingerprint
the guard captures from the supervisor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 20:28:58 +02:00
Brian McNaboe e8dcb28ba7 test(hook-augment): pin worktree payload-cwd resolution
The hook resolves a payload cwd inside a linked git worktree to that
worktree own indexed project (and a subdir of it), never a sibling
checkout or nothing, and an unindexed worktree gets a deliberate
"no indexed graph project matched" notice rather than a silent 0 bytes.
Main-checkout resolution is unchanged. These regression tests use a real
linked worktree and lock the payload-cwd -> project invariant.

Fixes #1752

Signed-off-by: Brian McNaboe <bmcnaboe@gmail.com>
2026-09-08 14:08:59 -04:00
Martin Vogel d40ed1a6bb Merge remote-tracking branch 'origin/main' into fix/1954-uninstall-symlinked-config
# Conflicts:
#	tests/test_cli.c
2026-09-08 19:55:13 +02:00