Re-index and refresh required (12.2.2). RESOLVER_GENERATION moves from 11 to 12 and the parsed cache from 7 to 8 (Go, Python, Kotlin/Java import resolution; method calls on call or new receivers; new Foo() edges; see the generation 12 table above). The filemeta and resolution-deps sidecars move from 4 to 5, so the first index after upgrading rebuilds in full. Vault link fixes (frontmatter [[wikilink]] relationships, [[Note#^id]] block references) apply only when a vault is re-derived, and the Markdown derivation version did not change, so that does not happen by itself | Run nestweaver index --repo <path> --force once per repo and nestweaver brain refresh <vault> once per vault; until the re-index stale-check exits 2, dead-code refuses and blast-radius misses the new edges |
affected-tests diffs one repository. With --base-ref it is the indexed repo checked out at the working directory's git root; with --files it is the only indexed repo, and when several are indexed the new --repo <name or UID> is required (exit 64 otherwise). Repo-relative paths resolve only in that repo, and --json names it. MCP affected_tests takes repo the same way and refuses files/changed_files together with base_ref (it used to ignore base_ref) | Pass --repo (MCP repo) when more than one repo is indexed; send either changed files or a base ref, not both |
daemon status exits 69 when the daemon is unhealthy (running but unreachable, or an owner it cannot verify). setup attempts every tool and exits non-zero if any failed. remove-repo with an ambiguous name exits 3. An empty query argument exits 64, and so do pre-push-impact with a bad --format or --min-severity, list-features without --config and interactions show with no arguments. memory consolidate --apply with nothing to apply exits 0. A database error under --json prints a JSON envelope | Treat 69 as "restart the daemon" (nestweaver daemon --db <db> restart), 64 as a usage error |
An explicit read-symbols --root (MCP root) must be inside an indexed repository or vault, or a linked worktree of one. A body read never follows a symlink out of the repository, and the web /source route walks from the repo root without following symlinks | Omit --root, or point it at the indexed checkout |
Git runs isolated for every repository NestWeaver reads, including pre-push-impact --local-changes, the code watcher and blast-radius. The repository's core.fsmonitor, hooks, clean/smudge/process filters, textconv and external-diff drivers, core.askPass and signature verification never run; a repository whose driver configuration cannot be listed is refused. Working-tree status/diff calls pass --ignore-submodules=dirty, so a submodule's own filters never run either (a moved gitlink still counts as changed; submodule contents were never indexed). A file a repo-local filter (for example Git LFS installed with --local) would have normalised may now show as modified in change detection | Nothing to change for normal repositories; expect LFS-tracked files to appear modified where a filter used to hide the difference |
Git metadata a repository borrows from another (a forged .git/commondir, a gitfile into another repo) is refused, and such a layout never takes the other repo's identity. /proc, /sys, /dev (at any depth) and other system roots are refused as repo or vault roots, and an instance id containing /, \ or .. is rejected | Index the real checkout, not a path through a pseudo-filesystem |
An unreadable source in a language that cannot carry API contracts is skipped and disclosed (read_error row, coverage_status: "degraded") instead of failing the index or watcher batch. An unreadable JS/TS/Java/Kotlin source still fails closed, because skipping a controller would publish an incomplete contract set. A file that vanishes mid-scan is a deletion. A malformed OpenAPI spec that never published contracts is skipped and disclosed instead of failing every index | Gate on coverage_status / --fail-on-skip; fix the named file's permissions |
Edits to .gitignore, .ignore and .git/info/exclude are reconciled: newly hidden sources leave the graph and newly visible ones are indexed, at watcher time and at startup. Global-excludes patterns with a slash are anchored at the repo root. A watcher that cannot publish, an interrupted vault refresh and an interrupted embedding pass now show in brain status until they finish | Nothing to change |
An incremental index that was killed part-way leaves a per-repo "publication incomplete" record: stale-check reports the repo incomplete and the next index rebuilds it in full. A full index whose bulk-load checkpoint fails now fails instead of reporting success. A daemon whose database file was deleted or replaced refuses writes | Re-run nestweaver index --repo <path> for a repo stale-check reports incomplete |
Client-supplied lists and values are capped and stated in each MCP tool's inputSchema: context seeds at most 256 (duplicates collapsed), kinds 32, repo/tag filters 100 (1000 for hub_nodes/bridge_nodes), file lists 1000, count_patterns.patterns 64. set_extension: uid 1 KiB (not blank, no invisible format or bidi characters such as U+202E), key 256 bytes, value 64 KiB of JSON nested at most 64 deep, no control characters, the sidecar written as compact JSON, last_indexed_at reserved, node and property counts capped. brain_add_source names: 1-255 characters with no control characters (CLI and store too; an already-stored bad name is listed escaped and can be removed by path or UID). MCP stdio frames are capped at 64 MiB | Split oversized requests; read the limits from tools/list |
brain_remove_source refuses a target that matches sources of different kinds (a repo and a vault sharing a name) or several sources, with candidates; repo URLs match exactly (no suffix match). It also accepts the derived repo name the other tools use. prune_stale removes a source only when its root is not found; an unreadable root is kept with a warning | Retry with a UID from candidates |
Metadata writes (set_extension, unset_extension, interactions forget) wait at most NESTWEAVER_METADATA_WRITE_WAIT_SECS (default 15) for the write lock, then answer write-busy; a cancelled request is never applied. MCP count_patterns has one max_millis budget for the whole call (default 10 s), shared by all patterns, and marks a pattern it cut truncated: true with lower-bound counts. The CLI's count-patterns takes the same budget as --max-millis (1-600000) and, when the budget cuts any pattern, prints a stderr warning and exits 75 (EX_TEMPFAIL) instead of 0 | Retry a write-busy answer; raise max_millis (--max-millis) for a large pattern set, and treat exit 75 as lower-bound counts |
brain_context and brain_search rank an exact-case symbol match first and apply repo filters before the result cap. regex_search without a trigram index finds what count_patterns finds. blast_radius gives the same risk on identical calls. Concise MCP rows keep uid and location, so they are larger | Nothing to change; budget for slightly larger concise responses |
New response fields: missing_sections (note_get), did_you_mean_by_target (read_symbols), changed_symbol_count, proposals_already_applied (brain_memory_consolidate, whose limit is now applied before any proposal moves), refresh_vault_paths, truncated on pattern counts, and publication_in_progress. note get, backlinks and MCP note_get resolve a path-qualified title without .md; [[Note#^id]] block references resolve | Read the new fields where useful; they are additive |
contracts diff reports parameter and response-status changes, and --fail-on-breaking fails on them | Review the newly reported changes before gating |
The CLI sends the daemon absolute --config, root and backup save paths, and the daemon refuses relative ones. watch-stop no longer starts a daemon. Tracing respects --no-color, NO_COLOR and non-TTY stderr, and an invalid numeric NESTWEAVER_* value warns once instead of being silently ignored. Terminal control sequences from indexed content are escaped when stdout is a terminal. Secret env var values no longer appear in --help | Nothing to change |
backup save --force refuses a destination that is the live database or one of its files (also through a symlink or hard link), and backup restore --force replaces only a NestWeaver data directory. config validate checks that fusion weights sum to 1.0, rejects duplicate repos and caps the file size | Pick a backup path outside the database's files |
setup was checked against each tool's documented config location: VS Code and Copilot get .vscode/mcp.json under servers; Continue gets .continue/mcpServers/nestweaver.json; Kiro .kiro/settings/mcp.json; Cline gets a printed snippet; Aider's invalid repo-map: line is gone. Hook commands quote an absolute --db. A changed --db is reported and --force rebinds it. setup refuses to write through a symlinked directory outside the project. The Claude Code hooks (shipped and installed) now read tool_input and return hookSpecificOutput, so they actually fire; re-running setup claude-code repairs an older install | Re-run nestweaver setup after upgrading |
hooks --install takes --db (written into the hook as an absolute path) and never writes through a symlink. Run from a subdirectory, it binds the repository root's database. It mentions core.hooksPath only when that is set. --uninstall restores the newest backup only when the installed hook is NestWeaver's; otherwise it leaves the hooks alone | Pass --db when the database is not ./nestweaver.lbug |
The code watcher now resolves each batch with the full index's resolver over the whole-repository view, so a watcher-maintained graph matches a fresh index (re-exports through __init__.py and type-aware member calls used to be lost on large batches). A file the batch cannot re-parse keeps its edges. The daemon keeps one parse cache per database in memory and refreshes it each batch | Nothing to change; expect somewhat higher daemon memory on large repositories |
Claude Code integration: the shipped search hook searches the grep/rg pattern, not the path. The Bash hook setup installs fires only when grep, rg, find, fd, ack or ag is the command actually invoked, and does nothing without jq. setup recognises only the exact hook commands it writes, so a user's similar-looking hook survives a re-run | Re-run nestweaver setup claude-code to refresh the hooks |
setup trae, setup devin and setup hermes write .trae/config.json, devin.json and .hermes/config.json. Those paths are not where the tools read MCP servers (Hermes reads mcp_servers in ~/.hermes/config.yaml; Devin reads .devin/mcp_config.json or ~/.config/devin/mcp_config.json), so the registrations are ignored | Register NestWeaver by hand in those tools' own config until setup is fixed |
Interaction memory (--track-interactions) is now a re-rank, not part of the walk. A node the query's walk reached is scaled by at most +5% by how often agents used it; a node the seed cannot reach stays out, however often it was used. Results a query only showed no longer count toward the three-session floor, and the end-of-session success signal reinforces only results the agent then opened (B3). Before this, interacted nodes were teleport targets, so context could list dozens of unrelated symbols | Optionally run nestweaver interactions clear once to drop sessions recorded under the old rules, whose shown-only results still carry a floor |
MCP brain_impact sends its dependents once, as impact_nodes; the duplicate nodes list is gone from MCP (the CLI --json keeps both). A cut made to fit the output budget is disclosed in output_budget.cuts, and truncated_by_limit is set only when limit was the cause (M3) | Read impact_nodes |
A repo filter that names no repo (hubs --repo nosuch, every --repo listed below) writes "error": "not found" in --json, the word every other not-found envelope uses; it was "not_found". status still reads "not_found" (B6) | Branch on status, or on error == "not found" |
New exit codes: summary --level <invalid> exits 64 (was 1); summary --target matching nothing exits 2 with a not-found message and envelope (was 0, "graph may be empty"); context --feature <name> naming no [[features]] entry exits 2 and suggests declared names in did_you_mean (was 1); investigate-expand resolving none of its targets exits 2 (a partial resolution still exits 0); investigate --scope project:<unknown> exits 2 and a malformed --scope exits 64, each reported once instead of inside an "Internal error" (B9, V4) | Treat 2 as a miss and 64 as a usage error |
pr-impact reviews one repo: the indexed repo checked out at the working directory's git root, or the one named by the new --repo <name or UID>. A repo-relative --files path now resolves in that repo only, not in every indexed repo with a file at the same path. When neither names exactly one repo every repo is matched, as before. --repo keeps its class on both routes: not found 2, ambiguous 3 (with candidates), over 512 bytes 64. An empty --repo '' is ignored (every repo is matched), and a selector that names nothing, such as repo:::, is not found (2) (B4) | Run pr-impact from the repo under review, or pass --repo |
Web: a <module> owner reads path::<module> in /api/v1/symbols/top (name and display_name), overview landmark labels, search hits (name, title, display_name) and snapshot.msgpack node names; the Cypher export keeps the raw name and adds a display_name property (R1, B5) | Read display_name where present; do not treat name in these responses as the raw symbol name |
brain status reports the daemon's manifest reconciliation as manifest_recovery (state, attempts, retry_after_seconds, error) and warns while it is deferred, retry_scheduled or blocked. A capture slowed by contention defers the rebuild and refuses no repo; a repo that runs out of its own 60 s budget on 3 passes in a row is refused with a contention: reason (no index --repo remedy needed) and the other repos publish. The write gate is held for at most 60 s of captures per reconciliation (D3). The text output shows the state too | Nothing to change; a contention refusal clears itself once the repo captures in time |
A watched vault batch that collides with another writer's transaction retries with backoff (2575 ms in all, the write lease released while it waits). If the collision outlasts the retries the batch's notes are owed and replayed on the retry backoff and at the next start, instead of the watcher exiting; brain status lists them under "Watcher reconciliation pending" and shows the daemon's last_watcher_failure, which now survives the watcher's restart (also in text) (V1) | Nothing to change; nestweaver brain refresh also replays owed notes |
A note-to-code relink no longer forces a PageRank recompute on every ranked read: the code-only ranks stay warm and .pagerank.json is rewritten at the new generation (D1) | Nothing to change |
A database error is called corruption more carefully. A write-transaction collision, an index publication in flight and a busy daemon are never corruption. A file shorter than its catalog read while another process writes says so and names the next step (if it repeats, nestweaver daemon --db <db> stop and re-run); an engine assertion or an unclassified corruption is corruption whether or not the daemon is up, and so is a truncation reported inside a timed-out health check or a refused publication (V2) | Follow the step the message names before restoring a backup |
investigate answers through a live watcher batch with publication_in_progress, but does not save that map as a bundle: the result carries bundle_not_saved (also printed in text) and its bundle_id cannot be expanded or hydrated (V4, I3) | Re-run investigate once the batch finishes to get a bundle to drill into |
A call a JS/TS/Python/PHP file makes at its top level, outside every function, class and method (a bare call, an IIFE, an Express-style route handler, a decorator, a __main__ guard, a top-level await), is now owned by a per-file <module> symbol (Module, entry point module_init). impact shows it as the caller, dead-code treats it as a root (so a function called only at module load is no longer listed, and <module> itself is never listed), and a one-line function above such a call no longer owns it. An owner that calls something is its own process, named <repo-relative path>::<module>, so detect-changes lists more processes in repos with decorators, route registration or top-level test setup; an owner with no outgoing CALLS is not a process. Name search hides <module> unless you search for exactly <module>. Resolver generation 10 and parsed cache 6 (nw-113) | Run nestweaver index --repo <path> --force for each repo after upgrading; search <module> by its exact name |
Module owners are labelled <repo-relative path>::<module> everywhere (impact rows, read-symbols, context, flow-trace, investigate neighbours, ambiguity candidates, process names, Mermaid export, the GraphML label, the web UI). symbol --json keeps each symbol's raw name and adds a labelled display_name to the symbol and every caller and callee. A file that is only top-level code gets a summary --level file line with no exports 0 symbols clause, and no line at all when that code calls nothing indexed in another file. read-symbols, impact, flow-trace and context look one up as path::<module>; the path is the exact repo-relative path or a suffix on a / boundary (app.js, or conftest without the extension), and a suffix that several files share is ambiguous | Name an owner path::<module>; lengthen the path when it is ambiguous |
A qualified name now narrows every name lookup, not only owners. The segments before the name are read two ways and both readings are kept: the first as a file (an exact repo-relative path, a suffix on a / boundary, or that suffix without its extension, such as conftest) followed by a class chain, or all of them as a class chain. So path::name works, and Class::method and path::Class::method work when the index records the method as a member of that class. A Rust impl block in a different file from its struct is not recorded that way, so Struct::method is not found there; its Did you mean: line names the path::name that resolves. Outer::Inner::method works in Java and C#, where a nested class is a member of its outer class. Python nested classes and TypeScript namespaces are not recorded as containers, so there it is not found, with the same not-found message. A class name that is also a file stem (Svc::run with a/Svc.ts and a class Svc in b.ts) is ambiguous rather than answered by the file. A leading / is ignored and \ reads as /. impact, flow-trace, symbol, context, brain context, context --feature entry points and MCP brain_context all resolve qualified names this way. A qualifier that names neither is not found (exit 2), where it used to resolve by the last segment. Every one of these commands answers such a miss the same way impact does: a Did you mean: line of path::name specs (path::Class::method for a member) on stderr, and did_you_mean in the --json not-found envelope and the MCP envelope. Suggestions are substring matches first, then names a small typo away (edit distance up to 2; up to 1 for a 3- or 4-letter name), and a path::name whose path names a file is answered from that file's members (the close ones, else all of them in source order). read-symbols and MCP read_symbols answer a miss the same way: not found (exit 2) with the Did you mean: line on stderr and did_you_mean in JSON and MCP. Before calling it a miss they try shorter readings that are still qualified, dropping the outermost qualifier each time (zzz::Svc::run reads as Svc::run), and say so when one answers. A leading qualifier that could be a file (it has a / or a ., or an indexed file has it as its stem) is never dropped: nothere.ts::Svc::run is not found, and the shorter reading that does answer (Svc::run) is the first did_you_mean entry, so another file's Svc::run is never read in its place. When one is dropped they say so: a warning: line on stderr and a qualifier_ignored entry (query, note) in JSON and MCP. They never fall back to the last segment alone, and a path::<module> is never shortened: a file with no top-level calls has no owner, so its path::<module> is not found rather than a list of every owner in the graph. Ambiguity candidates carry file_path, and each ambiguity carries a hint that only offers what tells the candidates apart: "pass a UID or path::Class::name" when each candidate has its own class, "pass a UID or path::name" for an unqualified name whose candidates are each in their own file, "pass a UID or path::Class::name" again when candidates share a file but a class tells them apart, and otherwise "pass a UID" | Qualify a name as path::name or Class::method, or pass a UID |
Module owners are callers only. They are left out of clusters, repo-map and investigate entries; of the changed-symbol lists and counts of detect-changes, affected-tests (including its tier-1 test count), pr-impact, brain diff and pre-push-impact; and of embeddings and vector search. An owner that calls a changed symbol is still reported as affected. A detect-changes process listed only because a member file's top-level code changed has affected_symbol_count: 0 (owners are not changed symbols) and carries top_level_code_changed: true to say why | Nothing to change |
context, brain context and context --feature rank owners after every real symbol and list at most 3. The rest are counted in module_owners_omitted (JSON and MCP), printed as "+N more files' top-level code", and left out of the totals; when an owner is listed or left out, each command's Connected (...) heading says so and how many (… owners listed last, N more not counted); otherwise the heading is unchanged. Owners the --limit cuts are counted the same way. When owners are hidden, by the cap or because so many files call the seed at load time that the owners sink under the walk's usual relevance floor themselves, their places are refilled with real symbols the walk scored under that floor (at most as many as owners were hidden, never past --limit), so a helper called at load time from many files keeps its real neighbours; the totals count them | Read module_owners_omitted |
summary --level symbol and MCP get_summary list every real caller but at most 3 owner callers, then +N more files' top-level code | Nothing to change |
dead-code seeds its walk from owners but counts them in neither "entry points" nor "excluded"; --json gains module_owner_roots. A walk seeded only by owners is not degraded: coverage needs entry_points + module_owner_roots > 0. With --repo (MCP repos) both counts are that scope's own roots, like the other totals; coverage still asks whether the whole-graph walk had any root, so a library reached only from another repo is not degraded. --json (and MCP) carry that whole-graph count as walk_roots, and the text warning "no entry point was found" is printed only when walk_roots is 0 | Read module_owner_roots beside entry_points |
read-symbols path::<module> returns the file's top-level code: the lines outside every function, class and method, with each elided body shown as ⋮ | Nothing to change |
pr-impact, blast-radius and detect-changes report a changed source file that is only top-level code (a script, a seeder, an entry file) instead of a silent Low with nothing changed: a changed-file-top-level-only warning (on all three) names its path::<module> owner and the first 10 symbols that code calls (+N more after that). Files that load it only for its side effects (import './x', a bare require) have no edge in the graph, so the warning says its importers were not assessed and the run is partial with gate degraded-unknown. In pr-impact and blast-radius JSON the callees are listed apart in top_level_callees (edge_type: TOP_LEVEL_CALL, depth 1). They are not dependents, so they stay out of affected_symbols, affected_symbol_count, the clusters and the risk level, which is Low unless the file has real dependents. The owner is still not a changed symbol | Read top_level_callees as what the changed top-level code calls, not as dependents; check the file's side-effect importers by hand |
affected-tests selects a test file whose own top level calls a changed function. When none of that file's tests was reached, its tests list is empty and selected_by: "top_level_code" (JSON and MCP) says why; it counts as one test, so the summary never reads 0 beside a selected file, and the text output shows it as (top-level code). A test file that only imports the changed module, without calling it, is not selected | Nothing to change |
affected-tests lists each test file once, in the best tier any of its tests reached, with all of its affected tests there (nw-799). A file used to appear in both tier 1 and tier 2 | Count files per tier, not tests |
read-symbols --token-budget (MCP token_budget) cuts a first symbol larger than the budget to its leading lines, closed by a marker line, and sets body_truncated: true; it used to return it whole. The text output then ends with body truncated to fit the token budget, not truncated: 0 symbol(s) dropped. A budget of 0 still returns the first symbol whole | Check body_truncated before treating a body as complete |
After an edit, the watcher and incremental index always re-resolve a changed file's direct importers; the 200-file cap now bounds only the transitive expansion. Past 5,000 direct dependents the pass is skipped with a warning naming index and index --force; past 200 the count and time are logged. Barrel files that only re-export are re-resolved too, so their reexport_resolved edges survive edits | Nothing to change |
A nested JS/TS function declaration binds only where it is visible: inside its enclosing function (including nested functions and callbacks there) and, in strict code (TypeScript, an ES module including any .mjs/.mts file, a "use strict" directive in the file's or an enclosing function's prologue), only inside its block. A call before it in that scope still binds it (hoisting); a module-level declaration still binds file-wide. A Python def nested in a function binds only inside it. A bare JS/TS call never binds a method, getter or setter (this.x() and obj.x() are unchanged), so a same-named free function wins. A nested class's members are MEMBER_OF that class, not a same-named top-level one. The full index and the watcher now agree on all of these (nw-787, nw-789) | Calls that bound a sibling's nested function, a block function after its block, or a same-file method now bind the import, the caller's own function or nothing. Re-index with --force (generation 11) |
The index CPU budget (NESTWEAVER_INDEX_CPU_PERCENT) is off by default (nw-814). It was added because macOS was believed to kill a daemon using 90s of CPU in 180s; it does not. macOS's CPU usage monitor is per thread, armed only by a spawner that asks (launchd does), and non-fatal: it writes a *.cpu_resource.diag report ("Action taken: none") and the daemon keeps running. Parse workers still run at utility QoS on macOS, which keeps the machine responsive. An explicitly set 1–40 still applies the budget, with 1–11 raised to 12 (the smallest budget with room beyond the 20s write reserve); 41–99, including an older plist's 50, mean off | Nothing to change. To keep a budget, set NESTWEAVER_INDEX_CPU_PERCENT to 12–40 and re-run daemon start so the launchd plist carries it |
daemon gc also prunes the authority registry (~/.local/state/nestweaver/authority), which grew one lock record per database scope ever locked (nw-811). It removes records whose database, publication root or operation journal is gone, and empty records from earlier releases that are older than 24h, unheld and match no live database; a record some process holds is never removed. --dry-run reports what would go and changes nothing (it skips the launch-agent and directory sweeps). --db X limits the prune to X's records and leaves the older empty records alone. A registry gc cannot sweep is a warning, not a failure | Run nestweaver daemon gc --dry-run to preview, then nestweaver daemon gc |
A watcher pass over a file with a class keeps its MEMBER_OF edges and resolves this.m() / self.m() as type_aware, as a fresh index does. Before, it dropped every MEMBER_OF edge of the edited file, so dead-code on a watcher-maintained graph could list constructors | Nothing to change; graphs kept by an older watcher regain the edges on the file's next edit or a --force index |
| A code watcher keeps running when a committed batch's evidence cannot be saved. It notifies the committed graph and discloses the outstanding repair, retaining replay paths in memory across retries. Successfully persisted replay debt also survives restart; startup repairs imports across bounded source batches. | Restore access to the reported evidence files and let the watcher retry. If replay debt itself could not be persisted, keep the watcher running until repair completes; after interruption, run nestweaver index --repo <path> --force to rebuild missing evidence and relationships. |
Rebuild required. LadybugDB 0.21 (0.21.2 as of 12.2.x) changes how text keys are hashed and its on-disk format marker did not change, so NestWeaver now refuses a database built by an earlier version before opening it: exit 1, nestweaver::db_rebuild_required, the exact rebuild command, no daemon started, no byte changed. Snapshot format is now 4; older snapshots are refused, and backup restore of a pre-upgrade archive warns that the result needs a rebuild | Stop the daemon, run nestweaver backup save <file> (it reads the old database read-only and marks the archive as pre-upgrade), then nestweaver publication rebuild --config <instance.toml>; the old database stays in place until the new one validates. To roll back, reinstall the previous version and restore your backup. Do not open a rebuilt database with an older NestWeaver. See docs/guide/publication-rebuild.md |
A daemon killed by a fatal signal (SIGSEGV, SIGBUS, SIGABRT, SIGILL) while answering is now named. A database with damaged index pages can open under LadybugDB 0.21 and fault on a later lookup; the command used to print a bare transport error … broken pipe, and the respawned daemon reported a healthy brain status. The command now exits 1 with nestweaver::daemon_engine_crashed and the recovery, MCP tool calls return the same message with isError: true, and brain status prints a warning (--json: a warnings[] entry of kind daemon_unclean_exit with exit, signal, at_unix, pid, count). A daemon killed from outside (kill -9, the OOM killer) is reported as exited_unexpectedly, with no claim about the storage engine. daemon stop (with or without --force), Ctrl-C on a foreground daemon run and an idle exit leave no warning, and the warning clears when a daemon next shuts down cleanly. Nothing in the database is changed | Restore the most recent backup with nestweaver backup restore <archive>, or rebuild with nestweaver publication rebuild --config <instance.toml>. Do not retry the request that crashed: it will crash the daemon again. See Troubleshooting |
Renaming a registered vault with --name (brain add <root> --name X, brain refresh <root> --name X, brain watch, MCP brain_add_source) is now refused, exit 1, when another root's vault already holds that name (case-insensitively), with the same message and remedy as registering a new root under it. It used to succeed and leave two vaults answering to one name. Without --name, brain refresh, brain watch, brain add and MCP brain_add_source now keep a registered vault's stored name instead of renaming it to its directory's (a new vault is still named after its directory) | Pick a free --name, or remove the other vault first |
brain context --kinds and --intent, and context --intent, now refuse a value outside their fixed vocabulary at parse time with exit 64 and a message naming the valid values (they exited 1 with an internal error). MCP brain_context, regex_search and count_patterns report an unknown kind as an invalid argument | Fix the flag value; the message lists the accepted ones |
Every repo filter now resolves through the one repo resolver and keeps its failure class: clusters --repo, dead-code --repo, brain context --repos, investigate --scope repo:<name>, cross-repo-contracts --repo, impact --repo, flow-trace --repo, contracts list --repo, contracts drift --repo and service-summary --repo exit 2 (status: "not_found") for a repo that is not indexed, 3 (status: "ambiguous", with candidates) for a name several repos share, and 64 for a selector over 512 bytes. A malformed selector such as repo::: names no repo and exits 2. An empty --repo '' exits 64 on hubs, clusters, dead-code and affected-tests, but impact, flow-trace and pr-impact ignore it and match every repo. dead-code, brain context and cross-repo-contracts used to exit 1 with an internal error; clusters --repo on the daemon route reported an ambiguous name as not found; investigate --scope repo:<name> matched repo UIDs by substring and merged every match, so a fragment that matched only a repo UID (not its name, URL or root) now misses; impact/flow-trace --repo fell back to substring matches on file paths and UIDs. Over MCP a repo filter that names no repo is isError: true with the not-found envelope, and an ambiguous one names its candidates | Branch on the exit code, or on status in the JSON envelope; pass a repo UID or root path to pick one of several same-named repos |
A wikilink that exactly and uniquely names a note (case-insensitive, any folder: its filename stem is unique in the vault and no other note has that title) now resolves at confidence 1.0 and is no longer reported. brain broken-links, MCP brain_broken_links, memory lint's broken_wikilinks (and broken_wikilinks_total) and doc-stats now count only BROKEN links: unresolved (0.0) or ambiguous (several notes match; rows gain candidate_count). Links that resolved to one note by a fuzzier tier (a shared stem narrowed by folder, a path whose basename alone matched, an alias) move to a separate list that is not broken: broken-links JSON low_confidence is now that array (was a count; the count is low_confidence_total, with low_confidence_truncated), and ambiguous is added; total/returned/truncated describe broken links only, and offset/limit window both lists. memory lint gains low_confidence_wikilinks (+ _total); doc-stats gains ambiguous_link_targets, and low_confidence_link_targets no longer includes ambiguous links | Read low_confidence_total for the old count; gate on total (or unresolved + ambiguous) for broken links. Existing vaults are re-resolved automatically: the Markdown link derivation version moves to 2, so the daemon re-derives each vault once in the background (or run brain refresh <vault>) |
A resolver-generation-stale repository now degrades blast-radius / pr-impact, detect-changes and affected-tests only when it owns a changed file, or is reachable from a repository that does through any chain of recorded or declared repo links (any symbol-to-symbol edge, or a [[links]] entry, in either direction). Any other stale repository is disclosed in a resolver-generation-stale-unrelated warning notification (naming it and the index --force remedy) and no longer degrades the gate, refuses affected-tests, or appears in resolver_stale_repos, which now lists only the repositories that degrade the answer. Declared links count when the daemon or MCP server was started with --config, and on the direct CLI route from --config or else the config the database's daemon last started with | Gate on resolver_stale_repos / gate_state as before; read the resolver-generation-stale-unrelated notification to find stale repositories elsewhere, or run stale-check |
The 409 for an empty seed container on POST /api/v1/brain/context carries the CLI's coded body, {"error": "seed container is empty", "status": "empty", "message"}. The remedy fits the container: add notes to a vault, re-index a repo that has no symbols | Branch on error: "seed container is empty" |
investigate-expand / investigate-hydrate on an unknown or expired bundle exit 2 (was 1), and --json writes {"error": "not found", "bundle_id", "message"} | Branch on exit 2, then run investigate again for a new bundle |
Every web API error body is JSON {"error": …}: axum's own rejections (a query parameter or JSON body that does not parse, a body over the limit, a wrong method) used to answer text/plain (or an empty body), and an unknown /api/… path answered an empty 404. Statuses are unchanged. Outside server mode, /admin/api/* on the UI is a JSON 404; it used to serve the SPA's HTML with 200 | Parse error bodies as JSON |
A declared project repo removed with remove-repo (or indexed since the last materialization) is now disclosed, or cleared, by the daemon's next code-link pass: list-projects and project-context print Warning: declared repo not a member: … without waiting for materialize-projects | Nothing to change |
brain status names a project whose declared repos resolve to none by its name, not its uid; --json code_links gains unscoped_project_names (index for index with unscoped_projects). notes_changed_as_of, printed as "as of the last pass", is now re-dated by every pass that finds changed notes; it used to keep the date of the first pass that saw them | Nothing to change |
list-projects lists each project's member repos: a Repos: line in text ((none) when it has none) and a member_repos object (project name to [{uid, name}]) in --json. The daemon's project listing carries them as repos on each project | Read member_repos instead of calling project-context per project |
cluster <id> with an id no cluster has prints a bounded hint on stderr (the count, the id range, the ten largest, and nestweaver clusters) instead of every cluster (2.2 MB on a large graph); still exit 2. ranking rank <missing> --json now writes the {"error": "not found", "symbol": …} envelope on exit 2 like the other read commands, and nestweaver --help no longer names a nonexistent rank command | Run nestweaver clusters for the full list |
investigate-expand / investigate-hydrate without --json now print text on the daemon route too (they printed JSON whether or not --json was passed); an entry with no body prints (body unavailable: <reason>). When a symbol's repo was indexed at a checkout that has since moved, the reason now names that recorded path and says it no longer exists, instead of "no recorded local_root". The bundle-cache warning of all three investigate commands now suggests --root / --db, not --repo / --config, which they reject | Pass --json for machine-readable output |
POST /api/v1/context and /api/v1/brain/context refuse a blank seed ("" or whitespace) with 400, as the CLI and MCP already did; seeds: [""] used to answer 200 with arbitrary results. /api/v1/context now shares the 100-seed cap, and both routes cap the request body at 64 KiB (413 past it) | Drop blank seeds before sending |
GET /api/v1/brain/notes?after= must be a note uid of the listed vault (note:<vault uid>:<hash>, or any note:vlt:…:<hash> without vault=). Anything else, including an empty or double-encoded cursor, is 400. It used to restart silently at page 1 or return an empty "end" page | Send back the X-Next-After value decoded once |
A context seed that names a vault with no notes (or a repo: with no indexed symbols), and nothing else, is no longer the No seeds resolved miss a bogus uid gets. The engine error starts Seed container is empty:; brain context exits 1 (was 2) and --json prints {"status": "empty", "error": "seed container is empty", "message", "empty_seeds"}; MCP brain_context is isError: true with that text rather than the not_found envelope; POST /api/v1/brain/context is 409 (was 404). A bogus vlt: uid is unchanged | Branch on status: "empty" (or 409) to tell an empty vault from a wrong uid |
Symlinks in a vault are still not followed (a link can point outside the vault or duplicate a note under a second path), but they are no longer silent: a symlinked directory, or a symlinked file with a Markdown name, is listed in skipped_files / skipped_notes with reason code unsupported and a reason starting symlink to a …. It is a policy skip: coverage stays complete and link derivation is not blocked. A .brainignored link is not listed. The watcher treats a symlink event the same way; a link pointing outside the vault used to fail the whole watcher batch, and one pointing inside was indexed through the link | Move or copy the target into the vault to index it |
An indexed note that grows past max_note_bytes now leaves the graph and search on the watcher and brain refresh --since routes too, as a full refresh already did, and is disclosed once as an oversized skip row. It used to keep its old text (still searchable, with its old links) and, when it grew while no watcher ran, was replayed as a full batch on every watcher start | Shrink the note or raise [indexing].max_note_bytes, then refresh the vault |
brain status answers while the daemon re-derives a vault and says so: --json gains vault_derivation.in_progress (vault_uid, root_path, phase, started_at_unix_seconds, elapsed_seconds, indexed_notes; null when none runs) and the text prints Vault derivation in progress: <root> (phase: …). A config file that cannot be reloaded for the repository-eligibility refresh is now a repo_eligibility_unrefreshed warning instead of failing the whole status | Read in_progress to see why link-graph tools refuse a vault for now |
MCP: a lookup tool whose target does not exist now returns isError: true with a JSON envelope in both the text and structuredContent: {"status": "not_found", "error": "not found", <target key>, "message", "did_you_mean"?}. brain_impact flips from isError: false to true; flow_trace, note_get, backlinks, cross_repo_contracts, project_context, brain_context/code_context (no seed resolved), investigate_expand/investigate_hydrate (unknown bundle), brain_diff (unknown repo), clusters (a cluster_id no community has; it used to return an empty list) and brain_remove_source return the envelope instead of prose; brain_memory_related and read_symbols with no target resolving now set isError: true, and brain_impact, brain_memory_related and read_symbols misses gain message. Through a daemon with an upstream, a brain_impact miss on both tiers (or the upstream unavailable) carries the envelope at the top of the two-tier result, and a read_symbols miss on both tiers keeps it after the merge. backlinks with a uid naming no note is now this miss (CLI exit 2), not an empty success. Searches that match nothing stay isError: false (nw-557) | Branch on isError and read structuredContent.status; retry with a did_you_mean entry |
MCP: tools/call with an unknown tool name is now a JSON-RPC error -32602 (Unknown tool: <name>), the same shape as a missing name. It used to be a result with isError: true. Schema-invalid arguments are unchanged: still a result with isError: true (nw-576, nw-700) | Handle the JSON-RPC error for a misspelt tool name |
clusters --json now uses the MCP clusters envelope: clusters (was communities), total / returned (were total_communities / returned_communities), per-cluster size (was member_count), plus cluster_count, symbol_count and limit (nw-559) | Read the new keys; the member lists are unchanged |
MCP blast_radius, affected_tests and detect_changes accept files, hub_nodes/bridge_nodes accept top, and read_symbols accepts neighbors, the CLI flag names. Two spellings of one argument with different values (e.g. changed_files and files, limit and top, targets and uids_or_fqns, target and name) are now refused with 'X' and 'Y' are aliases; detect_changes used to keep changed_files silently (nw-558) | Pass one spelling, or the same value in both |
An empty string for an identifier or query argument (uid, title, name, target, project, symbol, query, pattern, bundle_id, key, tag, path, base_ref), and an empty list or "" entry in seeds, targets, uids_or_fqns or patterns, is now a schema error: MCP isError: true, CLI exit 64. get_summary with name: "" used to return the whole catalogue, and backlinks/query_extensions/brain_memory_related with uid: "" answered success (nw-577) | Omit an argument rather than sending "" |
regex-search rows gain column (1-based character column on line) and matched (the matched text, at most 200 characters), so several matches on one line are no longer identical rows. The text output appends :COL to path:line (nw-549) | Use column to tell same-line matches apart |
brain doc-stats text prints one unresolved links: line (per note and target, with the per-section count in parentheses) instead of two lines that read as a note miss and a section miss. JSON and MCP keys are unchanged (nw-554) | Update scrapers of the old two text lines, or use --json |
brain context <name> --json with no seed resolved (exit 2) now includes status: "not_found" and message, the engine's text naming the nestweaver investigate command to run for a question. The daemon route's text now prints that sentence rather than the RPC wrapper (nw-511) | Show message to the user |
read-symbols <target> --root <dir> with a --root that does not exist, cannot be read, or holds none of the indexed files (reason no indexed file readable under it) now exits 1 with "error": "unreadable_root" and an unreadable_root object (path, reason, remedy). More generally, a read where every target resolved but no body could be read now exits 1, not 2. Exit 2 now means only that a target did not resolve (nw-539, nw-706) | Gate on 1 for unreadable source and 2 for unresolved targets; pass the right --root, or omit it to read from each repo's recorded root |
brain refresh gains --json and --fail-on-skip (nw-196). --json prints one object on stdout: vault, mode ("full" or "since"), coverage_status, skipped_count, excluded_by_request_count, skipped_files[] (path, reason_code, detail, observed_bytes, limit_bytes, excluded_by_request), frontmatter_unparsed[] (the same fields without excluded_by_request) and message (the text summary); progress stays on stderr. excluded_by_request is true for a .brainignore match. --fail-on-skip exits 1 when any row is not excluded by request. A vault whose only skips are .brainignore exclusions now reports coverage_status: "complete" everywhere (daemon progress, MCP brain_add_source, brain refresh --json); it used to read "degraded" on the daemon and MCP routes | Use --fail-on-skip or the JSON rows to gate on vault coverage instead of parsing the text summary |
summary --token-budget N whose first matching summary alone exceeds N now returns that summary and flags it with budget_exceeded_by_first_item (requested_tokens, returned_tokens, note), instead of returned: 0 and an empty list. MCP get_summary does the same. Later summaries are still cut by the budget (nw-644) | Check budget_exceeded_by_first_item if a response must stay under the budget |
nestweaver mcp --config <file> with an invalid config now answers the client's first request (normally initialize) with a JSON-RPC error naming the file and the parse error, then exits 1. It used to exit 1 with empty stdout (nw-656) | Show the JSON-RPC error; fix the config |
summary (text) and generate-guide --config/--rules-from are now served by a running daemon instead of opening the store directly, which failed while the daemon held the write lock. generate-guide never sends the config file: the CLI sends only the guide sections as a projection (guide_config: links, features, projects; no tokens, auth, git or URLs) plus the --rules-from contents as rules. Such a request goes to the local daemon only, never to a federated upstream. Each input is capped at 1 MiB (exit 1 when exceeded). MCP brain_guide accepts guide_config and rules; a config path is still refused. An explicit --config must match the running daemon's, as for other commands (nw-690) | Start the daemon with the same --config you pass to generate-guide |
context <missing> --json still exits 2, but message is now the root cause only, without the code_context RPC failed: code: 'Internal error' wrapper and the repeated text (nw-550) | Nothing to change unless a script matched the wrapper text |
brain refresh --since now lists its skip rows, and the "NOT marked current" note names the paths that could not be read. A refresh refused because the vault is Blocked names the vault, the directories unreadable now, when the automatic retry is due, and the remedy (a full brain refresh without --since). brain status prints a Vault BLOCKED: <root> line, and the daemon's vault_derivation status gains blocked_vaults[] (nw-694) | Run a full brain refresh <vault> to clear a Blocked vault without waiting for the retry |
A config-less brain add --instance X no longer breaks brain context after a daemon restart (ForeignRecord): derivation records carry the live instance, and a refresh heals a record stamped with the ambient default (nw-693) | Run brain refresh <vault> once on an affected database |
The daemon now rebuilds the manifest sidecar (and clears manifest-debt.json) when a repo's recorded eligibility is from the superseded pre-nw-652 fingerprint version; one such repo used to block the rebuild for every repo. suggest-links while manifests are owed now says they are not ready and why, instead of advising daemon start for a daemon that is running (nw-680, nw-700) | Nothing to change; re-index a repo named in the rebuild error if the sidecar stays blocked |
| The local UI rejects a non-loopback Host or mismatched Origin with 403 (nw-682) | For a deliberate local reverse proxy, set NESTWEAVER_UI_ALLOWED_HOSTS to its hostname and forward the browser's original Host |
UI responses now include CSP, nosniff, frame-denial and referrer headers (nw-625) | Allow the UI's same-origin assets when configuring a local reverse proxy |
GET /api/v1/source reads only indexed source paths. An unindexed path is 404 (source_not_indexed) and a malformed path is 400 (nw-682) | Handle the machine-readable error code; an arbitrary file path is no longer read from disk |
An ambiguous /source path is 409 (ambiguous_file, with repo candidates), and the web UI lets you choose a repo (nw-683) | Pass repo=<repo uid> when a relative path exists in more than one indexed repo |
Web UI: a deep link with scene params (node, mode, lens, seeds, kind, workspace) but no representation now opens the graph representation; it used to keep the JSON/table mode left in browser storage and write it back into the URL (nw-571) | Add representation=table (or json, matrix) to a link that needs a non-graph view |
Web UI: ?kind=repo (or note, service, symbol) without a node now filters Overview to that landmark kind and stays in the URL; it used to be stripped to / and ignored. Any other kind without a node shows a warning. GET /api/v1/overview accepts `kind=repo | service |
Web UI: ?engine=wasm now stays in the URL (it used to be stripped on the first state change, which quietly returned the page to the server engine), downloads snapshot.msgpack once per graph generation instead of four times, and the status bar shows the active engine (nw-570) | Nothing to change; drop engine=wasm from a link to return to the server engine |
An unreadable, invalid UTF-8 or invalid-glob .brainignore now refuses vault add, refresh and watch before changing indexed notes or sidecars (nw-684) | Repair the file and retry; a missing .brainignore still means no extra exclusions |
| Note-to-code links now scope candidate definitions before preferring them (nw-685) | Expect links into the note's own project when another project defines the same name |
A new note under a project's vault_folder gains project membership during reconciliation (nw-679) | Expect newly created notes in project-context after reconciliation |
| A UID seed with no text label no longer opens a semantic seed search (nw-686) | Expect structural results for the UID without unrelated semantic matches |
read-symbols verifies indexed spans and reports stale or relocated source instead of returning another symbol's body (nw-689) | Handle the additive stale-span/relocation fields in read-symbols responses |
CommonJS module.exports.X and exports.X functions are now definitions with call edges. Resolver generation 7 requires a full repo re-index before dead-code answers (nw-687) | Run nestweaver index --repo <path> --force for each repo after upgrading |
A JS/TS test block is now named <runner> <title> (describe getTier, `it returns gold | |