Skip to content

fix(hooks): tell subagents without MCP tools how to reach the graph via the CLI - #5

Open
halindrome wants to merge 57 commits into
mainfrom
fix/subagent-reminder-cli-fallback
Open

halindrome wants to merge 57 commits into
mainfrom
fix/subagent-reminder-cli-fallback

Conversation

@halindrome

Copy link
Copy Markdown
Owner

Summary

Tell subagents that have no MCP tools how to reach the graph through the CLI.

Upstream issue: DeusData#2837

The installed SubagentStart hook matches every agent type ("*"), but many agents are defined with an explicit tool list that has no MCP tools (e.g. tools: Read, Grep, Glob, Bash). The SubagentStart input carries no tool list, so the hook can't detect this. Those agents can still use the graph through codebase-memory-mcp cli <tool>, but the reminder never said so.

Change

  • ha_lifecycle_json_from_root (src/cli/hook_augment.c): for SubagentStart only, append one sentence to both the indexed-project and no-project context variants:

    If the codebase-memory MCP tools are not available to you, run the same tools from the shell: codebase-memory-mcp cli <tool> --help shows each tool's flags.

  • Other lifecycle events (SessionStart, compaction) are unchanged. The same context reaches every dialect (Claude Code, Copilot, Hermes, Kimi, Cline).
  • The longest variant is 1068 bytes, well inside the 2048-byte context buffer.

Acceptance criteria

  1. The SubagentStart context (with and without an indexed project) contains the CLI fallback sentence naming codebase-memory-mcp cli <tool> --help.
  2. The SessionStart and compaction contexts do not contain it.
  3. The context is never truncated: the longest variant stays under the buffer size.
  4. Existing hook tests stay green.

Testing

  • tests/test_cli.c: new assertions on the SubagentStart outputs (indexed and unindexed), plus a negative assertion on SessionStart. Red before the change, green after.
  • The 38 cli_hook* tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CDDUoWEZMNZ9fLvzp2pPGT

Jumaga2015 and others added 30 commits September 1, 2026 21:37
cbm_store_fetch_call_edges, cbm_leiden/cbm_louvain, and
cbm_layout_to_json document caller-owned memory ("caller frees
both" / "the caller frees it" / "Caller must free()") but exported
no matching free function, unlike the rest of the API
(cbm_store_find_nodes -> cbm_store_free_nodes,
cbm_layout_compute -> cbm_layout_free, etc.). From C a bare free()
works via static linking, but a caller across an FFI boundary (e.g.
C# P/Invoke) has no way to reach the DLL's internal free() unless
it is exported by name.

Adds three paired free functions following the existing pattern:
- cbm_store_free_call_edges(src, tgt)
- cbm_leiden_free(result)
- cbm_layout_free_json(json)

Updates the one internal production caller (mcp.c SCC analysis) to
use cbm_store_free_call_edges instead of a bare free(), and the
existing unit tests for cbm_leiden/cbm_louvain/cbm_layout_to_json
to use the new paired frees. Adds fetch_call_edges_basic,
fetch_call_edges_truncated, and fetch_call_edges_empty_project to
test_store_arch.c, covering cbm_store_fetch_call_edges /
cbm_store_free_call_edges, which had no prior test coverage.

Fixes DeusData#1762

Signed-off-by: Jumaga2015 <jumaga2015@gmail.com>
Signed-off-by: lorenzozanee <wyz0707@proton.me>
A record's accessors are implicit, so `record Point(double x, double y,
double z)` declares no `x()` and extraction emitted no node for one. The call
`p.x()` had nothing to resolve to, fell through to the short-name registry,
and bound to the first `x` in the project — typically an unrelated class's
private field. Measured on two indexed Java trees, that was 278 of 2444 and
2649 of 29509 CALLS edges.

Extraction now emits one Method definition per record component, skipping any
accessor the record declares itself (JLS 8.10.3), so the accessor has a node
and the call resolves to it.

And a Java call may no longer bind to a Variable or a Field. A Java call
expression never names data, so such an edge is a spelling collision, not a
call. The guard follows cbm_go_suppress_bare_field_ref — a pure, label-keyed
predicate — and is called from both pass_calls.c and pass_parallel.c so the
sequential and parallel resolvers stay identical. Java-gated: Kotlin
properties hold function values and C has function pointers, so both
legitimately call through data.

Signed-off-by: MopicMP <obshiq123@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…y build

The shared tier-2 registry registered Python defs one at a time without a
mid-build finalize, so every Method's receiver probe scanned all registered
types linearly: O(methods x types), single-threaded in lsp_cross_prepare.
Keep the per-def registration order and back only the existence probe with a
cbm_ht set of type QNs, as the Go and Rust builders do; the registry is
identical by construction.

On the Python 3.14 stdlib + site-packages the builder drops from 67-75 s to
0.20-0.25 s, with a byte-identical graph.

Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
Brings the branch up to main at a222715. Two conflicts, both resolved by
keeping both sides:

- src/ui/layout3d.h: the new cbm_layout_free_json declaration sits
  before main's region-level layout section.
- tests/test_store_arch.c: the Leiden/Louvain tests release their
  results through the new cbm_leiden_free; main's
  leiden_result_memory_core_ownership test is kept.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The three exported release functions added for DeusData#1762 freed raw heap
blocks. Since the memory-core ratchet landed, the allocating side has to
go through the core as well, and on current main cbm_leiden already
allocates its result in CBM_MEM_CLASS_OTHER, so a raw free() in
cbm_leiden_free left the class counters drifting.

- cbm_store_fetch_call_edges: allocate and grow the source/target
  arrays with cbm_alloc/cbm_realloc in CBM_MEM_CLASS_STORE. An
  allocation failure now finalizes the statement, releases both arrays
  and returns CBM_STORE_ERR instead of writing through a NULL array.
- cbm_store_free_call_edges, cbm_leiden_free and cbm_layout_free_json
  release with cbm_free in the class the matching allocation used.
- cbm_layout_to_json writes its output through cbm_json_writer_allocator,
  so the string is an OTHER-class core block; the UI server's five
  release sites now call cbm_layout_free_json.
- The header comments name the paired free instead of "caller frees".
- Tests: ownership checks for all three pairs measure the class counters
  across the allocating call and the paired free, so a raw allocation on
  either side fails; the call-edge check grows past the initial capacity.

Refs DeusData#1762

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings DeusData#2420 (O(1) type membership in the shared Python cross-registry
build, DeusData#2413) up to date with main a222715.

Conflicts in internal/cbm/lsp/py_lsp.c, resolved as follows:

- Includes: main already includes src/foundation/hash_table.h (module
  re-exports) and added lsp_node_iter.h. Main's lines are kept; the
  branch's duplicate include is dropped.

- py_register_lsp_defs: main changed it to return bool (false when the
  package scope cannot be normalized). The merged signature keeps main's
  bool return and adds this branch's PyKnownTypes *known parameter.

- cbm_run_py_lsp_cross_status: main's failure handling is kept and
  passes known = NULL, as the branch did for the per-file path.

- cbm_py_build_cross_registry: main added an early `return NULL` when a
  def fails to register. The merged loop passes &known and frees the
  type-QN set before that early return, so the set does not leak on the
  failure path.

- The test seam cbm_py_lsp_test_register_defs (added on main) passes
  known = NULL.

tests/test_py_lsp.c and tests/test_py_lsp_scale.c merged cleanly.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Replace the wall-clock ratio test pylsp_scale_cross_registry_build_linear
(py_lsp_scale suite) with pylsp_cross_registry_build_work_is_linear in
the py_lsp suite. The new test builds the same generated input (n
classes, each followed by two methods, in extraction order) and reads
the seam-build CBM_LSP_WORK counter instead of a clock, so its verdict
does not depend on host load, and it runs in PR CI, which skips the
perf suites.

4x the classes must cost at most ~4x the counted work. With the
type-QN set the build examines no registry candidates (0 at 1000 and
4000 classes); with the scanning receiver probe the work grows from
2,809,000 to 23,236,000 and the test fails.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings DeusData#2244 (agent detection through the UTF-8 path wrapper) up to
date with main a222715. The branch was based on 45c45be, 1,323
commits behind.

The merge is clean. The merged tree adds exactly this branch's 22
lines to main and removes 6, in src/cli/cli.c and tests/test_cli.c.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Windows regression test for DeusData#2145 created its profile directory
with test_mkdirp and read the result as a boolean. test_mkdirp returns
0 on success, so the test failed as soon as the directory existed and
never reached the detection it was written to check.

Inverting the check is not enough on its own. test_mkdirp uses
cbm_mkdir, which is the narrow _mkdir on Windows. Under an ANSI code
page such as 1252 it does not create the intended name: it creates a
directory whose name is the UTF-8 bytes read as code-page characters.
The old narrow stat finds that mangled directory and the new wide
lookup correctly does not, so the test would pass without the fix and
fail with it. th_mkdir_p goes through the wide API and creates the
real name, the way a non-ASCII Windows profile actually looks.

Since 2b329fe, Claude Code is detected from settings.json,
.claude.json or the claude CLI, through cbm_file_exists, which is
already wide on Windows; a bare .claude directory no longer marks it
as detected. The test now asserts Cursor, which is detected by its
directory alone, so the assertion goes through dir_exists, the
function this branch changes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Make this repository a Claude Code plugin marketplace so users can find
and add the server from inside Claude Code:

  claude plugin marketplace add DeusData/codebase-memory-mcp
  claude plugin install codebase-memory@codebase-memory

This is slice 1 of DeusData#1188: discovery only. The plugin registers the MCP
server and nothing else; the skill, the graph agents and the context
hooks still come from `codebase-memory-mcp install`.

- .claude-plugin/marketplace.json: one plugin, sourced from ./plugin.
- plugin/.claude-plugin/plugin.json: version pinned to the current
  release. Without a pinned version every commit to main would be a new
  plugin version for installed users.
- plugin/.mcp.json: stdio server `npx -y codebase-memory-mcp@0.12.1`.
  The pin keeps main from advertising a server version npm does not
  have; the npm package downloads the release binary on first start and
  verifies it against the release checksums.
- .gitignore: the root .mcp.json stays ignored; plugin/.mcp.json is
  published.
- README: install section, and "use one or the other": with both the
  plugin and `install`, Claude Code runs the server twice under two tool
  namespaces.
- tests/test_version_metadata_contract.sh: both plugin files are
  release-tracking surfaces; a new claim form reads the npm package spec
  (`codebase-memory-mcp@X.Y.Z`), and the unregistered-surface sweep now
  covers plugin/ and .claude-plugin/. The post-release sync that bumps
  server.json must bump these pins too, or this contract goes red.

No language count is stated anywhere in the plugin metadata, so the
language-count contract has nothing new to track.

Refs DeusData#1188

Co-authored-by: Kody <kaidi.shi.1121@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Preserve the current C/C++ field-binding policy while integrating the Java
record accessor change. Handle varargs record components with array return
types and reject data targets before sequential LSP call emission, matching
the parallel path. Add focused regressions for both cases.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Pick up the shared CI contract repair while preserving the approved plugin changes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A record's implicit accessors are members the compiler writes and the
source never spells. Their nodes now carry the property generated=record,
and every registry registration path files them in the exact
(qualified-name) map only, never in the short-name index. The Java
type-aware resolver still binds p.y() on a Point to Point.y; a call such
as v.y() on a type from outside the project no longer binds the accessor
through a unique_name / suffix_match / field_type_hint / fuzzy guess.

- extract_defs.c: each implicit accessor carries extra_props
  generated=record (key CBM_PROP_GENERATED, cbm.h).
- registry.c: cbm_registry_add_qn_only, and the pure predicates
  cbm_def_props_generated / cbm_node_props_generated.
- pass_definitions.c, pass_parallel.c, pipeline_incremental.c: the three
  registration sites route a generated member to the exact map only.
- pipeline_delta.c: the incremental preseed proxy keeps the mark, so an
  incremental re-index registers the member exactly as a full one does
  (the proxy carried only a few properties and the accessor came back
  into the short-name index).

Tests: pipeline_java_record_accessors_generated_qn_only (sequential,
parallel and incremental venues) and two registry unit tests.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The import resolver's symbol-name fallback tries the trailing segments of
an import path that names no project symbol, and a package segment that
merely spells a record accessor bound the import to it. On elasticsearch,
`import javax.crypto.spec.PBEKeySpec` bound a record's spec() accessor,
and `new PBEKeySpec(...)`, annotations and type uses then followed the
import onto it: 6,302 IMPORTS edges into accessors, plus CALLS, USAGE,
DECORATES and THROWS through the import map.

A generated member is registered by qualified name only, and no Java
import can name an instance accessor, so the fallback now skips it exactly
as it skips a Python def-local (DeusData#1548).

The record pipeline fixture gains an import whose package segment spells
an accessor. Without the change it fails in the sequential, parallel and
incremental venues: an IMPORTS edge into Point.y, and make() -> Point.y.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData and others added 19 commits October 27, 2026 19:18
When a JVM LSP callee has no exact node and no Class.method tail match,
cbm_pipeline_lsp_target_node takes the one project callable with that
leaf name. A record accessor became such a callable: on elasticsearch
`Objects.nonNull(x)` (37 CALLS) and `filter(Objects::nonNull)` (230
CALL_REFERENCE) bound a test record's nonNull() accessor.

A generated member is registered by qualified name only. It still joins
through its owner's Class.method tail, but the leaf-name fallback no
longer considers it, either as the unique callable or as a rival that
makes a same-named project callable ambiguous, so every other bind is
decided exactly as before the accessors existed.

The record pipeline fixture gains a record Seen(boolean nonNull) with
`Objects.nonNull(o)` and `filter(Objects::nonNull)` callers. Without the
change it fails in the sequential, parallel and incremental venues.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Windows headers define near and far as empty macros, so the locals named
near and far in the generated-accessor pipeline test did not compile on
the Windows leg. Rename them to near_m and far_m; the node names stay.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Conflict in src/pipeline/pass_calls.c: DeusData#2305 moved the inline LSP-target
emission of resolve_single_call into calls_emit_lsp_target() and added
the unresolved-call evidence around emit_classified_edge. The Java
data-member veto of this branch is ported into that structure:

- calls_emit_lsp_target: the veto runs once the target node is known,
  before the resolution, the evidence and any spawn or edge. It returns
  true (handled), so as before no edge is emitted and no registry
  fallback runs after it.
- resolve_single_call: the veto stays in front of main's
  candidate_qn/calls_emitted bookkeeping, matching the parallel
  resolver, whose veto also precedes that bookkeeping.

pass_parallel.c and the remaining files merged cleanly.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Recognize explicit C-family code in .inc files before extraction while
preserving BitBake recipe bodies and ObjectScript markers. Exercise disk
and byte classifiers, callback/linkage extraction and fast/full indexing.

Signed-off-by: knewstimek <knewstimek@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ia the CLI

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 DeusData#2837

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
@halindrome

Copy link
Copy Markdown
Owner Author

QA Round 1

Round 1 · PR #5 · Reviewer: Claude Opus 5.5 (qa-panel, sequential single-reviewer path: tiny diff) · Contract source: upstream issue DeusData#2837 + PR acceptance criteria

Verdict: clean. The change is correct; no findings.

Contract Verification

Criterion Status Evidence
SubagentStart context (indexed + no-project) contains the CLI fallback sentence naming codebase-memory-mcp cli <tool> --help satisfied src/cli/hook_augment.c 1709–1716: cli_hint set only when event == "SubagentStart", appended via a trailing %s to both snprintf formats (1733–1734 indexed, 1745–1746 no-project). Covered by cli_hook_session_resolves_custom_named_index_by_root_path (indexed) and cli_hook_augment_lifecycle_output_contract (unindexed).
SessionStart and compaction contexts do NOT contain it satisfied Exact match on "SubagentStart"; every other event (SessionStart, PreCompact, PostCompaction, pre_llm_call, TaskStart, UserPromptSubmit) gets "". Tests assert absence on SessionStart (indexed + unindexed). Compaction is untested but takes the same "" branch.
Never truncated: longest variant < 2048 bytes for every dialect satisfied Worst case computed from the format literals: indexed with a 191-byte project name (HA_METADATA_CAP 192) + Tier 3 + hint = 1066 bytes; no-project = 804 bytes. Hint goes into the shared context[2048] before dialect selection, so the bound holds for every wrapper.
Output remains valid JSON for every dialect satisfied Builders serialise via yyjson_mut_obj_add_str (escaping). The hint has no " or \. Hermes, Kimi and Cline never accept SubagentStart (ha_dialect_event_supported, 1133–1161), so they never receive it. Tests parse the output with yyjson_read.
Tests red before / green after; existing tests stay green satisfied New assertions match a backticked string that exists only in the new literal (red before). Test run below.

Schema Change

No schema change (hook context string + tests). Schema gate not configured — did not run.

Security

SAST not applicable: no CI security stage is configured for this target, so no scan ran.

Notes

  • The hint is accurate: run_cli handles --help after the tool name and calls cbm_cli_print_tool_help.
  • All three callers of ha_lifecycle_json_from_root take back an opaque string; there is no downstream impact.
  • Findings were not attributed on this path (sequential); there were none to attribute.

Test run

  • scripts/test.sh --suites "<all 191 suites from build/c/test-runner --list-suites>" on 7f2fd10: 9933 passed, 118 failed, 12 skipped.
    • The failures were all in incremental (one cascade from its first fastapi full index returning NULL) and one grammar_probe_f assertion.
    • The full run was on a shared, heavily loaded machine.
    • Every cli suite test, including the new hook assertions, passed.
  • Re-run in isolation: scripts/test.sh --suites "incremental grammar_probe_f" gives 348 passed, 0 failed on this branch, and the same 348 passed, 0 failed on unmodified upstream/main. The earlier failures were load-induced, not caused by this PR, which changes only a hook context string.
  • The default scripts/test.sh stops at contract Step 0t (tests/test_runtime_isolation_contract.sh, inside scripts/security-install.sh) in this local environment. Unmodified main fails the same way, so it is environmental. CI runs the full default leg.

Navigation: cmm+ctx


Posted by the developer account (QA-agent token unavailable). Review performed by Claude Opus 5.5 (Anthropic) in Claude Code via qa-panel.

…e-agent-detection

fix(cli): support non-ASCII Windows agent paths
…-functions

fix(store,ui): export paired free functions for FFI callers (DeusData#1762)
…n-marketplace

feat(plugin): Claude Code plugin marketplace, discovery slice (DeusData#1188 slice 1)
…r-calls

fix(java): resolve record accessor calls, and never bind a call to data
…gistry-index

perf(py-lsp): O(1) type membership in the shared Python cross-registry build
…ting

fix(discover): select C++ for C-family .inc fragments
Hook context reads best as plain facts: the SubagentStart sentence now says the same tools are also available from the shell instead of instructing the agent. The command the tests match is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants