test(hook): bind the conflict notice with a portable reproduce-first test

The end-to-end shell regression could not run on CI (recorded in the
previous commit), which left the fix without a gating test. Move the
notice DECISION out of main.c - which is not linked into the test
runner, and is why no C test could reach it - into src/cli/
hook_augment.c, which is. main.c now calls the shared API for both the
absent-daemon and build-conflict cases instead of carrying its own
copies of the strings, so the test binds the real production path
rather than a parallel one.

cli_hook_conflict_emits_stdout_notice_issue1388 asserts what the bug
actually broke: a build conflict yields a stdout systemMessage naming a
different build and pointing at 'daemon stop'; the absent-daemon notice
stays distinct and keeps pointing at 'daemon start' (which cannot heal
a conflict); and non-Claude dialects receive no bare stdout JSON, whose
channel it would corrupt.

RED-on-revert verified: restoring the pre-fix behaviour (conflict emits
nothing on stdout) fails this test alone, for the right reason
('conflict is NULL'). cli suite 258 passed, the local end-to-end shell
test still green against a seam-bearing binary, lint-ci clean.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-05 15:56:18 +02:00
parent 0e0e481b67
commit ea466487e8
4 changed files with 71 additions and 12 deletions
+14
View File
@@ -477,6 +477,20 @@ char *cbm_hook_augment_lifecycle_json_for(const char *input, const char *forced_
void cbm_hook_augment_arm_deadline(void);
char *cbm_hook_augment_read_stdin(void);
/* Why a hook client is not augmenting. The hook caller only ever sees stdout,
* so each reason that is actionable by the user must have a stdout notice
* (#1388: a build-conflicted daemon used to report on stderr alone, which is
* invisible in-session and reads as silent skips). */
typedef enum {
CBM_HOOK_ADMISSION_DAEMON_ABSENT = 0, /* no daemon running: `daemon start` heals it */
CBM_HOOK_ADMISSION_BUILD_CONFLICT /* daemon runs another build: needs `daemon stop` */
} cbm_hook_admission_t;
/* The JSON systemMessage a hook client must print on stdout for `reason`, or
* NULL when nothing should be printed. hook_dialect NULL = Claude Code, the
* only dialect that surfaces a stdout systemMessage to the user. */
const char *cbm_hook_admission_notice(cbm_hook_admission_t reason, const char *hook_dialect);
/* Process one already-read hook payload using a caller-owned MCP session.
* Returns a malloc-owned hook output JSON string, or NULL for fail-open/no
* augmentation. This is the daemon entry; it never arms a process-global
+21
View File
@@ -1437,3 +1437,24 @@ int cbm_cmd_hook_augment(int argc, char **argv) {
free(input);
return 0;
}
const char *cbm_hook_admission_notice(cbm_hook_admission_t reason, const char *hook_dialect) {
/* Only Claude Code (the NULL dialect) consumes a bare stdout JSON object;
* emitting it into another dialect's channel would corrupt that protocol. */
if (hook_dialect) {
return NULL;
}
switch (reason) {
case CBM_HOOK_ADMISSION_DAEMON_ABSENT:
return "{\"systemMessage\":\"codebase-memory-mcp: no CBM daemon is running, so graph "
"augmentation is currently skipped. Run `codebase-memory-mcp daemon start` (or "
"open an MCP session) to enable it.\"}";
case CBM_HOOK_ADMISSION_BUILD_CONFLICT:
return "{\"systemMessage\":\"codebase-memory-mcp: graph augmentation is skipped: the "
"active CBM daemon runs a different build than this binary (usually an update was "
"installed while the old daemon kept running). Run `codebase-memory-mcp daemon "
"stop`, then retry - the next command starts a matching daemon.\"}";
default:
return NULL;
}
}