From c52ca5d34c76b4f731090ab17a2dc3390dea6467 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 19 Oct 2026 23:52:05 +0200 Subject: [PATCH] feat(hooks): name the graph follow-up tools in every search augmentation When an agent greps for a symbol that the graph knows, the PreToolUse augmentation (Grep, Glob and Bash searches) lists the matching symbols. It now closes with one line that is cbm's own statement, outside the untrusted symbol block, naming the graph tools for the usual next question: [codebase-memory] For these symbols, trace_path lists callers and callees, get_code_snippet returns the exact source, and search_graph finds related symbols by name. The line carries no repository-derived value and is stated as facts, as Claude Code asks of hook context. It is appended at the string's real end into reserved room: the list loop's offset can run past its cap when a row is truncated, so appending at that offset could have written past the buffer. Searches without a hit, errors and non-search events are unchanged. Refs #509 Signed-off-by: Martin Vogel --- src/cli/hook_augment.c | 13 +++++++++++- tests/test_cli.c | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/cli/hook_augment.c b/src/cli/hook_augment.c index 79a197716..ef6b7a3fe 100644 --- a/src/cli/hook_augment.c +++ b/src/cli/hook_augment.c @@ -377,6 +377,13 @@ static char *ha_build_args(const char *project, const char *token) { return out; /* caller frees */ } +/* Closes every search augmentation that found symbols: cbm's own statement, + * outside the untrusted data block, naming the graph tool for the likely next + * question at the moment the agent greps. Facts, not commands. */ +static const char HA_SEARCH_FOLLOW_UP[] = + "\n[codebase-memory] For these symbols, trace_path lists callers and callees, " + "get_code_snippet returns the exact source, and search_graph finds related symbols by name."; + /* Parse the MCP envelope returned by cbm_mcp_handle_tool and, if it is a * successful search_graph result with >=1 hit, format a compact * additionalContext string. Returns malloc'd text or NULL. @@ -432,7 +439,7 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is return NULL; /* valid project, just no matching symbols */ } - char *text = malloc(4096); + char *text = malloc(4096 + sizeof(HA_SEARCH_FOLLOW_UP)); if (!text) { yyjson_doc_free(idoc); yyjson_doc_free(edoc); @@ -476,6 +483,10 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is safe_path, safe_label[0] ? " " : "", safe_label); } } + /* off can run past the list cap when a row is truncated, so append at the + * real end; the allocation reserves room for the whole follow-up line. */ + size_t used = strlen(text); + snprintf(text + used, 4096 + sizeof(HA_SEARCH_FOLLOW_UP) - used, "%s", HA_SEARCH_FOLLOW_UP); yyjson_doc_free(idoc); yyjson_doc_free(edoc); diff --git a/tests/test_cli.c b/tests/test_cli.c index c02091d78..539b0df18 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -13498,6 +13498,50 @@ TEST(cli_hook_augment_context_tracks_search_json_shape) { PASS(); } +/* A search that found symbols closes with cbm's own follow-up line, after the + * untrusted symbol list and on its own line, naming the graph tools for the + * next question; it carries no repository-derived value. */ +TEST(cli_hook_augment_context_names_graph_follow_ups) { + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *st = cbm_mcp_server_store(srv); + const char *proj = "hookproj"; + cbm_mcp_server_set_project(srv, proj); + cbm_store_upsert_project(st, proj, "/tmp/hookproj"); + cbm_node_t n = {.project = proj, + .label = "Function", + .name = "someIndexedSymbol", + .qualified_name = "hookproj.mod.someIndexedSymbol", + .file_path = "mod.py", + .start_line = 1, + .end_line = 4}; + ASSERT_GT(cbm_store_upsert_node(st, &n), 0); + char *envelope = + cbm_mcp_handle_tool(srv, "search_graph", + "{\"project\":\"hookproj\",\"name_pattern\":\".*someIndexedSymbol.*\"," + "\"limit\":5,\"format\":\"json\"}"); + ASSERT_NOT_NULL(envelope); + + bool is_error = true; + char *ctx = + cbm_hook_augment_format_context_for_testing(envelope, "someIndexedSymbol", &is_error); + ASSERT_FALSE(is_error); + ASSERT_NOT_NULL(ctx); + const char *list = strstr(ctx, "\n- hookproj.mod.someIndexedSymbol"); + const char *follow = strstr(ctx, "\n[codebase-memory] For these symbols, trace_path lists " + "callers and callees, get_code_snippet returns the exact " + "source, and search_graph finds related symbols by name."); + ASSERT_NOT_NULL(list); + ASSERT_NOT_NULL(follow); + ASSERT_TRUE(follow > list); + ASSERT_NULL(strstr(follow, "someIndexedSymbol")); + ASSERT_NULL(strstr(follow, "mod.py")); + free(ctx); + free(envelope); + cbm_mcp_server_free(srv); + PASS(); +} + /* Exercise the real PreToolUse + Bash route instead of calling only the * tokenizer seam. This pins the event guard and the graph lookup path together. */ @@ -18479,6 +18523,7 @@ SUITE(cli) { RUN_TEST(cli_codex_preflight_reports_heading_and_reason); #endif RUN_TEST(cli_hook_augment_context_tracks_search_json_shape); + RUN_TEST(cli_hook_augment_context_names_graph_follow_ups); RUN_TEST(cli_hook_augment_bash_pretooluse_reaches_augmenter); RUN_TEST(cli_hook_augment_bash_pattern_extractor); RUN_TEST(cli_hook_augment_coverage_requests_machine_json);