Commit Graph
468 Commits
Author SHA1 Message Date
Martin Vogelandpcristin e5bb74916a feat(watcher): prune projects whose root stays missing (ENOENT-only, grace window)
Distilled from #738: the watcher now prunes a watched project whose root
directory has genuinely disappeared - deleting the cached DB (+wal/shm,
validated name, cache-dir-only paths) and removing the watch entry - so
vanished worktrees stop being watched forever (#286).

Hardened beyond the original PR to remove a data-loss hazard (the cached
DB can hold user-authored data such as the ADR, unrecoverable once
deleted):

- Only ENOENT/ENOTDIR stat failures count as missing. Any other failure
  (EACCES, EIO, transient mounts, macOS TCC revocation) resets the
  streak and logs watcher.root_stat_error with the errno; Windows
  (mingw/UCRT) maps not-found to ENOENT so the check holds there.
- Pruning requires BOTH >=3 consecutive missing polls AND a sustained-
  absence grace window since the streak's first miss (default 600s,
  override via CBM_WATCHER_PRUNE_GRACE_S), tracked with monotonic
  cbm_now_ms.
- Root reappearance resets streak + timestamp (watcher.root_restored).
- The pruned entry is released via the deferred-free list poll_once
  drains (one freeing model shared with cbm_watcher_unwatch).

Limitation: only currently-watched projects are pruned; stale DBs left
by older sessions are out of scope.

Co-authored-by: pcristin <xxxokzxxx@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:40:22 +02:00
Martin Vogel 09148ab121 Merge pull request #780 from SS-42/fix/adr-path-normalization
Normalize project paths for ADR lookups
2026-07-03 18:38:18 +02:00
Martin Vogel eaf2ca58b0 Merge pull request #771 from apappas1129/fix/767-imports-import-targetable-label
fix(pipeline): reject non-file-like IMPORTS targets in fallback strategies
2026-07-03 18:36:50 +02:00
Martin Vogel 6a6c7a98ca Merge pull request #766 from apappas1129/fix/730-tsconfig-alias-dotdot-climb
fix(pipeline): resolve ../ climbs in tsconfig path alias targets
2026-07-03 18:36:45 +02:00
Martin Vogel fa12a38c22 Merge pull request #757 from SyntaxSawdust/codex/issue-731-vs-schema
fix(mcp): tighten ingest traces schema for strict clients
2026-07-03 18:36:20 +02:00
Martin Vogel 3a1387edc7 Merge pull request #774 from minorityre/codex/sync-mcp-server-version
Sync MCP server version with binary version
2026-07-03 18:36:16 +02:00
Martin Vogel 23e4bce030 Merge pull request #537 from Golevka2001/main
fix: unwatch deleted projects to prevent zombie reindex
2026-07-03 18:35:39 +02:00
Martin Vogel b97d814788 Merge pull request #781 from SS-42/fix/parent-watchdog-startup
Exit promptly after parent death
2026-07-03 18:35:35 +02:00
Martin Vogel e89aa776a7 feat(index): report per-file indexing failures via skipped[] + logfile (Stage 2/B2-B4)
index_repository silently dropped files that failed to index: CBMFileResult.has_error
was set but never read, oversized files (>100 MB) were dropped with no signal, and
read/extract failures only bumped a logged-only counter — a file that couldn't be
indexed just vanished from the graph.

Collect and surface them:
- New retained {path, reason, phase} error list on struct cbm_pipeline (mirrors the
  excluded_dirs pattern) + accessor + a back-pointer on cbm_pipeline_ctx_t so both
  extraction paths can append. Wired in BOTH the sequential (pass_definitions) and
  parallel (per-worker, merged lock-free) paths — small repos take the sequential
  path, so wiring only one would leave the guard vacuous.
- Feeds: read-fail, extract-fail, the newly-CONSUMED has_error (parse timeout /
  parse failed, with error_msg), and oversized. The cross_lsp phase is reserved for
  the crash supervisor (Track C) and not fed here (the cross-LSP passes are
  best-effort with no failure return; feeding the no-source case would be false
  positives).
- MCP/CLI response gains top-level "skipped_count" (0 on clean) and, when >0, a
  capped "skipped":{files[<=50],count,truncated} + "logfile". Status stays "indexed"
  — a reported skip is the expected, handled outcome, not a degradation.
- Per-run logfile (full uncapped list) written ONLY when >=1 file is skipped:
  $CBM_INDEX_LOG override else <cache_dir>/logs/<project>-<epoch>.log.
- Generous env-configurable caps (src/foundation/limits.c): CBM_MAX_FILE_BYTES,
  default raised 100 MB -> 512 MiB; over-cap files are REPORTED (phase oversized) +
  WARNed, never silently dropped.

Reproduce-first: tests/test_index_resilience.c (gating) — an oversized file (cap
lowered via env) must appear in skipped[] with the 2 good files still indexed and a
logfile written; a clean run has skipped_count 0 and no logfile. Genuine guard:
no-op'ing the recorder makes the oversized file silently vanish (RED). Full suite
5750/0, no ASan/UBSan.

Part of the resilient-indexing effort (Track B surfacing layer). Refs #668.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 22:11:00 +02:00
SS-42 273a3af008 fix(mcp): normalize project paths for ADR lookups
Signed-off-by: SS-42 <noreply@incogni.to>
2026-07-02 22:27:49 +03:00
SS-42 f607c0fc32 fix(main): exit promptly after parent death
Signed-off-by: SS-42 <noreply@incogni.to>
2026-07-02 21:43:07 +03:00
armand 6c56b8fd91 Sync MCP server version with binary version
Signed-off-by: armand <armandhell123@gmail.com>
2026-07-02 16:40:25 +02:00
Alexandros Pappas fe2b072d02 fix(pipeline): reject non-file-like IMPORTS targets in fallback strategies
A wildcard tsconfig path alias (e.g. "@lib/*" -> "./src/lib/*") matches
any import specifier sharing that prefix, including an unrelated scoped
npm package meant to resolve from node_modules. The direct match
already failed cleanly (no node exists at the substituted path), but
cbm_pipeline_resolve_import_node's Strategy 4 - a generic "strip a
trailing path segment and retry" fallback applied to every language,
not just its intended Rust crate::/self::/super:: case - re-resolves
the truncated import string and can land on the tsconfig's OTHER, bare
alias entry that maps straight to a directory. The resulting QN
collides with that directory's real Folder node (cbm_pipeline_fqn_module
and cbm_pipeline_fqn_folder both dot-join path segments with no type
tag), and Strategy 4 accepted whatever node it found there.

Strategy 3 (symbol-name fallback) already guards its matches with
import_targetable_label(), which excludes Folder/Project/etc. Strategy
1b (resolve_sibling_file, used only for markup/build-tool sibling
imports - SCSS/Just/BitBake/Meson/Pony, none of which are directory-
module languages) and Strategy 4 lacked the same guard; both now use
it too.

Strategy 1 (the primary, direct resolution path) intentionally keeps
no such filter: Go and Java are directory-module languages in this
pipeline (cbm_lang_module_is_dir), so a bare package import correctly
and deliberately resolves straight to a Folder node there - adding the
guard to Strategy 1 broke that (verified via the Go/Java integration
tests) before being scoped down to just the fallback strategies where
the collision actually originates.

Closes #767.

Signed-off-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
2026-07-02 21:20:22 +08:00
Alexandros Pappas 70377035ae Merge branch 'main' into fix/730-tsconfig-alias-dotdot-climb 2026-07-02 19:51:36 +08:00
Martin Vogel b9b7905309 Merge pull request #749 from DeusData/distill/672-git-canonical-root
fix(git): correct canonical_root for worktrees and subdirectory projects
2026-07-02 12:33:01 +02:00
Alexandros Pappas 8cc1d1e9fa fix(pipeline): resolve ../ climbs in tsconfig path alias targets
tsconfig.json `paths` entries that climb out of their config's directory
(e.g. a tsconfig at apps/web/tsconfig.json mapping "@shared/*" to
"../../packages/shared/src/*", the standard monorepo layout) resolved to
a broken path: resolve_target_relative() only stripped a single leading
"./" and otherwise concatenated dir_prefix and target verbatim, leaving
literal ".." segments in the result. Those never match a real module's
FQN, since cbm_pipeline_fqn_module tokenizes on '/' without collapsing
them - so every cross-package import through such an alias looked like
dead code.

resolve_target_relative now walks the target's path segments and pops
a directory off dir_prefix for each "..", the same normalization
cbm_pipeline_resolve_relative_import already does for plain relative
imports.

Closes #730.

Signed-off-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
2026-07-02 17:41:22 +08:00
Martin Vogelandanivaryam 3f3ae855b8 fix(git): derive canonical_root from git's own absolute common-dir
Replace the manual join(input_path, --git-common-dir) + realpath/_fullpath with
`git rev-parse --path-format=absolute --git-common-dir` (git 2.31+), which returns
git's own absolute, canonical common-dir. Because a main repo and its linked
worktree both query the same git binary, they resolve to the IDENTICAL path, so
canonical_root is consistent across worktrees and platforms with no
platform-specific path canonicalization.

This is the truly platform-agnostic #659 fix:
- git resolves the relative ".." internally, so a project indexed from a
  subdirectory gets the real repo root (the #659 bug).
- No realpath/_fullpath, so the msys-vs-native path-representation divergence that
  broke the worktree==main-root invariant (test_pipeline.c
  git_context_linked_worktree) on the Windows CI leg is eliminated — both sides
  get git's single canonical representation.

Falls back to the previous relative-join derivation (Unix realpath) when git is
older than 2.31 and doesn't support --path-format (abs_common_dir empty). The
input_path-join fix and the Windows test-harness SKIP_PLATFORM guards from the
earlier commits on this branch remain for that fallback path.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: anivaryam <anivaryam.dev@gmail.com>
2026-07-02 11:22:03 +02:00
Martin Vogel 01c5921952 Merge pull request #751 from DeusData/distill/693-scoped-search-spaces
fix(search): scoped search must handle repo paths containing spaces
2026-07-02 09:56:10 +02:00
Dustin Persek 540ca5f5c9 fix(mcp): tighten ingest traces schema
Signed-off-by: Dustin Persek <dustin.persek@protonmail.com>
2026-07-01 22:57:50 -04:00
Martin Vogel 1cca8588e8 Merge pull request #750 from DeusData/fix/683-cbm-fopen-worktree
fix(discover): use cbm_fopen for worktree gitlink + commondir reads
2026-07-02 01:51:07 +02:00
Martin Vogelandlg320531124 3dd24719b0 fix(search): scoped search must handle repo paths containing spaces
search_code's scoped grep writes an "<root>/<file>" filelist and pipes it to
xargs. With a newline-separated filelist + plain `xargs`, a space anywhere in
the absolute path splits one file into several bogus arguments, so grep finds
nothing and scoped search returns 0 matches for any repo indexed under a path
containing a space (#687).

Fix: write NUL-separated records and consume them with `xargs -0`, so each path
stays a single argument regardless of spaces. On Windows the scoped path uses
PowerShell `Get-Content | Select-String -LiteralPath`, which reads line-by-line
and already handles spaces, so the filelist keeps newline separators there.

write_scoped_filelist writes each record piece-by-piece with fwrite/fputc (no
fixed-size stack buffer), so arbitrarily long absolute paths cannot overflow.

Reproduce-first: search_code_scoped_path_with_spaces_issue687 indexes a repo
rooted at "<tmp>/my project" and asserts total_grep_matches > 0 (RED on the old
newline + plain-xargs code -> 0 matches; GREEN after). Full suite green.

Distilled from #693 (thanks @lg320531124). Keeps that PR's correct NUL/xargs-0
idea and its Windows newline branch, but replaces its char buf[2048] + unbounded
memcpy (a stack buffer overflow reachable via long indexed paths, PATH_MAX 4096)
with the direct piece-by-piece write, and adds the reproduce-first guard it
lacked.

Closes #687

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: lg320531124 <lg320531124@users.noreply.github.com>
2026-07-02 01:24:29 +02:00
Martin Vogel abda9641b4 Merge pull request #748 from DeusData/distill/650-trace-ambiguity
fix(mcp): trace_path must not conflate distinct same-named functions
2026-07-02 00:48:59 +02:00
Martin Vogel e82f6da0a9 fix(discover): use cbm_fopen for worktree gitlink + commondir reads
Follow-up to #683 (@Naam). The worktree info/exclude fix read the `.git` gitlink
file and its `commondir` with raw fopen(); on Windows raw fopen uses the ANSI
code page and fails to open non-ASCII (e.g. CJK) repo paths, so a worktree
indexed under such a path would silently not honor info/exclude /
core.excludesFile. Switch both reads to cbm_fopen() (the UTF-8 -> _wfopen
wrapper), matching the rest of discover.c (e.g. read_core_excludes_file at 248).

No behavior change on POSIX (raw fopen already opens UTF-8 byte paths there);
this is a Windows-only correctness/consistency fix, verified by the full test
suite staying green and cross-platform CI. discover.c now has no raw fopen.

Refs #682, #683

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 00:40:31 +02:00
Martin Vogel 46c11ff189 Merge pull request #683 from Naam/fix/worktree-info-exclude
fix(discover): honor info/exclude and core.excludesFile in linked worktrees
2026-07-02 00:31:29 +02:00
Martin Vogelandanivaryam 354befe557 fix(git): correct canonical_root for worktrees and subdirectory projects
detect_changes returned an empty impacted_symbols set for projects indexed
inside a git worktree or from a repo subdirectory (#659): canonical_root was
computed wrong. `git rev-parse --git-common-dir` emits a path relative to the
directory passed via -C (input_path), NOT to worktree_root, and the "/.git"
suffix was stripped textually without resolving ".." components. So e.g.
"../.git" joined against worktree_root, or "/ws/scripts/../.git" strip, left an
unresolved ".." and never matched the project's stored root path.

Fix (src/git/git_context.c): join the relative --git-common-dir against
input_path (the -C dir) instead of worktree_root, and realpath()/_fullpath()-
normalize the result BEFORE stripping "/.git" (the .git dir always exists for a
valid repo, so resolution succeeds). This is consistent with the existing
realpath'd root_path used for prefix matching (mcp.c).

Reproduce-first: tests/test_git_context.c. The subdirectory case is the genuine
guard — a repo indexed from a subdir yields a *relative* --git-common-dir, so the
unfixed code leaves an un-normalized "<root>/subdir/.." (verified RED; GREEN after
the realpath fix). The linked-worktree case is a supporting invariant, not the
#659 reproducer: git that emits an *absolute* worktree --git-common-dir (e.g.
2.48.x) doesn't manifest the bug, so it passes either way; it still enforces the
worktree->main-root invariant. All three git-based tests SKIP_PLATFORM on Windows
(the CI shell there cannot init a repo via system()).

Distilled from #672 (thanks @anivaryam); that PR's production fix is taken
verbatim. Its repo_root/subdir tests ran git unconditionally and failed the
Windows CI leg ("failed to init git repo"); here all three git-based tests are
Windows-skipped so ci-ok is green cross-platform. Supersedes #696, which only
realpath'd after the strip without changing the join base and so still yielded
the workspace parent for the subdirectory case.

Closes #659

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: anivaryam <anivaryam.dev@gmail.com>
2026-07-02 00:19:16 +02:00
Martin VogelandMatt Van Horn 7d6d52b9d7 fix(mcp): trace_path must not conflate distinct same-named functions
The #546 fix (bfs_union_same_name) BFSs from every graph node sharing the
resolved name and merges the results, so a symbol split across a real
implementation and a body-less ambient .d.ts stub reports a complete caller
set. But pick_resolved_node flagged ambiguity only on an exact score tie, and
node_resolution_score adds the body span, so two genuinely-DIFFERENT functions
that share a name but differ in body length score differently, dodge the tie,
and get their caller sets unioned into one confidently-conflated answer -- the
mirror of the #546 under-report.

Complete the ambiguity rule: two or more *real* callable definitions sharing a
name (callable label + non-empty body span) are ambiguous and routed to the
existing disambiguation (snippet_suggestions) path. A body-less span==start node
is an ambient declaration / signature stub -- a fragment of one logical symbol
-- so a real implementation plus its .d.ts stub stays non-ambiguous and still
unions, preserving the #546 behavior.

Reproduce-first: tool_trace_call_path_distinct_defs_not_over_unioned (two
unrelated same-named functions with different spans -> merged caller set on the
old code = RED; ambiguous on the fix = GREEN) plus
tool_trace_call_path_dts_stub_unions_with_impl guarding that the stub+impl union
still works. Full suite green.

Distilled from #650 (thanks @mvanhorn); its trace_-prefixed reimplementation
collided with main's already-landed bfs_union_same_name and could not merge.

Refs #546, #650

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-01 23:37:56 +02:00
Martin Vogel c9dd016883 Merge pull request #745 from DeusData/fix/project-name-length-cap
fix(fqn): cap derived project-name length to avoid ENAMETOOLONG
2026-07-01 22:32:27 +02:00
Martin Vogel 2d48c06042 Merge pull request #703 from ShauryaaSharma/fix/windows-non-ascii-700
fix(windows): use _wfopen for non-ASCII project paths
2026-07-01 22:13:09 +02:00
Martin VogelandMatt Van Horn db20e53459 fix(fqn): cap derived project-name length to avoid ENAMETOOLONG
cbm_project_name_from_path hex-encodes non-ASCII path bytes (#571), but had no
length cap -- a deep CJK / non-ASCII path triples in length past the
filesystem's 255-byte filename-component limit, so <cache>/<name>.db becomes
un-openable (ENAMETOOLONG).

Cap the derived name at FQN_MAX_NAME_LEN (200): names within the cap are
returned UNCHANGED (no drift for already-indexed projects), while a longer name
keeps its first (CAP-9) bytes plus an FNV-1a hash of the full name as a "-%08x"
suffix, so distinct long paths stay distinct. The hex encoding scheme is
unchanged.

Distilled from #624 by Matt Van Horn (the ENAMETOOLONG catch); the rest of that
PR was redundant with the #571 fix already on main and would have swapped the
encoding scheme, causing project-name drift.

Reproduce-first: project_name_length_capped_issue624 builds a deep CJK path
(~376-byte raw name) and asserts the result is <= 200, validator-safe, and that
two long paths differing only in the trailing segment map to different names; a
short CJK path stays byte-identical (no drift). RED (376 > 200) -> GREEN. Full
suite 5738/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-01 21:23:12 +02:00
Martin Vogel 7cf1e4d213 Merge pull request #619 from catsmonster/fix/hook-augment-windows-paths
fix(hook): make hook-augment work on Windows drive-letter paths
2026-07-01 20:59:52 +02:00
Martin Vogel 67099fa249 Merge pull request #736 from DeusData/fix/deprecate-raw-json-cli
feat(cli): deprecation-warn raw-JSON args + full flag-ops smoke coverage
2026-07-01 17:21:27 +02:00
catsmonster 4bb9effe4c fix(hook): make hook-augment work on Windows drive-letter paths
hook-augment's PreToolUse Grep/Glob augmenter required POSIX-style
'/'-prefixed absolute paths in two guards, so a Windows cwd (C:\repo or
C:/repo) always failed: the _WIN32 cwd check returned early, and the
walk-up loop guard (dir[0] == '/') never iterated. The result was a
structural no-op on Windows — zero additionalContext for every Grep/Glob,
even for an exactly-indexed symbol.

Add cbm_hook_path_is_abs(), accepting POSIX "/..." and Windows "X:/..." roots;
normalize backslashes to forward slashes on the Windows cwd; and stop the
parent-climb at the drive root as well as the POSIX root. POSIX behavior
is unchanged.

Adds a cli_hook_augment_path_is_abs regression test (the predicate is
exposed as cbm_hook_path_is_abs via cli.h) covering POSIX and Windows
drive roots — it fails on the pre-fix POSIX-only guard and passes after.

Verified with that test plus a standalone walk self-test (full path
resolves first, then climbs to one level below the drive root, symmetric
to POSIX); clang-format clean on all three files; clang -fsyntax-only on
the translation unit. Not built through the full toolchain locally (no
make/zlib) — relying on Windows CI to confirm the build.

Fixes #618

Signed-off-by: catsmonster <shaked.brand@gmail.com>
2026-07-01 06:19:30 -06:00
Martin Vogel 5c92f5c63e feat(cli): deprecation-warn raw-JSON args + full flag-ops smoke coverage
Raw JSON (cli <tool> '{...}') still works for back-compat, but now emits a
deprecation warning to STDERR (stdout stays clean JSON for piping) nudging
users to flags / --args-file / stdin. Only the raw-JSON path warns.

Also make scripts/smoke-test.sh exercise the whole new CLI input surface
end-to-end against the built binary: every tool call migrated from raw JSON to
FLAG form (index_repository / search_graph / trace_path / get_graph_schema /
query_graph incl. all Cypher checks / get_architecture / search_code /
delete_project -- flag form passes the query as one argv token, so the old
JSON-escaping dance is gone), plus dedicated guards for an integer flag
(--limit / --depth), a bare boolean (--exclude-entry-points), a repeated array
flag (--semantic-query -> JSON array), piped stdin, --args-file, and per-tool
--help (RC 0 + shows a flag; unknown tool -> RC!=0). One raw-JSON call is kept
as the deprecation guard (asserts the warning fires; the flag form asserts it
does not).

Reproduce-first: reverting the warning makes the raw-JSON smoke assertion fail
("did not emit a deprecation warning"); restored -> green. Full smoke ALL
PASSED; C unit suite 5735/0 (unchanged -- only main.c + the smoke script).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-01 13:27:03 +02:00
Martin Vogel d4c0b6bdbc fix(mcp): keep available_projects error JSON valid when many projects (#235)
collect_db_project_names built the "project not found or not indexed" error's
available_projects list into a fixed 4 KB buffer and, on overflow, let snprintf
truncate the last name mid-token then clamped offset to the buffer end -- so the
error response was malformed, unterminated JSON (e.g. ...,"tmp-cb],"count":50})
whenever enough projects were indexed to overflow the buffer. A client parsing
the error would fail. Found while exercising the CLI against a cache with 280
indexed projects.

Write element-boundary instead: before each name, require the whole element
(optional leading comma + the quoted name + NUL) to fit in the remaining buffer;
otherwise stop at that boundary. The array then always holds only complete names
and count == its length, so the JSON is always valid (just fewer names when the
buffer is full -- callers can run list_projects for the complete set).

Reproduce-first: tool_bad_project_error_valid_json_issue235 builds 50 dbs with
~120-char internal names (well over 4 KB), triggers the error, and asserts the
response parses as JSON and available_projects is an array with length == count.
RED before (yyjson_read returns NULL on the truncated buffer), GREEN after. The
existing tool_bad_project_name_no_overflow_issue235 (buffer bounds) still passes.
Full suite 5736/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-01 12:58:32 +02:00
Martin Vogel 4a422856e3 Merge pull request #719 from DeusData/fix/readonly-query-open
fix(store): open query stores READONLY so queries never mutate dbs
2026-06-30 18:54:07 +02:00
Martin Vogel ad422bc59a Merge pull request #717 from DeusData/fix/704-store-resolution
fix(mcp): resolve projects by internal name + filter ghost dbs (#704)
2026-06-30 18:53:45 +02:00
Martin Vogel c11e659047 Merge pull request #716 from DeusData/fix/680-cli-flags
feat(cli): accept flags, stdin, and --args-file for tools + per-tool --help (#680)
2026-06-30 18:52:46 +02:00
Martin Vogel 8ec7523a30 fix(store): open query stores READONLY so queries never mutate dbs
Query tools opened the project db with SQLITE_OPEN_READWRITE and ran write
pragmas (PRAGMA journal_mode=WAL + wal_checkpoint), so every query MUTATED the
db (rewrote the header, created -wal/-shm) and FAILED outright when the db or
its directory was read-only (the WAL pragma cannot create -shm on a read-only
filesystem -> resolve_store returned NULL -> "project not found"). Latent
data-integrity bug found while diagnosing #704 (a query grew a 0-byte shadow
db to 100 KB).

- cbm_store_open_path_query now opens SQLITE_OPEN_READONLY. configure_pragmas
  gains a read_only mode that runs only the non-writing pragmas (foreign_keys,
  temp_store, busy_timeout, mmap_size) and skips journal_mode / wal_checkpoint
  / synchronous. The read-write open (cbm_store_open_path) is unchanged.
- Read-only-filesystem fallback: SQLite opens lazily, so a RO-FS failure
  surfaces on first access; we probe with SELECT 1 and, only if the file
  exists, retry once with a `file:<path>?immutable=1` URI (URI-special
  characters percent-encoded) which bypasses WAL and reads the main db
  directly. The fallback is gated to that failure path so writable-fs readers
  still see uncheckpointed WAL frames via -shm.
- manage_adr is the only resolve_store caller that writes; it now opens its own
  read-write handle (cbm_store_open_path) for ADR storage (in-memory/embedded
  stores still write through the resolved store). All 12 resolve_store callers
  audited; query_graph is read-only (the Cypher engine rejects writes).

Reproduce-first (non-vacuous, no real read-only mount needed):
- readonly_query_does_not_mutate_db: snapshot a rollback-journal db's bytes,
  run search_graph, assert the file is byte-identical + no -wal created. RED
  before (the WAL pragma rewrote the header).
- readonly_query_succeeds_on_readonly_fs: chmod the containing DIRECTORY 0555
  (the real read-only-fs symptom -- a 0444 file alone is vacuous since SQLite
  downgrades the open), run search_graph, assert it succeeds. RED before (WAL
  pragma could not create -shm -> "project not found"); exercises the immutable
  fallback.
Full suite 5724/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 16:48:40 +02:00
Martin Vogel 1d1772a24f fix(mcp): resolve projects by internal name + filter ghost dbs (#704)
Project resolution was filename-addressed but internal-name-validated:
resolve_store opened <cache>/<passed>.db and required the internal
projects.name row to equal the passed name, while list_projects advertised
the .db FILENAME. When a db's filename differed from its internal name (a
copied/renamed db, or a legacy '.'-vs-'-' username twin from a past path
sanitization), it showed in list_projects but every query returned "project
not found or not indexed". Node rows are keyed on the internal name, so just
opening the file is not enough -- queries must use the internal name too. The
intermittency ("works now, fails 10 min later") was a warm in-process store
cache masking a broken cold resolve; a fresh cli call is always cold.

Key list + resolve on the INTERNAL project name, with no file renames or
mutation:
- list_projects / collect_db_project_names / build_project_json_entry now
  advertise each db's internal projects.name (via db_internal_project_name)
  and skip ghost/empty/corrupt dbs (0-byte, no project row) so the listed
  names are exactly the ones that resolve.
- resolve_store keeps the fast path (open <passed>.db, get_project -- the
  common case where filename == internal name). On a miss it runs a bounded
  cache-dir scan, adopting the db whose sole internal project name equals the
  passed name (gated on exactly one project row so it never serves another
  project's data). No match -> NULL (a typo stays not-found). The scan runs
  only on the fallback.

Reproduce-first: tool_resolve_store_by_internal_name_issue704 builds an
isolated CBM_CACHE_DIR with a control db, a drifted db (filename != internal
name), and a 0-byte ghost; asserts list advertises the internal name (not the
filename), the drifted project resolves via the scan, and the ghost is neither
listed nor resolvable. RED (113/1, list lacked the internal name) -> GREEN
(114/0). Full suite 5723/0.

Also hardened tool_bad_project_name_no_overflow_issue235: its fixture used
1-byte non-SQLite files that the new ghost filter would skip, making the
buffer-overflow guard vacuous; rebuilt it with valid dbs carrying long
internal names so the guard still fires.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 16:11:07 +02:00
Martin Vogel 3c2c12006f feat(cli): accept flags, stdin, and --args-file for tools + per-tool --help (#680)
The CLI only accepted a single inline JSON argument (cbm cli <tool> '<json>'),
which PowerShell mangles on Windows and which made `cli <tool> --help` return
"repo_path is required" -- the "--help" token was parsed as the JSON args, and
there was no per-tool help at all.

run_cli now resolves tool arguments by precedence: --args-file <path>, a raw
JSON object (back-compat), --flag / --flag=value / bare-boolean flags, piped
stdin, then empty {}. Flags are converted to a JSON object via the tool's
input_schema: kebab-case keys map to snake_case, values are typed
(string/integer/boolean), and repeated flags accumulate into array-typed
properties (--semantic-query a --semantic-query b -> ["a","b"]). stdin and
--args-file are UTF-8-clean and bypass shell quoting entirely (the robust path
on Windows). `cli <tool> --help` prints usage + the tool's flags, generated
from the same input_schema the MCP tools/list advertises -- no hand-maintained
table.

cbm_mcp_tool_input_schema() exposes a tool's schema; cbm_cli_build_args_json()
and cbm_cli_print_tool_help() do the conversion and help.

Reproduce-first: 8 cli tests (string / integer-not-string / bare-boolean /
repeated-array / kebab->snake / --key=value with spaces / bad-positional-errors
/ help) -- RED on a stub, GREEN after. Full suite 5730/0.

Note: true CJK-path support on Windows still needs a UTF-8/wmain entry point
(argv is ANSI-codepage-mangled upstream of parsing); stdin/--args-file mitigate
it and are tracked separately.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 15:44:36 +02:00
Martin Vogel 8b56c52231 fix(mcp): accept project_name alias + clearer no-store error (#640)
Query tools resolved the project only via the exact "project" key, but
list_projects surfaces the field as "name" and the not-found hint said
"pass the project name", so callers naturally pass "project_name". With no
alias the arg was NULL and resolve_store bailed before opening any .db, so
every query returned "project not found or not indexed" even though the
project was indexed -- the reporter's strace shows the cache dir scanned but
no .db opened. Reproduced on macOS; not Linux-specific.

Add get_project_arg() accepting project | project_name | project_id |
projectName across the 11 query handlers (not bare "name" -- index_repository
uses it for an explicit project-name override). Split the no-store error so a
missing argument reports "missing required argument: project" instead of
masquerading as "not indexed", and name the literal "project" key in the
not-found hint.

Reproduce-first: tool_get_architecture_accepts_project_name_alias_issue640
and tool_search_graph_accepts_project_name_alias_issue640 (RED before ->
GREEN after). Full suite 5724/0, no regressions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 12:51:49 +02:00
ShauryaaSharma 98218a282b fix(windows): use _wfopen for non-ASCII project paths (#700)
Standard fopen on Windows uses the ANSI/system codepage and silently
fails to open files whose absolute path contains non-ASCII characters
(accented, CJK, etc.). This causes the pipeline to skip parsing those
files entirely — File nodes are created by directory traversal (which
already uses wide Win32 APIs) but tree-sitter never receives the source,
so no Function/Class/Method nodes are extracted.

Add cbm_fopen(path, mode) to compat_fs:
- Windows: converts the UTF-8 path to UTF-16LE via cbm_utf8_to_wide
  (existing helper in win_utf8.h) and calls _wfopen.
- POSIX: delegates to fopen (no-op change, paths are already UTF-8).

Replace every fopen call that operates on user project file paths:
- src/pipeline/pass_parallel.c    (primary indexing path)
- src/pipeline/pass_definitions.c
- src/pipeline/pass_calls.c
- src/pipeline/pass_semantic.c
- src/pipeline/pass_usages.c
- src/pipeline/pass_lsp_cross.c
- src/pipeline/pass_pkgmap.c
- src/pipeline/pass_k8s.c
- src/pipeline/pass_envscan.c
- src/pipeline/path_alias.c       (tsconfig detection)
- src/pipeline/artifact.c
- src/discover/language.c         (content-sniff for language detection)
- src/discover/gitignore.c
- src/discover/discover.c
- src/discover/userconfig.c
- src/mcp/mcp.c                   (read_file_lines, adr.md reader)

Fixes #700

Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
2026-06-30 01:03:09 +05:30
Shane McCarron 5255939a95 fix(install): address QA round 1
QA round 1 (PR #632) flagged that the SubagentStart hook uses matcher "*",
the same matcher a user is most likely to pick for their own catch-all
SubagentStart hook. Because is_cmm_hook_entry keyed ownership on the matcher
string alone, install would remove the user's "*" entry and replace it with
ours, and uninstall could remove the wrong one.

Add an optional `match_command_substr` to the upsert/remove args, threaded into
is_cmm_hook_entry: when set, an entry must ALSO carry a hooks[].command
containing that substring to be claimed as ours. The Claude SubagentStart hook
passes "cbm-subagent-reminder"; all existing callers leave it NULL, preserving
their matcher-only behavior unchanged. Install/uninstall now only ever touch
CMM's own entry and never clobber a foreign "*" hook.

Add cli_claude_subagent_hook_preserves_user_entry covering the case: a
pre-existing user "*" SubagentStart hook survives both install and uninstall.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Shane McCarron 64af0b8c7e feat(install): inject CMM code-discovery reminder into Claude subagents
Subagents spawned via the Agent tool do not fire SessionStart, so the
existing SessionStart reminder never reaches them — they start without the
codebase-memory-mcp code-discovery guidance and fall back to grep/file-read.

Register a Claude Code SubagentStart hook (matcher "*") that injects a leaner
variant of the protocol via JSON additionalContext, omitting the
index_repository step since the parent session has already indexed the
project. SubagentStart injects context only via a JSON object on stdout, not
plain text, so the generated script emits a static JSON literal — no runtime
escaping and no python3/jq dependency. Advisory only; installed and removed
alongside the SessionStart hook in install_claude_code_config.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Nahim El Atmani f1b93829dc fix(discover): honor info/exclude and excludesfile in linked worktrees
In a linked worktree, <repo>/.git is a regular file ("gitdir: <path>")
rather than a directory. cbm_discover_ex gated all per-clone ignore
sources on .git being a directory, so inside a worktree it skipped
.git/info/exclude and the global core.excludesfile entirely and walked
into directories the user had excluded there (e.g. build/, twister-out/).
Committed .gitignore still applied via the issue #510 path, which is why
the gap was easy to miss.

Resolve the git common dir up front: when .git is a gitlink file, parse
'gitdir:', then follow <gitdir>/commondir to the shared dir where git
keeps info/exclude and config (falling back to the gitdir when no
commondir file is present). info/exclude and config are then read from
that common dir, so a worktree behaves like an ordinary checkout.

Adds discover_worktree_info_exclude (fails before this change) and
discover_worktree_committed_gitignore.

Signed-off-by: Nahim El Atmani <2959826+Naam@users.noreply.github.com>
2026-06-28 21:05:07 -07:00
Martin Vogel f7d6d34457 feat(diag): persistent memory trajectory + query-leak CI soak for #581
#581 (query-path memory growth) does not reproduce in our soak: a 10-min,
200-file query-only run shows RSS flat-to-shrinking and committed plateauing
(mimalloc already purges on free, purge_delay=0). Rather than ship speculative
mitigations for a leak we cannot measure, this adds the observability to find it
where it actually occurs, plus a continuous guard.

- diag: the diagnostics writer now appends a PERSISTENT NDJSON memory trajectory
  (one sample / 5s: rss, committed, peak fields, page_faults, fd, queries), kept
  on exit (rotates to a .1 generation past 8 MB) so users can send the time
  series post-mortem. The previous latest-snapshot file was overwritten every 5s
  and deleted on stop.
- ci: the soak now also runs a read-only query-leak leg (CBM_SOAK_MODE query-leak,
  no reindex/mutate) on every platform including Windows, so a regression that
  introduces a query-path leak is caught. soak-test.sh RESULTS_DIR is now
  env-overridable so both legs keep separate artifacts.
- docs: README Troubleshooting and Diagnostics section + a bug-report field
  explain the no-telemetry stance and how to capture and share the trajectory.

Build clean; unit suite 5714/0; trajectory verified to persist post-exit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 23:28:01 +02:00
Martin Vogel b7f1a71a6b style: apply clang-format to new mcp/ui/logging lines
The CI lint gate (scripts/lint.sh --ci, clang-format-20) flagged 7 files whose
new lines were not run through the project formatter. Reformat only those lines
(line wrapping / continuation alignment) — no behavior change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:49:17 +02:00
Martin Vogel 345425a1bb merge main into mcp protocol branch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:34:18 +02:00
Martin Vogel d5414111b3 feat(ui): add i18n and index picker UX
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:21:32 +02:00
Martin Vogel 63ed6789df Merge pull request #656 from caioribeiroclw-pixel/fix/openclaw-mcp-config
Fix OpenClaw MCP config path
2026-06-28 21:05:38 +02:00