Local runs left most of the machine idle: the serial tail ran sixteen
suites one at a time on otherwise-idle cores, the three platform legs
were launched by hand (usually sequentially), and every container-leg
object write crossed the virtiofs bind mount.
- run-tests-parallel.sh tail scheduling in two phases: the FLEX suites
(timing-shaped but free of the shared per-account daemon runtime
namespace) run CBM_TAIL_JOBS-wide (default 2), then the EXCL group —
daemon-family plus the suites that drive daemon one-shots or
supervisor rendezvous — runs strictly sequentially on a machine
exactly as quiet as the old fully-serial tail gave it. The wave was
already fed longest-first by the shard dealing order, so the drain-out
no longer ends on a heavy straggler.
- ladder.sh: one maintained entry point for the full local push gate
with the legs overlapped — lint, the Linux container suite, and the
Windows VM suite in the background, the macOS suite in the
foreground, one verdict per leg, logs kept per leg. A missing
prerequisite fails its leg loudly instead of silently skipping.
- win.sh test-par now runs through vm-run-tests.sh (--par mode): the
full parallel harness under the CI-shaped protected temp root with
complete output. It previously ran under the MSYS-shared /tmp and
piped through `tail -25` — the same truncated-blindness class that
hid 40 Windows failures from the `test` command.
- docker-compose: build artifacts and the incremental fixture cache
move to named volumes on the container VM's native filesystem. Object
writes over the virtiofs bind mount are the container legs' largest
avoidable I/O cost, and the fixture cache now survives across
container runs.
- test_mem(win): the first valid full-parallel VM run proved working-set
trimming beats the re-touch mitigation (19 MB resident of a 256 MB
double-touch at 18 parallel suites). The RSS probe now VirtualLocks a
64 MB span — locked pages are exempt from trimming, making the
measurement pressure-immune — with bounded touch-and-sample retries
when the lock is unavailable. Red-to-green under the same 18-job load.
- cli: the portable install's staging error now appends the activation
refusal note (predicate, SID, object) like the managed path already
does — a bare "activation transaction I/O failed" on a CI-only
failure is undiagnosable without it.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The first full Windows x64 CI run of this branch (earlier contract-step
walls had always aborted the leg) surfaced 40 failures: the managed
launcher's fail-closed install/update/uninstall gates and the suite's
POSIX-modeled activation tests were both new in this branch and had
never been reconciled, and they hid three real production bugs.
Production fixes:
- ui: config publishes with POSIX rename semantics. MoveFileExW's legacy
replace cannot supersede a destination a reader holds open (the name
lingers until the last handle closes, even with FILE_SHARE_DELETE), so
a config save racing an open reader failed. The temp file is opened
with DELETE access and renamed in-hand (bare-path target, NUL-padded
rename record) before close, falling back to MoveFileExW on any
rename-by-handle failure so non-NTFS config homes keep the previous
behavior.
- subprocess(win): the child's redirected log handle now shares write
access. POSIX places no lock on the redirected log, so a second
producer may append while the child runs; the missing FILE_SHARE_WRITE
mandatory-locked every other writer out, which the index-supervisor
callback-isolation test legitimately exercises.
- cli(win): the portable uninstall path composes the managed binary name
with its .exe suffix; it could never find (or remove) the installed
binary before.
- activation: security-predicate refusals are now diagnosable. The
transaction's predicates refuse without a usable OS last-error, which
collapsed five distinct refusal classes into one blind "status -3,
os 0" at every caller. Each refusing predicate records what refused —
predicate, untrusted SID, and object path — first-wins so the deepest
cause survives, and the staging error line appends it. This is what
identified every Windows refusal in this batch, including the real
install-failure class on drive-root-rooted payload directories
(Authenticated Users holds inherited Modify there; profile-rooted
directories do not).
Windows test dispatch (release binaries unchanged):
- Test builds (CBM_CLI_ENABLE_TEST_API, the existing test-object macro)
compile the portable activation flow on Windows, reachable solely
through the installed activation test seam and announced loudly on
stderr; release binaries compile exclusively the managed transaction,
so the dispatch polarity fails closed. The suite's install/update/
uninstall calls route through a seam-if-absent wrapper, keeping the
shared activation and agent-config semantics unit-covered on Windows;
POSIX arms keep exercising the real activation machinery.
- Two new release-gate tests assert the fail-closed contract with the
seam OFF: a portable payload may not self-update/self-uninstall, and
install refuses to adopt or replace a foreign file at the canonical
launcher path even under --force.
- Test-bug fixes the first valid Windows runs exposed: the version-
cohort crash test reused one conflict record across the probe and the
turnover acquire (whose entry memset erased the probe's detail before
the assertions read it); the activation staged-path assertion now
checks containment rather than a separator (the \\?\ namespace
performs no forward-slash translation); the ui atomic test captures,
cleans up, then asserts so a failure cannot poison later tests; the
PATH-visibility guard is POSIX-only (Windows configures PATH through
the user registry, not a shell rc).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Fixes surfaced while bringing the shared-coordination-daemon branch
green across macOS, the Linux containers, and the real Windows-ARM64 VM,
plus a new stability guard that caught one of them.
- daemon(win): the CLI teardown transition latched a failed release as
permanent, but a Windows participant-state release must briefly
try-hold the shared startup/legacy gates and legitimately collides
with a concurrent one-shot's teardown; the release is retriable by
contract (it always retains the transition), so parallel one-shot
commands no longer report "CLI coordination cleanup failed" despite
succeeding. Found by the new stability guard's churn section.
- cli/main(win): install/update/uninstall and the Augment hook-script
removal derive the managed launcher's identity from its plain
drive-form path (cli_windows_plain_utf8), not the \\?\ extended-length
form, so agent-config ownership matches on Windows and uninstall no
longer leaves the MCP entry or owned hook scripts behind.
- security: the `daemon start --open` browser launch is now shell-free
(ShellExecuteW on Windows, cbm_exec_no_shell elsewhere) instead of
system(); the CORS origin check spells out its two literal loopback
URLs so the static URL audit sees a complete value. Both clear the
Layer-1 allow-list audit.
- coordination: version-cohort lock retries now sleep a per-process
jittered interval — fixed-period retries can phase-lock two
participants so one starves. The activation-quiesce test's
observation window now covers worst-case candidate staging (three
tamper-defense hashes over a ~1 GB sanitizer binary exceed the old
30 s budget on container I/O).
- smoke/soak: the agent-config smoke matches Windows config paths
through their escaped-backslash quoted form; the soak parser skips the
update-available banner before the JSON summary and gained a one-shot
CLI admission-churn phase inside the RSS/FD leak window. The build-dir
safety fixture creates a real symlink/junction (MSYS2 ln -s otherwise
deep-copies) so its traversal-refusal contract is genuinely exercised,
and the symlinked-agent-roots test demotes its planted links to an
unprivileged owner when run as root so the refusal is really tested.
- tests(win): a new daemon-stability guard exercises the parameter
surface, hook fail-open with its rate-limited notice, start-twice and
occupied-port handling, busy-stop refusal against a live MCP session,
kill -9 crash recovery, and sequential/parallel one-shot churn.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Daemon lifecycle and Windows correctness, verified on a real Windows 11
ARM64 VM through the maintained test-infrastructure/vm drivers, plus the
macOS and Linux arm64 suites and the container lint gate.
Daemon lifecycle:
- daemon start/stop/status subcommands. `daemon start` launches a
PERMANENT daemon (spawn shape is byte-exact argv; survives idle
periods and session ends) and reports an already-active daemon
instead of failing. Permanence is honored at every stop latch:
last-committed-client disconnect, host initial-client window,
coordinator release, and application final-session close — a
permanent daemon also keeps admitting new sessions after its last
one closes.
- daemon stop refuses while sessions are active and lists the blocking
peers (pid/role) that must finish first; an idle daemon drains
through the activation-shutdown machinery with the ACK ordered after
connection interrupts. A second stop is idempotent. The wire ops are
no-cohort first-frame requests with peer fingerprint authentication,
so stop/status never conflict with an exact-build admission gate.
- One-shot CLI commands now execute through the daemon (index workers
keep their local supervised path). A cold CLI run that had to spawn a
temporary daemon prints a hint that `daemon start` removes the
per-command startup tax; a warm daemon is recycled silently.
- Hooks are connect-only fail-open: with no daemon present the hook
emits a visible, rate-limited notice (Claude-dialect systemMessage
plus stderr for other dialects) and always exits 0 — augmentation is
never allowed to block the caller's tool use.
- Version skew: a newer-build client automatically drains an
older-build permanent daemon (strict semantic-version triples only;
dev builds never auto-drain) and the build-conflict message names
`cbm daemon stop` as the manual escape hatch.
Windows IPC/runtime (real-VM verified):
- ipc(win): persistent pending overlapped ConnectNamedPipe. The accept
path used to destroy its listening pipe instance on every 20 ms poll
timeout; a client attaching in the teardown window was severed or left
on an orphaned pipe object whose HELLO no server handle could ever
read, absorbing the connect until the client's own timeout expired.
The pending connect now survives poll timeouts and nothing is
destroyed while a client could be attaching.
- ipc(win): drain-before-close for final responses. Closing a named-pipe
server handle can discard a just-sent response before the peer reads
it (POSIX stream sockets never lose buffered data on close). A bounded
cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX,
immediate on interrupted connections) now precedes close in
runtime_worker_finish and runtime_reject_inline, so hello-conflict,
capacity and disconnect acknowledgements reliably reach the peer.
- runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no
transport half-close, so close_begin now announces departure with an
explicit frame (ordered after APPLICATION_CANCEL, before the local
interrupt); the server releases the client's admission on receipt
instead of waiting for the handle to close. Admission-drop timing is
now identical to POSIX shutdown() semantics on every platform.
- runtime(win): client close cancellation. close_begin serializes with
request publication under the send lock, best-effort sends the active
token's APPLICATION_CANCEL frame, then interrupts local I/O; the
server cancels MCP/subprocess work promptly. Contract tests accept
both correct outcomes (interrupted transport or decoded CANCELLED).
- runtime: activation acknowledgement ordering. The activation ACK is
the requester's license to act on "snapshotted and draining", so every
connection interrupt is now initiated before the ACK is sent; a
session could previously get one more request serviced after the
requester observed the ACK.
- service(win): deadline-bounded private-file prepare. The conflict-log
prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms
timer granularity) burned ~1.6 s against permanently obstructed paths,
stalling hello rejections past the client's timeout. The retry budget
is now a 250 ms deadline; transient share collisions still retry.
- subprocess(win): cmd.exe /C payload encoder quotes metacharacters
correctly (root cause of the git-on-Windows failure cluster).
- watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native
Windows stop/unwatch cancellation test with exact-image verification.
- httpd: send_all writes in bounded 64 KiB slices. A single giant
nonblocking send() on Windows is absorbed wholesale into AFD kernel
buffering regardless of SO_SNDBUF, so send deadlines and interrupts
could never engage against a slow peer (and the full payload was
pinned in nonpaged pool). Slicing restores a deterministic
backpressure point; a test hook pins SO_SNDBUF for the deadline and
interrupt tests.
- ui/http: shutdown lifecycle — interrupt checks, response-wide send
deadline, explicit connection states, refusal to free a server while
a listener-owned connection is active.
Windows long-path support:
- Central path-aware wide conversion (canonicalize via GetFullPathNameW
and prepend the extended-length prefix for absolute paths >=240) at
the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite
store opens, and the daemon build-fingerprint/log paths. Deep managed
installs (a 64-hex generation directory routinely exceeds MAX_PATH)
now index, stage and activate correctly.
- activation transaction: its own file APIs and the component-walking
ancestry validators now operate in the extended-length namespace;
the launcher path is canonicalized (and prefixed when deep) once at
entry so every downstream exact-string comparison stays
form-consistent.
- Executable self-resolution uses the wide APIs (GetModuleFileNameW,
GetFileAttributesW) so non-ASCII install paths survive argv[0]
resolution.
Windows launcher install/uninstall transaction:
- FileRenameInfoEx names are NUL-terminated in an over-allocated
buffer. FileNameLength governs per the contract, but filter drivers
read FileName as NUL-terminated and appended adjacent heap bytes to
created names — a flaky, garbage-suffixed rename target. Both the CLI
and the launcher rename helpers are fixed.
- Uninstall retires state via rename-aside (.cbm ->
.cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex
chars so the bare rename target stays under the FileRenameInfoEx
NT-conversion ceiling at guard depths; 64 bits still uniquely
identify the generation.
- When the running launcher's mapped generation backings pin .cbm
against rename, the backings are relocated to activation-<pid>-N
.retired tombstones beside the install (a mapped image may be renamed,
never deleted; the launcher's liveness-guarded sweep reclaims stale
tombstones). Every relocation is recorded, and a FAILED uninstall
reverses the moves after restoring .cbm — via MoveFileExW with
extended-length paths on both arguments, since the deep generation
target is beyond the handle-based rename's bare-path reach — so a
restored install keeps its generation backings and stays runnable.
- After a committed uninstall the retired tree's backings are relocated
out so the tree is shallow enough for the detached cleanup's rd, and
the cleanup's working directory strips the extended-length prefix
(CreateProcessW lpCurrentDirectory silently ignores prefixed paths).
- Files created under Administrators-default-owner directories
(CopyFileW destinations, CREATE_NEW tombstones, probe directories)
are explicitly owner-stamped so the exact-owner validators hold on
runner images; guard fixtures stamp hand-built trees the same way.
Diagnostics, tests and infra:
- diagnostics: discovery is now an always-delivered JSON control record
(new cbm_log_control) that survives CBM_LOG_LEVEL suppression and
paths containing spaces; placement honors $TMPDIR with /tmp fallback
via a diagnostics-local helper; the soak parser reads the JSON record;
documented in docs/CONFIGURATION.md. Red-first coverage for suppressed
log levels, TMPDIR-with-spaces, and native Windows output-contract
assertions.
- tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint
parents with production-shaped ancestry (LocalAppData on Windows, via
th_secure_runtime_parent_new) — the runtime ancestry validation
correctly refuses temp roots whose ancestors grant mutation rights to
Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and
drive the documented startup-owner publication flow before reading
generation-bound endpoint addresses. This turns the 26 Windows
failures previously visible in CI's full-test job green without
weakening any validation.
- tests(win): the launcher guard covers the full permanent-launcher
contract including failed-uninstall restore and immediate reinstall
after uninstall; new daemon lifecycle and reworked hook-augment
guards run the start/recycle/stop flow end to end.
- tests: CBM_SKIP_PERF is now actually consumed by the test runner
(it was set by CI but never read, so perf suites ran everywhere);
four throughput/bench suites are classified as perf, the heavy
store_arch suite moved to the slow-timeout tier, and two
wall-clock-sensitive assertions were rewritten as invariant checks
with coarse hang-detector backstops.
- build/test infra: build-dir safety contract, UI dev-proxy security
contract, soak daemon-recovery contract, path-safety helper, the
Windows VM worktree-sync contract wired into scripts/test.sh, and
vm/win.sh guards building its clean embedded-UI product in an
isolated BUILD_DIR so it cannot clobber the incremental test build.
provision-windows.sh now installs Node.js for the guards UI build.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cli_windows_staging_root builds the per-user install staging root from
LOCALAPPDATA/USERPROFILE, but msys and CI harnesses export those in POSIX
drive form ("/c/Users/..."), which the CRT cannot resolve, and the target's
AppData\Local ancestors may not exist yet on a fake-profile fixture. Rewrite
a leading "/c/" to native "C:/" before use, create the whole ancestor chain
(a single mkdir left the root missing), and fall back to the temp root only
after verifying the directory exists. Also name the failing sub-step when
private staging cannot even create its directory, so the opaque "did not
remain runnable" message is no longer the only signal.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The activation transaction and the release-descriptor probe validate the
full ancestor chain of everything they stage: every path component must
have a trusted owner and grant mutation rights only to trusted identities.
That walk is deliberate TOCTOU defense and stays strict. The staging
directory, however, came from the process temp root, which routinely lives
in a tree that legitimately fails the walk through no fault of ours: under
msys2 TMP resolves inside the msys install tree (C:\msys64\tmp), and on
GitHub runners inside the runner work directory, both of which carry broad
inherited grants such as Authenticated Users modify on an upper component.
The staged pair then failed validation ("launcher descriptor candidate is
unsafe" / the smoke Phase 8 --version probe refusal) even though the staged
files themselves were private.
Add cli_windows_staging_root(): stage install and update transactions under
%LOCALAPPDATA%\codebase-memory-mcp, whose ancestor chain is user-owned end
to end, so the strict walk passes by construction instead of by environment
luck. Falls back to USERPROFILE\AppData\Local and finally the process temp
root when the environment is degraded.
Also make the staging observable where it previously failed blind:
cli_windows_stage_private_file reports which transaction step failed with
the OS error code, and cbm_mkdtemp warns once if the private security
descriptor cannot be applied and it falls back to default directory
security. A blind boolean here cost several blind CI round-trips.
win.sh: scp targets need native C:/ paths on Windows OpenSSH.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Phase 8 staging diagnostic added two long lines that clang-format-20
(the CI formatter) wraps differently. Reformat them so lint passes; no
behavior change.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cli_windows_prepare_install_pair folded launcher staging, payload staging,
the payload fingerprint match, and the staged-launcher --version probe into
one bool, so the caller could only report "launcher/payload source pair did
not remain runnable after private staging" without saying which sub-step
failed. Windows cbm_exec_no_shell does not inherit the child's stderr, so the
launcher's own refusal reason never reaches the log either. Split the checks
so each failing sub-step prints a distinct error, turning the opaque Phase 8
smoke failure into a precise signal on the next run.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Windows managed-install path (cli_windows_managed_install_activate)
printed its plan as "Would install managed launcher -> ..." but never
the words "dry-run", unlike the POSIX path's "(dry-run — no files were
modified)" - so `install --dry-run` did not clearly indicate dry-run
mode (smoke Phase 6a, confirmed by echoing the captured output).
Append the same dry-run indicator to the Windows plan line.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A dry-run mutates nothing (every install mutation is guarded by
!dry_run), but on Windows the activation plan's PATH/agent-config probe
can return non-OK, and the caller then returned early WITHOUT the
dry-run summary - so `install --dry-run` neither indicated dry-run mode
nor exited success (smoke Phase 6a). For a dry-run, emit the
"(dry-run — no files were modified)" indicator and return CLI_OK on
that path (the plan simply noted something), and log the underlying
status to stderr for triage. POSIX is unaffected (its dry-run
activation returns OK, so the branch is never taken).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Extensionless bash shims under %USERPROFILE%\.claude\hooks triggered
the Windows 'How do you want to open this file?' dialog whenever an
editor (Cursor) scanned the hooks dir, and could not execute without
bash anyway. All three Claude hook scripts (discovery gate, session
reminder, subagent reminder) now install as .cmd files with cmd syntax
on Windows - @echo off bodies, caret-escaped pipes in the reminder
text, 2>/dev/null redirects - and the install removes the pre-existing
extensionless twin so upgrades stop triggering the dialog. The
settings.json command paths follow automatically since they are built
from the same script-name macros. POSIX behavior is byte-identical and
guarded (no .cmd twin).
Closes#929
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Aider has no MCP support - it reads CONVENTIONS.md but can only run
shell commands. The installer wrote the shared MCP-tool-centric
instructions there, telling the model to call search_graph(...) etc.,
tools Aider cannot invoke; a reporter reasonably asked how that was
ever supposed to work (#1032).
Aider now gets a dedicated variant: the same discovery priority
expressed as runnable codebase-memory-mcp cli commands (usable via
/run), including first-index and list_projects bootstrapping, with the
no-MCP constraint stated explicitly.
Closes#1032
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_cli_build_args_json typed unknown flags as strings and shipped them
as JSON args the server ignores: 'trace_path --max-depth 1' succeeded
but traced at the default depth 3 — silently-wrong output that inflated
an agent evaluation by thousands of tokens of unwanted hop-2/3 rows.
Unknown flags for a known tool now fail loudly with a Levenshtein
nearest-flag suggestion:
unknown flag --max-depth for this tool (did you mean --depth?) — run
'cli trace_path --help' for the supported flags
Unknown-tool (no schema), raw-JSON, --args-file, stdin, and supervisor
worker argv paths are unaffected — none of them route through the flag
converter.
Reproduce-first: cli_build_args_json_unknown_flag_rejected is RED on
the permissive code (JSON produced) and GREEN with the rejection.
Closes#997
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Four refinements to the coverage signal:
- extraction: subtract DEFINITE recovery before flagging. Tree-sitter
error recovery plus the ERROR-descending def walker often re-extract
constructs inside a failed region (verified: `def broken(:` comes
back as a def); a region whose every line is covered by definitions
that START inside it is not a miss, and a fully recovered file is not
flagged at all. Container defs (Module/Package) don't count as
evidence, and partially covered regions stay flagged — the
#ifdef-split case keeps its flag because the first branch's function
is genuinely lost
- naming: the query_graph option is graph="missed" (the graph shows
ONLY misses — "coverage" was misleading); shadow project renamed to
"<name>::missed"; tool descriptions updated so agents discover both
the option and its semantics
- hook: the CLI-installed PreToolUse augmenter now also matches Read
and injects a coverage note when the file being read is listed as not
fully indexed ("line ranges X-Y could not be parsed — the file
content you are reading is ground truth"). Safe against the old
issue-362 hazard: the augmenter is structurally non-blocking (always
exit 0, additionalContext only), mirroring the Gemini matcher that
already includes read_file; matcher upgrade bookkeeping updated
- ui: "Missed files" toggle in the graph sidebar renders the miss
graph as a second graph option — /api/layout gains graph=missed
(same db file, shadow-project scoping; base project name validated
as before)
Tests: recovery-subtraction cases (recovered def unflagged, garbage
region flagged, trailing recovered defs keep the flag), CLI matcher
tests updated, e2e resilience fixtures switched to an unrecovered
miss; verified end-to-end: hook emits the note for a flagged file and
stays silent otherwise, /api/layout?graph=missed serves the miss graph
and the code graph is unchanged, frontend build + tests green.
Refs #963
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_cli_sha256_file shelled out to sha256sum/shasum, which is unportable:
the tool may be absent, and under cmd.exe the single-quoted path is not
dequoted so the hash of a downloaded release could never be computed on
Windows. Add a small in-process SHA-256 (FIPS 180-4, src/foundation/sha256.c)
and use it directly — no external tool, no per-OS branching, no popen. The
checksum test now covers three NIST vectors (empty, "abc", and a 56-byte
input that forces padding into a second block).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The macOS digest path invoked `shasum -a CBM_SZ_256` — a literal string
(not a macro; string literals do not expand) naming an invalid algorithm, so
the digest always failed on macOS, and the self-update path then treated a
could-not-verify result as non-fatal and installed the download regardless.
Use the correct algorithm, and install only a positively-verified download:
a mismatch, a missing checksum entry, or an unavailable hash tool now all
abort. Adds a known-vector digest regression test.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Add the auto_watch config key (default: true) and route both session
watcher-registration sites in mcp.c (autoindex completion and the
already-indexed connect path) through a register_watcher_if_enabled()
helper. When disabled, the skip is logged as a short structured line
(watcher.register.skipped reason=auto_watch_off).
Default is TRUE (opt-out): existing users keep background auto-sync
unchanged; the knob exists for multi-project users who want each
session contained to explicit indexing.
Distilled from the auto_watch half of PR #625. The PR's bundled
project-index dedup commit is excluded here — it belongs to #754's
own review. Unlike the original PR, no watch registration is added
to the index_repository handler (none exists on main; adding one
would be new behavior beyond the gate), and the default is flipped
from off to on to preserve current behavior.
Tests: default-unset registers the watcher on connect (guards the
no-behavior-change promise); auto_watch=false registers nothing —
verified RED against the ungated production code, GREEN with the gate.
Co-authored-by: Andy11-cpu <canada11@duck.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
On NixOS (and other non-FHS systems) /bin/bash does not exist, so
scripts with an absolute shebang fail to run. Switch the remaining
holdouts to /usr/bin/env bash: eleven scripts/*.sh,
test-infrastructure/run.sh, and the three Claude Code hook scripts
emitted by src/cli/cli.c (gate, session reminder, subagent reminder).
Distilled from PR #674, with parser-test coverage preserved: the
infra_parse_shell* fixtures in tests/test_pipeline.c intentionally keep
#!/bin/bash so absolute-path shebang extraction stays covered, and
tests/repro fixtures are untouched.
Also replace the GitHub-PAT-shaped fixture string flagged in the #674
thread with an obviously fake placeholder (ghp_FAKE...) that still
matches the ghp_ + 36-alnum secret detector.
Co-authored-by: Sandro Jäckel <sandro.jaeckel@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The truncation check `header_end + (int)comp_size > data_len` was
bypassable for comp_size >= 2^31: the int cast turns the size negative,
the sum drops below data_len, and the entry is accepted -- authorizing
inflate to read up to ~4GB past the downloaded archive buffer during
self-update ZIP extraction. Replace it with
`header_end > data_len || comp_size > (uint32_t)(data_len - header_end)`,
which is overflow-safe and strictly tighter.
Regression guard cli_extract_binary_from_zip_rejects_truncated_deflate_size_over_int_max
builds a 52-byte archive whose entry claims comp_size=0xFFFF0000 with a
self-terminating DEFLATE stream: the old check admits it and extraction
returns non-NULL (verified red pre-fix); the new check rejects it.
Secondary cppcheck cleanups from the same review:
- cli.c: extract zip_read_u16le/zip_read_u32le little-endian helpers
(behavior-identical) and widen zip_extract_entry sizes to size_t with
an explicit UINT_MAX guard before the zlib uInt narrowing.
- cypher.c: drop unreachable IS NULL / IS NOT NULL null guards in
eval_condition (resolve_condition_value result is checked earlier).
Distilled from PR #784.
Co-authored-by: SS-42 <noreply@incogni.to>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
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>
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>
Under -D_GNU_SOURCE with optimization, glibc's string.h macros make
strstr() on a `const char *` (with a constant needle) yield a
`const char *`. gcc 15 then rejects assigning that to `char *` with
-Werror=discarded-qualifiers, breaking the build:
src/cli/cli.c:1503: error: initialization discards 'const'
qualifier from pointer target type [-Werror=discarded-qualifiers]
Minimal repro (gcc 15.2):
const char *c = "hello";
char *p = strstr(c, "ell"); // gcc -D_GNU_SOURCE -O2 -Werror=discarded-qualifiers
codex_hook_strip only reads through begin/end/cut (offset math and
byte comparisons, never writes), so making them `const char *` is the
correct fix and preserves the function's existing const contract
(`content` is already const). The malloc'd output buffer stays char *.
No behavior change; build succeeds on gcc 15.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jeremy Maserang <gothuncc@gothaunty.com>
* fix(cli): copy and sign the built binary on install instead of re-signing the stale target
`install` (especially with --force) only ad-hoc-signed whatever already sat at
~/.local/bin/codebase-memory-mcp and never copied the binary the operator ran
install from. An operator who built from source and ran `install -y --force`
was silently left with the OLD binary at the target — running stale code while
believing they had upgraded (#472).
Install now mirrors `update`: detect the running binary, copy it to the
canonical target (skipping the copy when we already ARE the target, which would
otherwise truncate the running file), make it executable, ad-hoc sign the
TARGET, and point all agent configs at the target. A different pre-existing
binary is only replaced with --force or interactive confirmation; --dry-run
reports the intended swap without touching anything.
The copy+sign also resolves the secondary report: a hand-copied clang build is
linker-signed (flags=0x20002) and gets Killed:9 when spawned by an MCP host —
re-signing ad-hoc (flags=0x2) makes it launchable, which install now does for
the placed binary.
Adds cbm_copy_binary_to_target() (with a same-file guard) plus regression tests
covering the binary swap and the self-copy guard.
Closes#472
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(cli): address QA round 1
Extend cli_install_same_file_guard_issue472 with a symlink case: two distinct
path strings resolving to the same inode (which a non-canonical
cbm_detect_self_path vs the hardcoded target can produce) must also be detected
as same-file and skipped, not truncated. POSIX-only (#ifndef _WIN32).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(cli): guard install copy exec-bit assertion to POSIX
cli_install_copies_binary_to_target_issue472 asserted the executable bit on the
copied target, but cbm_copy_binary_to_target only chmods on POSIX (the exec bit
is not meaningful on Windows, and MinGW stat() derives it from the file
extension). Wrap the stat/S_IXUSR assertion in #ifndef _WIN32 so test-windows
passes; the content and creation assertions still run on all platforms.
Refs #472
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The standard linux release binary dynamically links glibc 2.38+ and
GLIBCXX_3.4.32, so it fails to start on Debian 11, Ubuntu 20.04/22.04,
RHEL/Rocky 8/9, Amazon Linux 2, etc. — yet install.sh, the npm and PyPI
wrappers, and the binary's own self-update all fetched it by default,
contradicting the "single static binary" promise.
Point every linux install + self-update path at the fully-static
"-portable" asset (gcc -static), which has no glibc floor. macOS/Windows
are unaffected and unchanged.
- install.sh, pkg/npm/install.js, pkg/pypi _cli.py: select -portable on linux
- src/cli/cli.c: self-update download URL AND checksum archive name both use
-portable on linux (they must match or the update fails checksum verify)
- scripts/smoke-test.sh: assert linux self-update targets the -portable asset
- scripts/ci/check-glibc-compat.sh: new guard — runs the binary inside
debian:bullseye (glibc 2.31) and asserts it starts
- _smoke.yml: run the guard on the portable binary in smoke-linux-portable
Reproduced: standard binary -> "GLIBC_2.38 not found" on glibc 2.31;
portable binary runs cleanly.
Antigravity support targeted the pre-unification layout, so it never actually
configured the CLI:
- detection probed ~/.gemini/antigravity/ — the CLI now installs under
~/.gemini/antigravity-cli/ (brain/, mcp/, settings.json)
- MCP config was written to ~/.gemini/antigravity/mcp_config.json — Antigravity
reads the SHARED ~/.gemini/config/mcp_config.json (mcpServers, command/args)
- the SessionStart hook + AGENTS.md targeted the stale dir
Point detection at ~/.gemini/antigravity-cli/, write the MCP server to the
shared ~/.gemini/config/mcp_config.json (creating ~/.gemini/config if needed),
and place AGENTS.md + the SessionStart reminder under ~/.gemini/antigravity-cli/.
Install, uninstall, install --plan, the detection test, and the README table
updated.
Codex, Gemini CLI, and Antigravity all gained lifecycle-hook support, so give
them the same non-blocking SessionStart reminder Claude Code has — stdout is
injected as session context, nudging the agent to use codebase-memory-mcp
graph tools before grep.
- Codex: append a sentinel-delimited [[hooks.SessionStart]] block to
~/.codex/config.toml (idempotent upsert, preserves other content, removable).
- Gemini CLI: SessionStart hook in ~/.gemini/settings.json, reusing the shared
JSON hook upsert (alongside the existing BeforeTool reminder).
- Antigravity: same JSON hook in ~/.gemini/antigravity/settings.json (it shares
Gemini hook semantics).
The reminder command is written to be valid both as a TOML single-quoted
literal and a JSON string (no single quotes, no newlines). Wired into install,
uninstall, and the install --plan receipt. remove_hooks_json now prunes an
emptied event key so removal leaves no stale "<Event>": [] cruft. README
Multi-Agent table updated. Tests cover Codex upsert/idempotency/removal and
Gemini parity.