v0.8.0
Added
-
Java as a supported language.
.javafiles now parse class, interface, enum, record, method, constructor, field, and@interface(annotation type) symbols plus method-call,newexpression, inheritance, import, and annotation-usage references (@Override,@Test,@Component, including@pkg.Fooscoped forms). Multi-declarator fields (String x, y, z;) emit one symbol per declarator. Bumps the supported-language count from eight to nine. -
Python rationale extraction.
find_symbolandexport_contextnow populateSymbol.rationale[]for Python definitions whose attached#comment or leading docstring begins with one of the recognized markers (WHY:,NOTE:,IMPORTANT:,FIXME:,HACK:,XXX:,TODO:). Covers two attachment shapes: contiguous#line comments immediately above adef/class(one-line gap allowed, breaks on the first non-comment node) and the first(string)child of the body when it's a triple-quoted"""..."""/'''...'''docstring. Decorated defs are handled by anchoring the prev-sibling walk at thedecorated_definitionwrapper so# TODO:above@app.route(...)/@property/@dataclass/@lru_cacheetc. still attaches; the walker also climbs from a class body's first method into the class header so comments tree-sitter parks atclass_definitionlevel still attach. Markers inside string literals (e.g.,s = "# TODO not a comment") do not trigger because the extractor reads tree-sittercommentnodes, not raw source. -
Shared MCP daemon for
codesage mcp. The configured MCP entrypoint now starts as a stdio shim and proxies MCP JSON-RPC over a per-user Unix socket to one long-lived CodeSage daemon. Main sessions and subagents therefore share the same project cache plus embedding/reranker model pools instead of eachcodesage mcpprocess loading its own copy. The socket lives under$CODESAGE_DAEMON_RUNTIME_DIR,$XDG_RUNTIME_DIR/codesage, or/tmp/codesage-$UIDwith private permissions; the socket name includes the running binary's version and executable metadata so a freshly rebuilt binary does not attach to an older daemon.codesage mcp --directpreserves the previous in-process stdio server path for debugging, andcodesage daemonruns the foreground daemon explicitly. HTTP/SSE remains intentionally out of scope. -
Daemon management subcommands.
codesage daemon statusprints the running daemon's pid, socket path, reachability, and log path (exit 1 when not running).codesage daemon stopsends SIGTERM and waits up to 10 s for clean shutdown. Both commands operate against the same per-binary daemon key the shim uses, so they apply to the right daemon without manual pid wrangling.
Changed
- MCP daemon hardening (review pass). Concurrency, lifecycle, and operational fixes after the daemon's initial review. Concurrency:
search/export_contextno longer hold the embedder + reranker locks across SQL retrieval. Embedding runs under a tight lock, reranking acquires its own lock only when invoked, so concurrent agents on the same project interleave SQL work instead of serializing. Cold model loads serialize per model key (per-keyMutex<Option<...>>) so two concurrent first-callers can't both construct separate ORT / CUDA sessions. Startup races: daemon startup uses a bounded retry loop with PID-based lock liveness, so a dying spawner no longer strands subsequent shims, and the misleading "another starter still holds" error is replaced with explicit "did not become ready (lock holder alive) — see " or transparent recovery when the holder is genuinely dead.wait_for_socketpolls the spawned daemon's PID; if it exits before binding (model load failure, etc.) the shim fails fast instead of spinning the full 5 s timeout. Security: socket bind tightens the process umask to 0o077 so the socket is born with restricted permissions; on accept, the daemon verifies peer-credentials viaSO_PEERCREDand refuses connections from foreign UIDs (defense in depth for the$CODESAGE_DAEMON_RUNTIME_DIRoverride). Lifecycle: SIGTERM / SIGINT trigger graceful shutdown that removes socket + pid files;serve_clientenforces a 1-hour per-connection ceiling so a deadlocked tool call can't pin an MCP session indefinitely; daemon log rotates at 4 MiB and keeps three generations (.log,.log.1,.log.2). Shim:proxy_stdioselects on first-direction-to-complete instead of waiting for both, so when the daemon closes its write half the shim exits immediately instead of blocking on stdin forever (pre-fix MCP-session hang). Daemon key derivation uses a stable FNV-1a hash instead ofDefaultHasher; on non-Unix,--runtime-diris now rejected with an explicit error. - PHP extension API feature slices. The PHP mapper now scans native extension implementation files for
PHP_METHOD(Class, method)andPHP_FUNCTION(name)definitions and emits onephp-ext-method/php-ext-functionlibrary feature per public API entrypoint. This applies to both repo-root Composer/PIE extensions (config.m4/config.w32+php_<name>.h) and php-src-styleext/<name>/directories. Macro examples inside C/C++ comments and strings are stripped before matching so commented-out methods do not become feature records. API slices keep PHP language semantics so.phpttests undertests/orext/<name>/testsattach through the existing nearby-test logic, while the coarser extension/module slices remain available for whole-extension navigation. - Node.js server route mapping (
express-route,fastify-route,hono-routeseed sources). Ports clawpatch PR #47's deterministic route detection for Express, Fastify, and Hono servers. Two-pass scan per source file: first identify route receivers from local variable declarations matching framework constructors (const app = express(),const router = Router(),const router = express.Router(),const fastify = Fastify(…),const app = new Hono(…)); then match<receiver>.(get|post|put|patch|delete|options|head|all)('/path', handler)calls against those receivers and emit oneroutefeature per match. The walker pre-strips////* */comments and` `template-literal bodies so doc-comment examples and\app.get(…)`templates don't false-match. Constructor scan additionally blanks regular"…"/'…'string contents so"const app = express()"written inside a string doesn't fabricate a receiver. Gated on the rootpackage.jsondeclaringexpress,fastify,hono, or@hono/*in deps/devDeps, so the per-file scan cost stays off non-server JS repos.entry_routeencodes"METHOD path"(matching the existing laravel-route shape) so two methods on the same path don't collapse to a single feature_id. Conservative on purpose: cross-file mount prefixes (Expressapp.use('/api', router), Fastifyregister, Honoroute`) are NOT resolved; emitting the inferred path would mislead more than it'd inform. Non-literal route paths are skipped. Eight regression tests cover Express direct + Router patterns, Fastify, Hono, comment/string/template false-match avoidance, receiver-must-be-framework-constructor, no-server-dep gate, and test-file exclusion. Smoke-tested on a 3-framework fixture: 6 declared routes → 6 features emitted with correct method+path encoding. - Python web route mapping (
flask-route,fastapi-routeseed sources) andsetup.cfgconsole-script support (setup-cfg-script). Ports clawpatch PRs #11 (Flask), #15 (FastAPI), and #28 (setup.cfg). Flask: scans.pyfiles thatimport flaskfor@<receiver>.route('/path', methods=[…])decorators wherereceiveris a local variable initialized fromFlask(…)orBlueprint(…). Defaults toGETwhenmethods=is absent. Emits one route feature per (method, path), somethods=["GET","POST"]produces two seeds withentry_route"GET /users"and"POST /users"rather than collapsing to a single feature_id. FastAPI: same pattern against@<receiver>.METHOD('/path')forapp = FastAPI()androuter = APIRouter(). Both walkers require animport flask/from flask import …(or fastapi equivalent) so a string literal that happens to mention the framework doesn't false-trigger. Known limitations matching clawpatch: blueprinturl_prefixandinclude_router(prefix=…)mount prefixes are NOT expanded; non-literal paths/methods are intentionally skipped. setup.cfg[options.entry_points]console_scripts = name = module:fn(INI-style multi-line value) is now parsed into the same shape aspyproject.toml [project.scripts]. Module path resolves throughresolve_script_module_pathsoentry_pathlands on the real.pyfile (e.g.acme/cli.py) rather than the manifest. Seven new regression tests cover Flask GET-default, methods-kwarg expansion, Blueprint receivers, the no-flask-import gate, FastAPI METHOD/APIRouter recognition, and setup.cfg console-scripts with module resolution. Smoke-tested on a 3-file fixture: 2 Flask + 2 FastAPI + 1 setup.cfg → 5 expected features emitted alongside the python-project seed. - Laravel slice expansion: eight new application-layer surfaces ported from clawpatch PR #5, complementing the existing controller / form-request / Artisan-command coverage. New seed sources:
laravel-project(composer.json + artisan + bootstrap/app.php as a singleServiceanchor withframework:laraveltag, plus phpunit.xml / .env.example / config/app.php / config/database.php / routes/{web,api,console}.php as context_files),laravel-job(per-class underapp/Jobs/**,FeatureKind::Job, taggedjob+async),laravel-service(app/Services/**,FeatureKind::Service),laravel-model(app/Models/**, taggedeloquent+database),laravel-migration(groupedConfigslice overdatabase/migrations/**, owning up to 50 files because migrations rarely make sense one-at-a-time),laravel-seeder(same grouped shape fordatabase/seeders/**),laravel-test-suite(one TestSuite per existingtests/{Unit,Feature,Integration,Browser}directory, owning up to 50 files each,test_command = "composer test"), andcomposer-script(composer.jsonscriptsentries restricted to an allowlist ofsetup,dev,test,typecheck,lint,format,analyse,analyze,deploy*to keeplist_featuresfrom filling with one-off pipeline scripts; thetestscript lands asFeatureKind::TestSuitewith the composer-test command attached). Smoke-tested on a real 1670-file Laravel application: 326 newly-created features atop the existing 182 (208 services, 94 models, 19 jobs, 2 composer scripts, plus one each of project / migration / seeder). Six new regression tests cover composer-script allowlisting, the project seed shape, per-class job/service/model emission, migration grouping, per-directory test suites, and the non-Laravel-project gate. - JS monorepo Next.js routes + nested-frontend discovery + Turbo
test_command(three related improvements to the JavaScript / TypeScript mapper ported from clawpatch PRs #18 (monorepo Next), #4 (nested frontend roots), and #37 (Turbo task graph)). (1) Per-package Next.js routes.next_app_routes/next_pages_routespreviously only scanned the repo root'sapp/andpages/, so a monorepo withapps/web/app/...orapps/marketing/pages/...emitted zero Next route features. Both now run relative to each discovered workspace package root and emit per-route seeds with theworkspacetag and a summary that names the owning package. (2) Nested frontend root discovery. When the repo root has noapp/orpages/AND no workspace package covers them, the mapper additionally probes the conventional dirsfrontend/,client/,web/,ui/for a nested Next/Vite app one level down. Lets a Rust-or-Go-backed repo with a React frontend infrontend/get its routes mapped without declaring an npm workspace. (3) Turbo-awaretest_commandrewrite. Whenturbo.jsonexists at the repo root AND declares atesttask (in either Turbo 1.xpipelineor 2.xtasksshape), every workspace package'stest_commandflips from the bare<pm> --dir <pkg> testshape toturbo run test --filter=<package-name>, so agents get dependency-aware orchestration with cached output reuse instead of running the bare test runner. Repo-root seeds keep their plain command (root-level tests don't filter to a package). Four new regression tests cover per-package Next route emission, nested-frontend discovery when the root has noapp/, the Turbo rewrite when atesttask is declared, and the negative case whereturbo.jsonexists but only declaresbuild. Smoke-tested on a synthetic monorepo: 3 packages + 3 routes (2 fromapps/webapp-router, 1 fromapps/marketingpages-router) all mapped, all three packages gotturbo run test --filter=@m/<pkg>.
Fixed
- C/C++ CMake mapper hardening. Eleven gaps surfaced by porting clawpatch PR #26's test fixtures: (1) uppercase keywords (
ADD_EXECUTABLE,ADD_LIBRARY) now match; the regex is case-insensitive. (2) Numeric-prefix target names (7zip) and dotted names (foo.bar) are accepted; the name regex was previously anchored to[A-Za-z_]start with no.allowed. (3) Late-boundtarget_sources(name PRIVATE|PUBLIC|INTERFACE src…)declarations are now parsed and merged into the matchingadd_executable/add_librarytarget; previously a target declared withadd_executable(name)and sources attached separately became an empty seed. (4) CMake bracket comments (#[[ … ]]and#[=[ … ]=]with any equals count) are stripped before regex matching, so commented-out targets no longer leak through as real features. (5) Sources containing variable substitutions (${APP_SOURCES}) cause the target to be skipped rather than emitted with a phantom owned-file entry. (6) Absolute-path sources (/src/main.cpp) are similarly skipped. (7) Header-only executables (sources all.h/.hpp/.hh/.hxx) are dropped, since these can't link as binaries. (8) Targets whoseowned_filesis empty after exclude filtering (e.g., a vendored INTERFACE library whose only file lives undervendor/) are dropped rather than emitted as phantom seeds. (9)INTERFACElibraries with only header sources still surface so reviewers can see the API. (10)**/CMakeFiles/**added toHARD_EXCLUDE_PATTERNS(joining the existing**/cmake-build-*/**) so the CMake build-tree state directory stays out of every index. (11) Thec-mainwalker now skips files undertests/,test/,__tests__/,Tests/or with_test/Teststem suffixes, so googletest/catch2/custom harnessmain()files no longer surface as CLI features when the project hasn't configured[index].exclude_patterns. Smoke-tested on libharu (3 features incl. onecmake-binfrom a literal target name) and lexbor (81 features, 0 test-path c-main leakage, 0 panics on variable-heavy CMake). - Laravel route parser hardening: four longstanding gaps that left real routes mis-attributed or under-prefixed, ported from clawpatch PR #5's route extractor. (1)
prefix()fluent chain expansion.Route::prefix('admin')->get('/users', …)now resolves to/admin/users; nested chains likeRoute::prefix('a')->prefix('b')->get('/c', …)resolve to/a/b/c. (2)routes/api.phpimplicit/apiprefix. Laravel's default route service provider auto-prefixes API routes; the mapper now applies the same default so the recorded URIs match what the framework actually serves. (3)use-import resolution.Route::get('/x', [UserController::class, 'show'])paired withuse App\Http\Controllers\UserController;(or anas Aliasrename) now bridges the route back to the controller file via the fully-qualified class name, instead of failing to match because the route stored only the short class name. (4)Route::controller(X::class)->group(fn () => { … })body expansion. Inner routes inside a controller-group closure now surface as individual route features attributed to the outer controller, with any outer/innerprefix()chains applied. A consumed-spans tracker prevents the same registration from emitting twice (once from the group pass, once from the top-level scan). Internal:parse_laravel_routescollapsed from two regexes-plus-by-key-map to one route regex + one controller-group regex; new helpersparse_php_use_imports,resolve_imported_class,fluent_route_prefixes,file_default_route_prefixes,route_uri_with_prefixes. Six new regression tests cover each pattern, plus alias-form imports. Smoke-tested on a real 1670-file Laravel application: 86 routes now carry the previously-missing/api/prefix, 89 routes got new feature_ids (URI changed because prefix expansion or controller-group bridging filled in). - Non-ASCII route paths preserved through JS strip pass. The
strip_js_implhelper that pre-strips comments / strings / template literals before the Express/Fastify/Hono route regex iterated bytes and emittedb as char, mangling every non-ASCII UTF-8 byte into its Latin-1 codepoint (éC3 A9→éC3 83 C2 A9). Any internationalized URL (/café,/привет,/ürün) was stored corrupted inentry_route, and because that field feeds thefeature_idhash,find_feature("/café")from an agent never matched. Rewritten to walk bycharinstead of bytes, so non-ASCII runs round-trip verbatim. Two new regression tests cover Latin and Cyrillic route paths. - Parser worker no longer panics on 1-byte string captures.
extract_referencespreviously didto_name[1..to_name.len() - 1]with no length guard, panicking withslice index starts at 1 but ends at 0whenever tree-sitter emitted a(string)node containing just one quote character (possible on malformed/truncated C/C++/JS/TS source). The panic propagated out of the rayon-parallel worker and aborted the whole index run. Extracted intostrip_surrounding_quoteswithlen >= 2guard plus six unit tests covering balanced quotes, single bare quotes, empty input, and mismatched quotes. - Workspace-only React/server deps now trigger route mapping. The React Router and Express / Fastify / Hono route walkers gated only on the root
package.json's deps. A monorepo declaringreactinapps/web/package.json(root has no React dep) missed every React Router slice; same for Express inapps/api/package.json. Newany_package_has_dephelper ORs the root package with every discovered workspace package.react_router_routesalso now scanssrc/+app/relative to each discovered package root, so the regex actually has files to walk in monorepos. Two new regression tests cover workspace-only React and Express deps. - React
<Route>regex now matches any prop order, including nested JSX. Previous regex requiredpath=beforeelement=AND broke onelement={<UsersPage />}because the nested/>ended the[^>]*capture early. Replaced withextract_route_tag_bodies, a{}-depth-aware byte scanner that captures the full attribute body, followed by independentpath=/element=regex passes. Matches<Route element={<X />} path="/users" />(element-first) and<Route path="/users" element={<X />} />symmetrically. list_featurestag filter now matches substrings as documented. The SQL bound%"{tag}"%(literal quote anchors around the tag), sotag="framework"missed compound tags likeframework:react-routerdespite the docstring promising "tag substring" semantics. Changed to%{tag}%. Newcrates/storage/tests/features_test.rsintegration test covers substring matching across compound tags, full-tag exact match, and the non-matching-substring negative case.- Post-commit / post-merge / post-checkout / post-rewrite git hooks no longer race their two passes. The generated hook body launched
codesage indexandcodesage git-index --incrementalin parallel with&. Both take the same project lock and exit 0 on contention, and the hook redirects stdout/stderr to/dev/null, so whichever lost the race silently skipped after every commit with no visibility. Now sequenced inside one background subshell:( cd "$root" && bin index && bin git-index --incremental ) >/dev/null 2>&1 &. Existing installations re-runcodesage install-hooksto pick up the new body. Hook-body generation extracted intogenerate_post_commit_hook_bodywith two regression tests asserting&&chaining and a single top-level&. bench/concurrency-audit.pybackups now snapshot the full database.backup_dbcopiedindex.dbwithout checkpointing;restore_dbdeleted the live-wal/-shmsiblings and overwroteindex.dbwith the un-checkpointed snapshot, silently dropping any committed-but-not-yet-checkpointed transactions. Common after a freshcodesage indexbecause WAL checkpoints are lazy. Now runsPRAGMA wal_checkpoint(TRUNCATE)beforeshutil.copy2, so the.dbsnapshot is a self-contained image of the database.bench/concurrency-audit.pyaborts when the checkpoint is contested.PRAGMA wal_checkpoint(TRUNCATE)returns(busy, log, checkpointed); abusy=1row means WAL frames remain on disk and the.dbsnapshot is stale. The earlier fix issued the pragma without reading the result, so when the per-user codesage daemon held a read transaction on the project'sindex.db(the common developer setup) the audit silently regressed to the pre-fix data-loss state. The script now reads the row and exits with a "stop the daemon and re-run" message when busy ≠ 0.bench/agent-tool-selection-harness.py--append-system-prompt-fileworks. The harness was passing--append-system-prompt-file <path>toclaude -p, which is not a real flag.claudeaccepts only--append-system-prompt <prompt>. Every task in a run using the option silently exited with rc!=0 and an empty tool-call list, invalidating the §2.3 prompt-steering measurement. The flag now reads the file and inlines its contents; subprocess stderr is also echoed whenclaudeexits non-zero so a future flag rename doesn't nuke another run unobserved.bench/extract-eval-cases.pyno longer leaks sibling-directory paths into the corpus.normalize_pathdid a rawstartswith(project_root)followed by a length-based slice, so files under sibling directories whose name extends the project root's path (<root>-test/,<root>_ref/...) were accepted past the prefix check and emitted with malformed relative paths starting in-or_. Rewritten withPath.resolve()+is_relative_toso the comparison is on path segments, not character offsets.bench/generate-llm-corpus.pysurvives a hungcodex execcall. A single 180ssubprocess.TimeoutExpiredused to propagate out of the per-file loop and discard every successful codex response collected so far. Now caught with the same "log and skip" treatment asrc!=0, so a flaky case costs one wasted invocation instead of the whole run. Missingcodexbinary surfaces an explicit error instead of an opaqueFileNotFoundError.- Reranker score extraction respects the output tensor's shape.
score_batchwas reading the firstbatch_sizeentries of the flat logits tensor, which is correct only whennum_labels == 1(thems-marco-MiniLM-L6-v2default). For any reranker withnum_labels > 1(binary-classifier or NLI-style cross-encoders configured via[embedding].rerankerin.codesage/config.toml) every score after the first came from the wrong row, silently scrambling the rerank order. Now extracts the positive-relevance column at the correct stride and softmax-normalizes for multi-class heads; one-timetracing::info!of the detected shape lands in the daemon log. Three new unit tests cover the single-label, binary-classifier, and three-class shapes. - CMake comment stripper preserves UTF-8 paths.
strip_cmake_commentswalked the source byte-by-byte and emittedbyte as char, mangling every multibyte sequence into its Latin-1 codepoint (soadd_executable(app src/café.c)produced a feature whoseentry_pathcould not be matched back to the real file). Now copies non-comment runs as full Unicode scalars; the bracket-close marker scan still uses byte offsets because],=,#,[are pure ASCII and can never collide with a UTF-8 continuation byte. Same bug class as the JS strip-pass fix that landed in this Unreleased section. - Laravel route parser ignores commented-out
Route::*calls.parse_laravel_routesran its route regexes directly againstroutes/{web,api,console,channels}.php's raw text, so// Route::get(...),# Route::post(...), and/* Route::delete(...) */all matched and emitted phantom route features. Routes are now passed throughstrip_php_comments_preserving_stringsfirst, a new helper that blanks///#//* */comments while keeping string literals (route paths are string literals) and byte offsets intact so the cross-passconsumed_spansdedupe still works. impact_analysison.javatargets resolves as a file.looks_like_file_targetwas missed when Java was added to the supported-language set, so a bare target likeUserService.javagot classified as a symbol, hit symbol lookup, found nothing, and returned an empty result silently..javais now in the file-extension allow-list.FileCategory::classifyno longer flagsLatest.java/Manifests.java/Latest.phpas tests. The Java and PHPUnit arms lowercased the path and then matchedtest.java,tests.java, andtest.phpwithout a separator, so any source file whose name happens to end in those substrings got dropped fromimpact_analysiswithsource_only=true. Tightened to require the standard uppercase-Tconvention (*Test.java,*Tests.java,*Test.php) on the original path.codesage daemonstartup test no longer races. The integration test's wait loop incrates/cli/tests/mcp_daemon.rsexited as soon as the.sockfile appeared, but the daemon writes the.pidfile a few statements later. Aread_dirscan landing in that window leftpid_file = Noneandexpect(...)panicked spuriously on busy CI runners. The loop now waits for both files.codesage mcpstdio shim no longer preloads the CUDA/cuDNN stack.main()unconditionally raninit_for_main()before clap parsed argv, so every invocation (including the per-agent stdio shim)dlopen'dlibonnxruntime.soplus the full NVIDIA dylib set (libcublasLt,libcudnn_*,libcufft,libnvrtc,libcurand, etc.) before falling through to atokio::io::copyloop between stdin and the daemon's Unix socket. Per-shim RSS measured at ~238 MB / ~4.3 GB virtual on the previous build despite the shim never constructing an Embedder or Reranker. The constraint behind the eager call (std::env::set_varis unsafe under Rust 2024 and must run before any thread spawns) only applies when an env var actually needs to be set, which the shim never does.main()now skipsinit_for_main()when argv resolves tomcpwithout--direct; all other commands (includingcodesage daemon,codesage mcp --direct, and every model-loading subcommand) continue to preload eagerly. Measured shim RSS after the change: 9.2 MB (26× drop), with zero CUDA/cuDNN libraries mapped into the process. Five regression tests lock in the argv detector's positive and negative cases.