search_code matched file_pattern against the file basename only, so any
pattern containing a directory (src/cli/*.c, apps/web/src/**/*.ts)
silently returned 0 results, while search_graph's file_pattern matches
the full repo-relative path.
A file_pattern containing '/' is now matched in C against the
repo-relative path of every indexed file, on all platforms, with
search_graph-equivalent semantics: '*' and '**' match across '/', '**/'
also matches zero directories, '?' matches one character, and a pattern
without wildcards is a substring match. Patterns without '/' keep the
existing basename behaviour. On Windows such patterns are scanned in
process from the already-filtered list instead of the PowerShell -like
post-filter. A search with a '/' pattern that returns nothing now carries
a hint explaining whole-path matching and pointing at path_filter, and
the schema describes both parameters.
Refs #2836
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
A full index saturates every core in the parallel extraction pass, and
interactive CLI/MCP queries (CPU-bound at start-up) were measured taking
twice their wall time behind it with flat CPU time: pure run-queue
contention. The index-worker role now lowers its own priority once,
before any indexing thread is created: nice 10 on POSIX,
BELOW_NORMAL_PRIORITY_CLASS on Windows. It never raises priority: a
process already nicer than 10 (or in the IDLE / BELOW_NORMAL class) is
left alone. Plain nice only, not macOS background QoS, which would pin
indexing to efficiency cores and throttle its I/O. Failure is logged and
ignored.
The test runs the helper in a forked child and checks both legs: a
normal-priority child ends at nice >= 10, and a child started nicer than
that is left where it was.
Refs #2834
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
The SubagentStart hook matches every agent type, but agents defined with an
explicit tool list (e.g. tools: Read, Grep, Glob, Bash) have no MCP tools, and
SubagentStart input carries no tool list to detect that. Append one sentence to
the SubagentStart context only (indexed and unindexed variants) pointing such
agents at `codebase-memory-mcp cli <tool> --help`. Longest variant is ~1.1 KB,
well inside the 2 KB context buffer.
Closes#2837
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Add cbm_daemon_host_wait_for_lifetime_for_test, which runs the production
lifetime loop over caller-held coordination handles, and a runtime test that
ages the claim marker, unlinks the cohort lifetime lock, and asserts the tick
refreshes the marker, logs daemon.lifetime_end reason=coordination_file_lost
file=cohort_lease, and stops the runtime. The seam runs an ephemeral
generation, so a missed loss ends at the initial client window instead of
hanging.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbYKGdU7pqG6AEuMQuJxiF
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
- Classify touch failures explicitly. A file is lost only on proof that
the path no longer names it: ENOENT, st_nlink 0, another inode, a
symlink or other non-regular entry, or a replaced lock directory. Any
other syscall failure (EIO, ESTALE, ENOMEM, EACCES on reopen) is
transient, so a flaky runtime volume no longer stops the daemon. The
POSIX private-lock touch and the ipc lock/marker touches share this
rule; the Windows touch reports a failed handle query as transient.
- The identity marker is classified by path (fstatat, no follow) before
it is reopened, so replacement by a symlink is now loss, not a
transient reopen failure.
- Tests: symlink replacement of the identity marker (lost); an
unreadable marker is transient (-1) and recovers (1) once readable;
the cohort touch test ages both held files and asserts they were
refreshed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
- Separate loss from transient failure. Touch helpers now report OK /
lost / transient (version_cohort: OK/UNSAFE/IO; ipc: 1/0/-1). Only a
proven unlink or replacement stops the daemon. A refresh failure on a
still-valid file (EROFS, EPERM, EMFILE on the marker reopen) logs
daemon.coordination_touch_failed once, retries every tick, and logs
daemon.coordination_touch_recovered when it clears.
- Cohort and claim locks now prove the path still names the held inode
(private_file_revalidate against the stored directory + name), not
just st_nlink, so rename-away, hard-link+unlink and a replaced
directory are caught like unlink.
- The runtime service's self-owned participant guard is refreshed by
the listener heartbeat too.
- Tests: rename-away detection; cohort lease/claim touch (lease with
only lifetime held is OK, each unlinked file reported independently);
IPC listener touch now checks timestamps are actually refreshed and
covers the participant guard and identity-marker replacement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
macOS com.apple.tmp_cleaner deletes /tmp files whose atime, mtime and
ctime are all older than three days. The daemon's long-held lock files
are empty and never written, so under a daemon that lives past three
days the cleaner unlinks them while they are held. The next participant
then creates and locks a fresh inode: forked index workers see the
cohort daemon marker as free, read the live daemon as uncoordinated and
exit 1 on every reindex, and a lost IPC lifetime lock or cohort lifetime
file stops protecting the generation at all. A lost socket identity
marker additionally makes listener close leave the socket pair behind.
The host lifetime loop now runs a heartbeat every 60 s over every file
the generation holds (cohort daemon claim and lease, participant guard,
IPC lifetime reservation, identity marker). Each file is refreshed with
futimens through its held descriptor, only after proving the path still
names that descriptor, so the cleaner never selects it. If any file is
already lost, the daemon logs daemon.lifetime_end
reason=coordination_file_lost and stops cleanly; the next client starts
a coordinated generation. Re-claiming in place would race that client.
Windows needs no refresh (no age-based cleaner; held lock handles deny
deletion), so its entry points only validate the handles.
Closes#2178
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Documentation and claim accuracy only — no behavioural change. Two of the
three findings are corrections to inaccurate statements the round-2 commit
(61ccd905) introduced.
The gating test's framing was overstated, and this time the overstatement was
measured rather than argued. Replacing the backoff call with the pre-change
`ctx->now + interval_ms * US_PER_MS` leaves the watcher suite green at 82/82:
the index_backoff_* tests pass because they exercise the pure function
directly, which the reverted scheduler no longer calls, and the streak tests
pass because the counter is untouched. So no test in this suite fails when
this change's behaviour is removed. The test now records that, with the
figure, instead of claiming it demonstrates the backoff. It is a regression
guard on a deadline being assigned at all — real, but narrower than round 2
said.
Closing that gap needs either a controllable clock or a further accessor
exposing next_poll_ns. Both were judged out of scope: a third exported symbol
purely for testing, on a change already carrying two, is the API widening
earlier rounds flagged. The gap is recorded in the test, the PR description
and the round-3 note rather than papered over.
The memory-visibility comment claimed index_failures follows "the same
discipline every other per-project field in this struct already follows".
That is false: active_git is serialized by projects_lock and registered is an
atomic_bool. Narrowed to the poll-mutated fields, which are now enumerated.
The round-2 decision to document rather than lock still stands — the panel
verified the underlying premise — but it was resting on an overbroad claim.
watcher.h said the delay "doubles up to a fixed ceiling", which skips the
shift cap: the delay plateaus at interval_ms << INDEX_FAIL_SHIFT_MAX, and
that reaches the ceiling only for interval_ms >= 4688 ms. Every interval this
watcher generates is >= POLL_BASE_MS so the ceiling is always reached in
practice, but the function is exported and a caller may pass less.
Refs #2015
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Retracts an incorrect justification from the round-1 commit (4fc5999a) and
closes the coverage it was used to excuse.
MAJOR — the delay wire-up had no coverage, and the reason given for that was
wrong. Round 1 claimed the gating half was "structurally unobservable without
an injectable clock". It is not. cbm_watcher_touch zeroes next_poll_ns, so a
wire-up that never sets a deadline leaves it at zero and the callback fires on
every poll: simply NOT calling touch between polls observes gating
deterministically, no clock involved. Only the delay's MAGNITUDE needs one.
watcher_index_failure_backoff_gates_repolling_issue2015 covers it — one hard
failure, then five polls without touch asserting no re-fork, then a touch
proving the retry still gets through (so the test cannot pass by observing a
dead watcher). Mutation-verified: deleting the wire-up yields
"failing_index_calls == 6, expected 1".
MINOR — round 1's F1 was half-fixed. Of the two single-token mutations that
finding named, only the streak reset was covered; widening the
sustained-failure "==" to ">=" still passed. watcher_sustained_failure_logs_
once_issue2015 drives 14 consecutive failures through a cbm_log_set_sink and
asserts exactly one emission. Mutation-verified: ">=" yields
"sustained_log_hits == 5, expected 1".
All three mutations F1 named are now demonstrated caught rather than argued
about: reset removal (round 1), wire-up deletion, and the "==" widening.
MINOR — cbm_watcher_index_failure_count reads index_failures under
projects_lock while poll_project writes it lock-free from a state snapshot.
That is the discipline every other per-project field here already follows, so
this documents the visibility contract rather than making one field atomic in
isolation: the accessor is a diagnostic and test seam, not a synchronisation
point, and must not carry scheduling decisions without atomic accessors first.
MINOR — watcher.h still described the pre-F3 backoff contract. It now states
the clamp is a floor as well as a cap, why the floor exists for an exported
function whose reachable inputs cannot currently hit it, and that negative
inputs are treated as zero.
Still uncovered, now stated precisely rather than hand-waved: the delay's
MAGNITUDE. Nothing asserts that ten failures produce a five-minute delay
rather than a five-second one. Gating is proven; the numbers are not, and
that does need a controllable clock.
Refs #2015
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
F1 (major) — the failure state machine the backoff feeds had no test
coverage. Deleting the streak reset, or widening the sustained-failure
comparison, left all 7788 tests green: the index_backoff_* tests cover the
delay arithmetic, and nothing covered the counter that selects which delay
applies. A streak that never increments silently degrades the backoff to the
unbounded retry it exists to prevent.
Adds cbm_watcher_index_failure_count(), alongside the existing
cbm_watcher_watch_count() in watcher.h's "Introspection (for testing)"
section, and a test that drives a real git repo through fail/fail/succeed on
the failing_index_callback seam already used by the #937 test, asserting the
streak goes 0 -> 1 -> 2 -> 0. Verified by mutation: removing the reset turns
it red at the reset assertion while every index_backoff_* test stays green.
The delay-GATING half stays uncovered on purpose. Every integration test
calls cbm_watcher_touch first, which zeroes next_poll_ns, so whether the
scheduler honours the computed deadline is unobservable without an
injectable clock the watcher does not have. That harness is a larger change
than this fix.
F3 (minor) — cbm_watcher_index_backoff_ms was non-monotonic for an interval
above the ceiling: zero failures returned the interval, one failure returned
the smaller ceiling. Unreachable today (POLL_MAX_MS < the ceiling), but the
function is exported, so the clamp is now a floor as well as a cap and
backing off can never schedule sooner than the project's own cadence.
F2 (minor) needed no code change: a busy-skip following an unresolved
failure streak keeps the backed-off delay, which is deliberate — only a
success clears the streak, and treating rc > 0 as recovery would let a
project alternating fail/busy never back off at all. The PR description's
over-strong "busy-skip is behaviourally unchanged" claim is corrected there.
Refs #2015
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
A hard index failure (index_fn < 0) left the retry cadence untouched, so a
persistently failing project re-forked an index worker every poll for as
long as the daemon lived. Observed on 0.10.8/macOS: 2233 consecutive failed
workers over 4h43m, ~8/min, every one dying at the same worker-side
coordination seal. Read queries stayed healthy throughout, so nothing
surfaced to the user while no project could be indexed at all.
#937 deliberately leaves the baseline uncommitted on a failed reindex so the
change is retried rather than lost. That guarantee is kept; what changes is
the cadence. Each consecutive hard failure doubles the delay, capped at five
minutes, taking a permanently failing project from ~480 attempts/hour to
~12 while still recovering on its own once the cause clears. Busy-skip
(rc > 0) and success paths are untouched.
The delay arithmetic is a pure exported helper so it is unit-testable
without a clock. watcher.index.err now carries rc and the consecutive-
failure count, and a distinct watcher.index.sustained_failure names a
project whose failures are clearly not transient.
Note: itoa_buf returns a single shared per-thread buffer, so the two-value
log line uses local buffers rather than two itoa_buf calls.
Refs #2015
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
tree-sitter's GLR parser merges ambiguous parse-stack heads recursively in
stack_node_add_link (internal/cbm/vendored/ts_runtime/src/stack.c), once per
nesting level. On pathologically nested input whose grammar is ambiguous
(Perl's paren-optional calls, f(f(f(...)))), that recursion overflows the
native stack during the parse — a small ~1 MB Windows thread stack, and even
an 8 MB POSIX stack at extreme depth — before any extraction runs. Grammars
that are unambiguous here (Java, C++) never trigger the recursive merge.
Cap the recursive merge at CBM_TS_STACK_MERGE_MAX_DEPTH (512) via a
depth-tracked inner worker; the public stack_node_add_link is a thin wrapper
seeding depth 0, so every existing call site is unchanged. Past the cap the
ambiguity is left on the GLR stack instead of merged — a valid parse, never a
wrong one. 512 frames is well within a 1 MB stack while far exceeding any
realistic source nesting.
Add perl_glr_deep_parse_recursion_capped: a crash-isolating regression that
parses deep ambiguous Perl directly (a forked child on POSIX, in-process on
Windows), bypassing cbm_extract_file's CBM_PERL_MAX_PARSE_NESTING pre-parse
guard so the vendored cap itself is exercised rather than the guard.
Narrow vendored-runtime change; no other parsing behavior is affected. The
higher-level Perl pre-parse guard (issue #913 context) stays in place and can
be retired in a follow-up once this cap is proven in the field.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Two O(n^2) patterns in the Perl LSP, the class the ARM
extract_wide_flat_file_is_linear guard flags:
- Every child-iteration loop used indexed ts_node_child(node, i), which is
O(i) per call and O(n^2) over a wide flat node. Route them all through a new
perl_collect_children() helper mirroring wd_collect_children() in
extract_defs.c: a forward TSTreeCursor collect for wide nodes (>= 64
children), NULL (cheap indexed access) for the small common case.
- perl_attach_methods rebuilt each package's whole NULL-terminated method
array on every sub via perl_type_add_method, O(subs^2) on a single-package
file. Replace with a collect -> qsort-by-package -> build-once batch
(O(S log S)); source order within a package is preserved so first-defined
still wins on redefinition.
No behavior change: all perllsp_* resolution tests and
lsp_perl_deep_expression_no_crash pass; check-lsp-originality.sh reports clean.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
A deeply nested Perl call chain f(f(f(...))) overflowed the stack while
parsing on small-stack platforms: tree-sitter's GLR parser recurses once
per nesting level in stack_node_add_link (vendored ts_runtime/src/stack.c)
merging the ambiguous parse-stack heads that Perl's f(...) grammar produces.
The overflow happens during ts_parser_parse, before any LSP walk runs, so
the CBM_LSP_PERL_MAX_WALK_DEPTH guards can never fire. This crashed
test-windows (CLANG64) with an AddressSanitizer stack-overflow and hung
test-unix (ubuntu-24.04-arm) to a 4h timeout on lsp_perl_deep_expression_no_crash.
Add cbm_source_nesting_exceeds(): an O(n) early-exit bracket-depth scan, and
skip extraction (clean has_error, zero edges) for Perl input whose nesting
exceeds CBM_PERL_MAX_PARSE_NESTING (128) before it reaches tree-sitter.
Scoped to Perl because only its ambiguous call grammar drives the GLR
recursion to the nesting depth; C/Java/Python parse the same shape with a
single stack head. 128 is ~10x above any real Perl nesting and well below
the depth where the parser overflows a 1MB stack.
Correct the lsp_perl_deep_expression_no_crash comment to describe the actual
mechanism (parser recursion, not the LSP walk).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Perl gained a full Hybrid LSP tier in this PR but was absent from the Hybrid LSP docs. Add a capabilities row to the "Languages with full Hybrid LSP" table, list Perl in the intro + features language lists, and bump the badge from 9 to 10 languages. Perl was already in the general language-support list and the resolution-quality tier (grammar-level); this covers the new type-resolution tier.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Optional, non-blocking follow-ups from the #461 review:
- perl_eval_function_call_type: split qualified static calls on the LAST
"::" (mirroring perl_resolve_function_call) so return-type chaining on
Foo::Bar::sub() resolves instead of yielding unknown.
- cbm_run_perl_lsp: finalize the type registry into a per-call scratch
arena (as php_lsp/java_lsp do) before resolution, so method/func/symbol
lookups use O(1) hash buckets instead of a per-file linear scan.
- perl_infer_self_type: match the `shift` invocant idiom at word
boundaries and accept `$_[0]`, so `my $self = shifty();` no longer
mis-binds the receiver (tightens the zero-edge guarantee).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Remove .vbw-planning/phases/22-perl-lsp-semantic-resolution/22-03-SUMMARY.md
per maintainer request — internal planning/scratch doc that should not land
in the repo.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
perl_resolve_function_call split a package-qualified call name on the FIRST
"::" via strstr. For multi-level packages (Foo::Bar::sub) this yielded
pkg "Foo" and a sub name still containing "::" ("Bar::sub"), which never
resolved. The call then fell through to the generic bare-name fallback, which
matches on the bare sub name only — collapsing distinct packages' same-named
subs onto a single winner and orphaning the rest. Single-level calls
(Foo::sub) were unaffected, which is why the existing test missed it.
Split on the LAST "::" instead so the full package name reaches
perl_lookup_method, which resolves to the correct fully-qualified sub.
Adds perllsp_static_multilevel_package_call covering the multi-level form.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
F1: dot the Exporter import target so seeded CPAN exported subs resolve.
perl_collect_qw_imports built colon-form targets (Scalar::Util::blessed)
but the stdlib registry keys curated CPAN subs in dotted form
(Scalar.Util.blessed) and lookup is exact-match. Wire in perl_pkg_to_dot
to dot the module portion and drop the now-unnecessary (void) cast.
F2: add a recursion-depth guard (CBM_LSP_PERL_MAX_WALK_DEPTH=512) to both
AST walkers (perl_resolve_calls_in_node, perl_pass1_scan) via a depth-
guarded wrapper + inner split, mirroring java_lsp's JAVA_LSP_MAX_WALK_DEPTH.
Past the cap a subtree is skipped (graceful degradation, no wrong edge),
preventing stack overflow on pathologically nested input.
F3: lock the shared last-"::"-segment normalization in lsp_resolve.h with a
direct regression test over cbm_pipeline_find_lsp_resolution: a qualified
static call still resolves AND the cross-namespace mis-attribution edge case
is bounded by caller-QN equality + the confidence floor.
F4: implement SUPER:: dispatch. Populate enclosing_parent_qn from the
enclosing package's first @ISA parent and resolve $self->SUPER::method() to
that parent's method (strategy perl_method_super). No known parent or
unresolved method emits no edge (zero-edge guarantee preserved).
Tests: perllsp_cpan_exported_function, perllsp_super_dispatch,
perllsp_super_no_parent_no_edge, lsp_perl_deep_expression_no_crash,
lsp_resolve_qualified_static_call_normalizes_colons,
lsp_resolve_misattribution_is_bounded.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Replace the plan-01 placeholder with the full Perl LSP test suite covering the
ten foundational resolution scenarios from 22-RESEARCH.md, plus the extract_perl
/ find_resolved / require_resolved / find_resolved_with_strategy helpers cloned
from test_php_lsp.c:
1. method via bless-assignment 6. use parent MRO
2. constructor class-method type 7. use base MRO
3. static package call 8. Exporter import (use Mod qw(f))
4. $self method dispatch 9. require fallback
5. @ISA inheritance 10. unresolvable receiver -> zero edges
Assertions match the resolver's actual QN scheme (module_qn.subname, dotted —
the Perl package governs dispatch, not the emitted QN), and the negative test
confirms the zero-edge guarantee for untyped scalar and unindexed package
receivers. Register suite_perl_lsp in test_main.c so scripts/test.sh runs it.
All 10 perl_lsp tests pass (3563 passed, 1 pre-existing unrelated failure:
search_code_multi_word in tests/test_mcp.c).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
perl_collect_isa_assignment relied on the assignment's `right` field, but
tree-sitter-perl flattens a parenthesized RHS (`our @ISA = ('Base')`) so the
`right` field points at the `(` token while the parent string literals are
sibling children of the assignment. The single form `@ISA = 'Base'` worked but
the common parenthesized form silently collected zero parents, so @ISA
inheritance never populated embedded_types and method dispatch could not walk
the MRO.
Scan every named child after the `=` instead of only the `right` field, covering
both `@ISA = 'Base'` and `@ISA = ('Base', 'Other')`. use parent / use base were
already handled via a separate path and are unaffected.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
- Add method_call_expression to perl_call_types in lang_specs.c so the
structural tier emits a method-call edge for the LSP bridge to refine
(parity with PHP member_call_expression). callee_name is the bare method
via the field-based extractor's `method` branch.
- Normalize the textual callee in cbm_pipeline_find_lsp_resolution to its
last "::"-separated segment so qualified static Pkg::sub() calls match the
resolved sub's dotted short-name (parity with PHP scoped_call_expression).
- Zero-edge guarantee preserved: untyped receivers still emit no edge.
Resolves DEVN-04 from plan 22-03. Verified on a Base/Derived/main fixture:
run_typed->{greet,describe}, run_static->helper, run_classcall->greet,
describe->greet (inherited), run_untyped->none.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
- cbm.c: include lsp/perl_lsp.h and dispatch CBM_LANG_PERL -> cbm_run_perl_lsp
- lsp_all.c: unity-include perl_lsp.c + generated/perl_stdlib_data.c
- Makefile.cbm: register TEST_PERL_LSP_SRCS and append to ALL_TEST_SRCS
- tests/test_perl_lsp.c: placeholder suite so the Makefile var resolves
(full suite + test_main.c registration land in plan 22-04)
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
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>
Verify the Claude SubagentStart reminder: install writes a SubagentStart
entry with a match-all matcher pointing at cbm-subagent-reminder, a second
upsert is idempotent (no duplicate entry), and removal leaves no SubagentStart
key behind.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
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>
Route Claude Code config paths (skills, .mcp.json, .claude.json,
settings.json, hook scripts) and agent detection through
CLAUDE_CONFIG_DIR-aware helpers, falling back to ~/.claude. Hook command
strings written to settings.json keep the legacy tilde form when the env
var is unset, so existing configs stay portable across HOME values. Prints
a one-line migration nudge when CLAUDE_CONFIG_DIR is set and a legacy
~/.claude tree still exists.
Adds cli_detect_agents_finds_claude_via_env and isolates CLAUDE_CONFIG_DIR
in the existing detection tests so the runner env can't leak in.
Distilled from #321 onto current main (adapts to the v0.7.0 non-blocking
augmenter hook signature, which the original branch predated). Closes#320.
yyjson_mut_obj_add_str stores the raw pointer without copying. Freeing
buf before yy_doc_to_str serializes the document causes yyjson to read
freed heap memory, producing garbage JSON. cbm_jsonrpc_format_response
then fails to parse the result field (res_doc == NULL), so "result" is
omitted from the JSON-RPC response and the MCP client hangs indefinitely.
Fix: hoist adr_buf to function scope (initialized NULL), remove the
premature free, and free it after yy_doc_to_str has serialized the doc.
Adds regression test tool_manage_adr_get_with_existing_adr that FAILS
before this fix and PASSES after it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix handle_get_code_snippet inline store-NULL check to return the same
JSON error format as REQUIRE_STORE and the other inline checks:
{"error":"no project loaded"} instead of a plain string
- Fix smoke_guard.sh query_graph invocation: pass "query" parameter
(not "cypher") to match what handle_query_graph actually reads; the
wrong key caused the handler to early-return before reaching the guard
- Remove extra blank line in handle_get_graph_schema guard block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update stale comment in verify_project_indexed: resolve_store now uses
cbm_store_open_path_query (no SQLITE_OPEN_CREATE), so store is NULL for
missing files; the helper catches the empty-but-present .db case
- Guard state updates in resolve_store behind successful open check: only
set owns_store=true and update current_project when store is non-NULL,
preventing misleading state when an unknown project is queried
- Expand smoke_guard.sh to test all 5 guarded handlers (search_graph,
query_graph, get_graph_schema, trace_call_path, get_code_snippet) instead
of search_graph only; each checks both the guard error and no-ghost-file
invariant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Asserts that query handlers return a guard error for unknown projects
and that no ghost .db file is created in the cache directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cbm_store_open_path_query() that opens with SQLITE_OPEN_READWRITE
only (no SQLITE_OPEN_CREATE); returns NULL when file is absent
- Declare cbm_store_open_path_query() in store.h
- Change resolve_store() in mcp.c to call cbm_store_open_path_query
so querying a nonexistent project never creates a ghost .db file
- Indexing path (cbm_store_open_path) retains SQLITE_OPEN_CREATE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add static helper verify_project_indexed() before handle_get_graph_schema
- Replace inline guard in handle_get_architecture with helper call
- Apply guard to handle_search_graph, handle_get_graph_schema,
handle_trace_call_path, handle_get_code_snippet, handle_query_graph
- All five query handlers now return {"error":"project not indexed —
run index_repository first"} instead of silently returning empty results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The parallel execution path (used for repos above MIN_FILES_FOR_PARALLEL)
was missing the cbm_pipeline_pass_k8s() call. The pass existed only in
the sequential fallback path, so any repo large enough to trigger parallel
indexing produced zero k8s Resource and kustomize Module nodes.
Discovered via live testing against a 954-file gitops repo: the parallel
path was taken, k8s pass was silently skipped, and search_graph returned
0 Resource nodes despite 213 kustomize overlays and hundreds of manifests.
Add the pass after cbm_parallel_resolve() in the parallel branch, matching
the same pattern (fail-open, cancel check, timing log) as the sequential path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check return value of cbm_pipeline_pass_k8s() in incremental path;
log warning on failure (fail-open, matches full pipeline behaviour)
- Add comment documenting pass ordering and the two known structural
limitations: File→Resource DEFINES edges and cross-file kustomize
IMPORTS edges are not emitted in incremental (gbuf only contains
nodes for changed files; File nodes from pass_structure are absent)
- Fix clang-format violation in incremental_kustomize_module_indexed
test (single-line if condition)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cbm_pipeline_pass_k8s() was called in the full pipeline (pipeline.c)
but absent from pipeline_incremental.c. This meant k8s Resource nodes
and kustomize Module nodes were never created or updated during
incremental re-indexing — only after a fresh full index.
Add the pass after the semantic pass, following the same timing-log
pattern as the other incremental passes. Pass changed_files (not the
full file list) so only modified/added YAML files are re-processed.
Add two regression tests:
- incremental_k8s_manifest_indexed: full index + add manifest via
incremental, verifies Resource node appears in the DB
- incremental_kustomize_module_indexed: same for kustomization.yaml
Module node
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ts_node_child(root, i) and ts_node_next_sibling() both call
ts_node_child_with_descendant() internally, making sibling iteration
O(N²) total on roots with many children (e.g. generated TypeScript
.d.ts files or large Perl files with 5,000–50,000 top-level nodes).
Replace with TSTreeCursor: ts_tree_cursor_goto_next_sibling() maintains
traversal state across calls and is O(1) per step, reducing total
complexity from O(N²) to O(N).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add explicit fallback path when fcntl(F_GETFL) fails: skip the FILE*
peek and fall through directly to blocking poll so idle eviction still
fires on timeout (Finding 1)
- Strengthen C unit test: verify id:1 (initialize) and id:2 (tools/list)
response IDs are both present, not just a substring match on "tools"
(Finding 2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use O_NONBLOCK + clearerr() in Phase 2 fgetc probe to preserve the
60s idle eviction timeout when both kernel fd and FILE* buffer are
empty (fgetc on a blocking fd would otherwise block indefinitely,
bypassing Phase 3 poll timeout and preventing cbm_mcp_server_evict_idle)
- Add #include <fcntl.h> for fcntl()/O_NONBLOCK
- Fix comment: "two-phase" → "three-phase" (implementation has 3 phases)
- Improve Python integration test: verify id:1 (initialize) and id:2
(tools/list) response IDs are both present, not just "tools" substring
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CONTRIBUTING.md: fix CBMLanguage enum file reference from
non-existent cbm_language.h to internal/cbm/cbm.h
- CONTRIBUTING.md: rephrase "do not use tree-sitter grammars" to
"do not require a new tree-sitter grammar", clarifying they reuse
the existing YAML grammar
- pass_k8s.c: correct header comment from "per top-level resource
document" to "first document only — multi-document YAML is not
yet supported"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lang_specs.c: add LangSpec entries for CBM_LANG_KUSTOMIZE and CBM_LANG_K8S
(both reuse tree_sitter_yaml()); add cbm_ts_language() switch cases
- extract_k8s.c: new file implementing cbm_extract_k8s(); kustomize path
walks block_sequence items under resources/bases/patches/components/
patchesStrategicMerge and emits CBMImport per scalar; k8s path extracts
apiVersion/kind/metadata.name and emits CBMDefinition with label "Resource"
and name "Kind/metadata-name"; malformed manifests (missing kind or name)
produce zero definitions
- cbm.h: declare cbm_extract_k8s() alongside other sub-extractor entry points
- cbm.c: call cbm_extract_k8s() after unified extraction for the two new langs
- Makefile.cbm: add extract_k8s.c to EXTRACTION_SRCS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CBM_LANG_KUSTOMIZE and CBM_LANG_K8S to CBMLanguage enum (before CBM_LANG_COUNT)
- Add kustomization.yaml/yml to FILENAME_TABLE mapped to CBM_LANG_KUSTOMIZE
- Add Kustomize and Kubernetes entries to LANG_NAMES
- Implement cbm_is_kustomize_file() with to_lower+strcmp pattern
- Implement cbm_is_k8s_manifest() scanning first 4KB for apiVersion: via ci_strstr()
- Declare both helpers in pipeline_internal.h Infrascan helpers section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Defensively free p->userconfig in cbm_pipeline_free in case run() was
never called (e.g. pipeline created but not executed before free)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pipeline.c: inline userconfig cleanup on early-exit paths (discover
failure and cancel check) so userconfig is always freed, fixing the
resource leak when pipeline exits before reaching the cleanup label
- userconfig.c: free partially-built entries array on global config
parse failure to fix alloc-failure memory leak
- language.c: probe user config for compound extensions (e.g.
".blade.php") by scanning from the first dot to the last, falling back
to built-in single-extension lookup — preserves built-in behavior
while correctly resolving user-defined compound extensions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read extra_extensions from ~/.config/codebase-memory-mcp/config.json
(global) and {repo}/.codebase-memory.json (project), merging them with
project config taking precedence. Unknown language values warn and skip.
Missing files are silently ignored (fail-open).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Validate child exit status (WIFEXITED + WEXITSTATUS) after waitpid so
the test fails fast if the child couldn't open the store, rather than
passing vacuously due to the "crashed" row never being written
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap bulk_crash_recovery test and its POSIX includes with #ifndef _WIN32
guards to fix compilation failure on Windows (fork/waitpid unavailable)
- Add negative assertion that the uncommitted "crashed" row is absent
after crash recovery, completing the test's correctness verification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cbm_store_begin_bulk() was switching the SQLite journal mode from WAL to
MEMORY for write throughput. If the process crashed mid-bulk-write the
in-memory rollback journal was lost, leaving the database file in a
partially-written, unrecoverable state.
WAL mode is inherently crash-safe: uncommitted WAL entries are discarded
on the next open. The performance benefit of bulk mode is preserved via
synchronous=OFF and an enlarged cache_size, both of which are safe under
WAL.
Remove the PRAGMA journal_mode = MEMORY from cbm_store_begin_bulk and
the matching PRAGMA journal_mode = WAL from cbm_store_end_bulk. Update
the header comments to reflect the new invariant.
Add tests/test_store_bulk.c with three tests:
- bulk_pragma_wal_invariant: asserts journal_mode remains "wal" after
cbm_store_begin_bulk via an independent read-only connection
- bulk_pragma_end_wal_invariant: asserts journal_mode remains "wal" after
cbm_store_end_bulk
- bulk_crash_recovery: forks a child that enters bulk mode, opens an
explicit transaction, writes data, then calls _exit() without committing;
the parent verifies the database opens cleanly and baseline data survives
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>