- analyze --no-share leaves the shared store even when routed to a
branch sub-index (gate on sharedStore, not placement.branch)
- clean --gc aborts a store whose member listing is unreadable instead
of treating it as empty, and skips non-directory entries under stores/
- reusing an existing commit graph no longer fails a finished analyze
when the redundant private graph cannot be wiped
- a store pointer holding JSON null, a number, a string, or an array is
treated as invalid
- clean, remove, and DELETE /api/repo hold the checkout slot's index
lock while deleting the slot
- the server analyze launcher waits on the checkout slot the worker
actually writes
- sync the Factory hook copy; isolate hook tests from storage overrides;
use a junction for the Windows worktree alias; the relative
GITNEXUS_HOME test now sets a relative home
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
These two strings were meant for e2a9467 and d746675 but were left out
of the commit; the zh-CN side landed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(setup): add Factory Droid (MCP + skills) to gitnexus setup
Register 'droid' in the editor-targets abstraction so `gitnexus setup -c droid`
writes the MCP server to ~/.factory/mcp.json and installs skills to
~/.factory/skills/ from the single canonical skills/ source (no per-editor
copies). uninstall.ts is target-driven, so removal is covered automatically.
Adds unit + round-trip coverage.
* feat(plugin): add gitnexus-factory-plugin for droid plugin install
* docs: add Factory Droid to editor support table and setup docs
* fix(factory-plugin): guard augment hook against fan-out and DB contention
Reuse the Claude adapter's acquireHookSlot and LadybugDB owner probe
(bundled byte-identical, kept in lockstep by a drift test) instead of
running an unguarded augment. Add direct tests for the hook and manifests.
* docs: align Factory row in editor support table
* fix(factory-plugin): honor GITNEXUS_HOOK_CLI_PATH so augment runs on Windows
* docs(hooks): point bundled guard copies at their drift tests
* docs(factory-plugin): note the Execute tokenizer's quoting limit
* docs(readme): clarify the Full tier and group the Factory row
* docs(hooks): trim drift note to a single line
* test(ci): run factory-plugin tests on the windows cross-platform lane
* refactor(hooks): drop the drift-note comments, the tests already enforce it
* fix(factory-plugin): pin CLI version and parse quoted shell patterns
- Pin mcp.json and the hook's npx fallback to gitnexus@<version> from
the plugin manifest, registered with the release sync script so a
mutable @latest can never execute on MCP connect or augment fallback
- Port the #2938 shell tokenizer (tokenizeShellWords + parseRgGrepPattern)
so quoted, backslash-escaped, --regexp=, -eVALUE, and -- patterns survive
- Add the #2938 regression matrix and pin assertions to factory-plugin.test.ts
* docs: add Factory Droid to published npm README
* fix(factory-plugin): wire marketplace so droid installs the Factory plugin
Add .factory-plugin/marketplace.json sourcing ./gitnexus-factory-plugin.
Droid reads it before .claude-plugin/marketplace.json, so
`droid plugin install` now delivers the Factory plugin (Execute matcher,
pinned mcp.json) instead of the translated Claude plugin (Bash matcher,
gitnexus@latest). Register the surface in the version-sync script and
cover the wiring in the factory and sync test suites.
* fix(factory-plugin): use registry lookup for index resolution
Bundle registry-query.cjs so external indexes resolve (#3060); re-pin to 1.6.12.
* fix(factory-plugin): sync Execute parser with Cursor hook
Fixes echo-rg and -f false positives; tighten test env isolation.
* fix(factory-plugin): stop no-match augment from re-running via npx
A PATH `gitnexus` that finds no match exits 0 with empty stderr, which
fell through to a second `npx -y gitnexus@<pin> augment` with its own 8s
timeout (16s worst case vs the 10s hook budget). Fall through to npx only
when the PATH launcher is missing (ENOENT); any launched PATH binary,
including a timeout or non-zero exit, now ends the augment.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(factory-plugin): filter augment stderr to the [GitNexus] block
runAugment returned raw child stderr, so npm/Node/LadybugDB warnings leaked
into additionalContext and noise-only stderr counted as success. Port the
Claude adapter's extractAugmentContext (verbatim, with isDebugEnabled) and
apply it on every launch tier before the success decision. Adds a drift test
against the Claude copy and PATH-tier noise/noise-only behavior tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(factory-plugin): quote DROID_PLUGIN_ROOT in hook command
An unquoted plugin root containing spaces (e.g. a Windows user profile
path) split into multiple argv words, so the PostToolUse hook silently
never ran. Quote it like the Claude plugin does, and pin the exact
quoted command in the hooks.json wiring test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(release): stage Factory plugin manifests in the release commit
The rc release job stages only the original four manifest surfaces in the
detached release commit, so the v<version> tag tree carried the Factory
plugin.json, mcp.json and marketplace.json at the previous version while
--check (working tree) passed. Stage them too, and guard the git add block
against the synced surfaces in sync-plugin-manifests.test.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(factory-plugin): simplify hook gates, spawn tiers and tests
- main(): resolve the repo only after the tool-name and pattern gates,
matching the Claude/Cursor hook order (skips fs/git work on no-op calls).
- runAugment(): share one spawnAugment helper between the
GITNEXUS_HOOK_CLI_PATH and npx tiers; PATH tier ENOENT logic unchanged.
- factory-plugin test: pre-filter comment lines instead of `continue`.
- sync-plugin-manifests test: hoist EXECUTABLE_MCP_FILES and derive
TOTAL_SURFACES from its length.
- fnSource(): throw when the function or its closing brace is not found.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): list Factory Droid in localized setup help
`localizeCliHelp` overwrites the `setup` command description with the
`help.command.setup.description` i18n key, so the literal edited in
index.ts never reached `gitnexus setup --help`. Add Factory Droid to the
en and zh-CN keys.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#2543)
- factory hook: run every augment tier under the bundled Unix timeout
guard (npx tier group-kills), keeping exactly-one-tier fall-through
- hook-db-lock-probe: trim GITNEXUS_HOOK_{LSOF,PS}_PATH once so a padded
override is used, not silently replaced (all 3 copies)
- hook-lock: evict a stale slot via rename-to-tombstone + identity check,
so a concurrently recreated fresh lock is never deleted (all 4 copies)
- registry-query: a set-but-invalid storage override resolves no repo
instead of falling back to the registry storagePath (all 4 copies)
- publish.yml: stage the ten skill mcp.json manifests in the rc release
commit; the staging test now requires every synced surface
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback round 2 (#2543)
- hook-lock: replace rename-to-tombstone eviction with an O_EXCL per-slot
`.evicting` marker plus an identity re-check before unlink, so a live
lock is never moved, and a crashed evictor leaves only a self-expiring
marker (all 4 copies)
- hook-db-lock-probe: clamp GITNEXUS_HOOK_PROC_CMDLINE_MAX to a named
256 KiB ceiling and require an integer, so an oversized override can
no longer fail the buffer allocation and miss a live owner (all 3 copies)
- registry-query: treat an empty GITNEXUS_STORAGE_PATH/ROOT as set but
invalid, matching the CLI's `!== undefined` rule (all 4 copies); the
factory test env now deletes those keys instead of blanking them
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback round 3 (#2543)
- hook-db-lock-probe: a capped /proc cmdline read stops early only once
both the GitNexus token and the mcp/serve mode are present (or at EOF,
the ceiling, or the budget), so a mode word such as `--require mcp`
before the GitNexus path no longer hides a live owner (all 3 copies)
- registry-query: correct the override comment; a filesystem root is
invalid only for GITNEXUS_STORAGE_PATH, not GITNEXUS_STORAGE_ROOT
(all 4 copies, comment only)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback round 4 (#2543)
- hook-db-lock-probe: an fd-directory read error other than ENOENT or
ENOTDIR on an identified server candidate now fails closed ('timeout')
instead of reporting not-owned (EMFILE/ENFILE/ENOMEM/EINTR)
- hook-db-lock-probe: resolve GITNEXUS_HOOK_TIMEOUT_PATH to an absolute
path before validating and caching it, so callers that spawn with a
request cwd can still execute the guard
- hook-db-lock-probe: document the chunked cmdline read's actual stop
conditions (all 3 copies)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Harden hook-lock eviction marker lifecycle (#2543)
Per the chosen option (B) for the stale-slot eviction race:
- `.evicting` markers carry a per-call owner token (pid + random hex)
- an evictor re-reads its token immediately before the slot identity
check and unlink; a stalled evictor whose marker was broken backs off
- `finally` removes the marker only while it still holds our token
- an orphaned marker is broken only if, re-checked just before unlink,
its bigint identity and token are unchanged from when judged stale
- doc comment states the two remaining two-syscall windows (slot
lstat->unlink, marker token->unlink); POSIX has no conditional
unlink, and the worst case is one extra concurrent augment
All four byte-identical hook-lock copies updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix CodeQL file-system race in hook-lock orphan-marker check (#2543)
breakOrphanedMarker stat'd the marker by path and then read it by path,
which CodeQL flags (js/file-system-race): the file could be replaced
between the two calls. Take the stat and the token from one open
descriptor (readMarkerSnapshot, O_NOFOLLOW where available) for both the
"judged stale" snapshot and the pre-unlink re-check. All four hook-lock
copies updated.
The replaced-marker test injected its swap via a readFileSync(path) spy,
which no longer fires; it now swaps the marker just before its second
open, counting opens of the marker path only (a per-path counter fired
early on slot-0 and let a mutant pass).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Unregister hook-lock exit listener on release (#2543)
Each acquireHookSlot registered `release` as a process 'exit' listener
that was never removed, so a long-lived process acquiring and releasing
slots repeatedly would accumulate listeners (MaxListenersExceededWarning)
and retain every closure. release() now removes itself. All four
hook-lock copies updated; a test asserts 12 acquire/release cycles leave
the 'exit' listener count unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- clean --gc never collects a slot whose index lock is held: an analyze
holds it until it registers the checkout, so a seeded but not yet
registered slot is busy, not orphaned.
- Reclaim compares absolute paths, so a relative GITNEXUS_HOME does not
make live commit graphs look unreferenced.
- graphPath is followed only into a published <commit>-<featureKey>
dir, never .publish-* staging (TS and all three hook copies).
- clean --gc fails on an unreadable stores root instead of reporting
nothing to collect.
- --no-share help states it is for opted-in clones.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Reject --no-share in a linked worktree before taking any lock or
indexing anything.
- clean --all and remove also delete each shared checkout's pointer.
- Delete an empty store only while also holding its cache lock, and
re-check emptiness under it.
- A store pointer is trusted only when the slot's own metadata names
this checkout; the editable pointer file just says where to look.
- Device names with an extension are prefixed on Windows only, so POSIX
slot names stay stable.
- Test fixtures use the gitnexus-test- prefix the stale-sidecar sweep
recognizes; help text and the private-graph label are accurate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Hold the slot index lock for the whole server embedding job, not
just the graph copy.
- --no-share re-registers and deletes the old slot under that slot's
index lock, so a running shared analyze cannot re-register it.
- clean --gc previews without --force, like every other destructive arm.
- status reports a pinned branch index as private.
- Slot names prefix Windows device names that carry an extension.
- A slot or store named ..<name> is a legal direct child.
- Shared-store suites run in the serialized lbug-db vitest project and
clear an inherited GITNEXUS_SHARED_STORE.
- Doc and test accuracy fixes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Address CodeQL js/path-injection and js/file-system-race on
shared-store.ts: every filesystem read rebuilds its path under a fixed
parent with an inline path.relative barrier, the .git probe is one read
(EISDIR marks a directory) instead of stat-then-read, and the graph
pointer cache stats and reads through one file descriptor.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The server embed job copies a shared checkout's graph under the slot's
index lock, so a CLI analyze in another process cannot interleave. A
forced rebuild with no embeddings to carry over drops the pointer
without copying a graph it would discard unread.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--help renders option text from the i18n catalog, so --share-with,
--no-share, clean --gc and clean --local-index need keys in both
locales, not just index.ts literals.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Never publish a graph whose build saw dirty files, and never trust a
local-index seed on the up-to-date path; it may hold reverted edits.
- Record slot pointers and reclaim under the publish lock, and reclaim
right after each publish, so a commit graph is never deleted between
publish and pointer save and superseded graphs don't pile up.
- clean --gc decides membership from the registry, so opted-in clones
and a main checkout without worktrees are not dropped.
- Leaving a store re-registers first, so an up-to-date run cannot leave
the registry pointing at a deleted slot.
- Drop pinned branch summaries when an entry moves into a store slot.
- Keep run.cjs and the AGENTS.md runner path inside the checkout.
- MCP handles follow the slot's current graph, and branch scoping reads
the slot's own metadata.
- Cache resolveGraphPath by metadata file identity; look up opted-in
entries with canonical registry paths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One graph-clone helper replaces two copy-then-rename blocks; clean and
status reuse formatSlotSize; leaving a store reuses
removeSharedStorePointer; withStoreLock is imported from its own module
instead of a re-export.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MCP, the HTTP API, group sync, augmentation, and the Claude hook (all
three byte-identical copies) resolve a flat slot's graph through
resolveGraphPath instead of joining 'lbug' onto the storage path, so
checkouts at one commit share one open database. The embeddings writers
(embeddings sync and the server embed job) take a private copy first
and never write an immutable commit graph. The post-analyze settle
probe accepts fresh metadata that points at an existing commit graph.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A shared checkout gets <repo>/.gitnexus/store.json pointing at its store
slot; resolution follows it only when it names that checkout's own slot.
An existing local index seeds the slot and is left in place. status
(text and --json) and doctor report the store, whether the graph is
shared or private, and any leftover local index, which
clean --local-index removes while keeping the pointer. With
GITNEXUS_SHARED_STORE=off a previously shared checkout indexes into its
own .gitnexus again and never writes a commit graph.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A clean shared checkout reads a commit graph and owns no graph file, so
the code-index presence check made status report it unindexed and
registry validation skip it. The check now follows the slot's
validated graphPath.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An independent clone joins a linked worktree's shared store only with
analyze --share-with <repo>, and only when its normalized origin URL
matches that member's (credentials stripped, as #2054 compares). The
registry remembers the choice. --no-share moves an opted-in clone back
to its own .gitnexus and reclaims its old slot; linked worktrees always
share and are pointed at GITNEXUS_SHARED_STORE=off instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deleting a shared checkout slot (clean, clean --all, remove, and the
server delete route) recounts references under the store's publish
lock and deletes commit graphs no member points at, then the store
itself once empty. A graph that cannot be deleted (open on Windows) is
reported and kept for the next pass. clean --gc also drops member slots
whose worktree is gone or no longer resolves to the store.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Linked worktrees read and write the parse cache and durable ParsedFile
store under the store's caches/ directory. Before pruning, a run folds
in the chunk keys recorded by every member slot and commit graph, and
the fold, prune and save run under a store-wide cache lock so one
member never evicts another's live chunks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new shared slot is seeded from the store's commit graph nearest to
HEAD, else from this checkout's or the main checkout's
repository-local index (copied under that index's lock, source left in
place). The run that follows is up to date or incremental instead of a
full build. If a pointed-at shared graph has been removed, analyze
falls back to a full build instead of an incremental update over a
missing baseline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A linked worktree writes its own slot in the shared store. A new slot
is seeded with a pointer to the nearest commit graph, so a clean
checkout at an indexed commit takes the up-to-date path and writes no
graph. A checkout with local changes gets a copy-on-write private
graph before its first write. After a successful run, a clean checkout
at HEAD publishes its graph into commits/ under a store lock, or drops
it when that commit graph already exists. Commit graphs are never
written after publish.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
getStoragePaths reads a flat slot's recorded graphPath only for checkout
slots under the stores directory; other paths keep <storagePath>/lbug with
no I/O. A recorded path outside the store's commit graphs is ignored.
resolveStoragePath falls back to an existing store slot for an
unregistered checkout, so reads never move to an empty slot.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Linked worktrees of one repository resolve to one store under
GITNEXUS_HOME/stores/<key>, keyed by the canonical git common dir. The
resolver reads the .git entry directly, so hot paths spawn no git. Slot
naming moves to a leaf module so storage-resolver and shared-store do not
import each other.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(query): send hub content once across process_symbols rows
With include_content, a symbol in several execution flows carried its
full source text on every (id, process_id) row. Keep content on the
first row for each symbol id and omit it from later rows. Membership
fields, is_entry_point, and symbol_count are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(query): point agents to the content row and lock the dedup in tests
The query tool text now says content is kept once per symbol id across
the whole process_symbols array, possibly under a different process_id,
and names context({uid, include_content: true}) as the fallback.
Tests: the integration suite asserts func:validate has two rows with
content on exactly one. Unit tests cover a later entry-point row that
is flagged and stripped, a hub in three processes, and that the shaper
does not mutate its input.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(query): state the content-once rule on include_content and in the query hint
The query tool's include_content property now says content is sent once
per symbol id, on its first process_symbols row, and that
context({uid: "<id>", include_content: true}) returns it for any row.
When a query asked for content, the Next hint adds that same fallback;
without include_content the hint is unchanged. The example call now
uses the "<id>" placeholder style used elsewhere in the tool text.
Tests: pin the property sentence, check the hint with and without
include_content, and cover a max_symbols slice that moves the content
row to a later process and a first row that is also the entry point.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(query): keep process_symbols attaches per (id, process_id)
Hubs that belong to several processes were collapsed to one row by unique-id first-wins, so later process cards advertised symbol_count with nothing to expand. Slice, pair-key, and recount now live in one shaper so each listed process can open its own attaches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(query): drop leftover unique-id attach bind name
The shaper already returns process_symbols; keep that name through the query response so unique-id vocabulary does not linger at the call site.
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(query): wrap process attach call for prettier
CI quality/format rejects the previous call shape.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(query): assert hub lines under each process card
A global duplicate count still passes when both lines render under one process.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(query): recount group symbol_count after the service prefix
The single-repo (id, process_id) join was readable as if @group results included process_symbols. Scope that sentence, and set symbol_count from the filtered attaches when a service prefix is set.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(query): document the attach contract and count prefix rows once
The Unreleased notes and query tool text now say how to join process_symbols, including the @group follow-up. Service-prefix filtering counts those rows in one pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: ignore local Vercel link artifacts
Keep .vercel and env files out of the repo after a local preview link.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): repair citation chips, code panel line math, stale agent state
Audit findings in the web client, each verified against the source:
- RightPanel: citation chips ([[path:10-20]], [[Class:Foo]]) were wired to a
stub resolver that always returned null, so clicking any citation did
nothing. Expose resolveFilePath from useAppState and use it.
- CodeReferencesPanel: graph startLine/endLine are 1-based but were treated
as 0-based, so the highlighted range and scroll target were off by one
line; AI citation cards always rendered "code not available" because the
snippet loader was a stub. Fetch per-citation snippets via /api/file.
- useAppState: sendChatMessage read llmSettings.activeProvider outside its
deps (stale provider capabilities after switching provider);
initializeAgent trapped projectName at '' for callers without an override
(system prompt labelled the codebase "project"); the embeddings 409 dedup
matched a message the server never sends for same-repo jobs.
- tools.ts impact: for path targets every symbol defined in the file shares
the filePath, so the disambiguation always picked the first row and could
analyze an arbitrary symbol while reporting a file impact. Prefer the File
node.
- useSigma: the layout timeout called stop() but never kill(), leaking one
ForceAtlas2 Web Worker plus four graph listeners per completed layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): release repo lock on cancel, IPC-first worker cancel, route hardening
Audit findings in gitnexus/src, each verified against the source:
- analyze-launch: cancelJob marks the job failed before the worker exits,
so the exit handler's terminal early-return skipped releaseLockOnce and
the repo stayed locked ("Another job is already active") until restart.
Release on terminal exit and when forkWorker bails on a terminal job.
- analyze-job: cancellation now sends { type: 'cancel' } over IPC first and
signals only after a 15s grace. On Windows child.kill('SIGTERM') is a
forceful termination, so leading with it could kill the worker inside a
LadybugDB write. Mirrors core/auto-sync/analysis-worker-launch.
- api resolveRepo: a job that FAILED during the hold-queue wait fell through
to the { __timedOut } sentinel ("taking longer than expected") instead of
404; only /api/repo checked the sentinel, so graph/query/search/file/grep/
embed/delete crashed on entry.storagePath with a 500 after a 5 minute hang.
Return null on failed jobs and check the sentinel in every consumer.
- api processes/process/clusters/cluster: resolve ?repo= through the HTTP
resolver (documented policy on resolveRegisteredRepoEntry) and pass the
registered absolute path to the backend; add the standard rate limiter.
The raw param previously reached the MCP resolver, which runs a
cwd-relative realpathSync probe + registry refresh on a bare-name miss
and accepts unambiguous partial names.
- api body handling: Express 5 leaves req.body undefined without a JSON
content type, turning "Missing X" 400s into TypeError 500s; body-parser
4xx errors (malformed JSON, over-limit) were also reported as 500.
- /api/file: the lexical path.relative check cannot see symlinks; re-check
containment on realpath so a cloned repo containing evil -> /etc/passwd
cannot read outside the root.
- repo-manager unregisterRepo: used the lenient reader, so a transient read
error (EBUSY/EPERM racing another process's atomic rename) turned into
writing [] and deregistering every repo. Use the strict-if-present reader.
- clean --branch: compared registry paths with raw path.resolve instead of
the canonical registryPathEquals used everywhere else (macOS /private/var,
Windows short names / drive-letter case) and reported indexed branches as
not indexed.
Tests: cancelJob IPC-before-signal contract; /api/file symlink escape (403)
and in-repo symlink (200), skipped where the host cannot create symlinks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: keep example env files visible and ignore local Cursor config
.env* also hid gitnexus/.env.example and eval/.env.example. .vercel was already ignored. The web app's .cursor/ stays local, including its MCP file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add realtime execution ops dashboard for Vercel monitoring.
Expose /api/ops snapshots over the local serve process and a ?view=ops SPA panel so analyze/embed jobs can be watched live from the hosted web UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address gitnexus-check review on cite/embed/resolve paths
Pass req into resolveRepo for process/cluster routes, tighten same-repo embed 409 handling, guard empty citation paths, fix snippet retry races, and drop the lone-File ambiguity fallback.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ops): harden CORS, redact paths, and bound ops streams
Restrict Vercel CORS to exact production hosts, omit raw repo paths/URLs from the unauthenticated ops feed, rate-limit and cap SSE connections, and fix dashboard SSE/poll edge cases.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): stabilize citation fetches and file impact matching
Retry cancelled snippet loads without duplicate in-flight reads, cap range-less citation downloads, and make impact file matching unique-suffix-aware with a synthetic File target when LIMIT drops the File node.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web/ops): close gitnexus-check review threads on SSE and redaction
Cap citation reads, reconnect ops on applied server URL, skip SSE onError after abort, and strip URL query/fragment from public repoName.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ops): abort SSE on poll fallback and harden repoName parsing
Prevent dual SSE+poll after a failed safety snapshot, skip overlapping poll ticks, ignore aborted streamSSE onError, and basename Windows drive-letter URLs so ops never leaks path segments.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ops/web): close gitnexus-check threads on SSE budget and credential leak
Keep finite SSE retries across short 200s, strip backend URL userinfo before ?server=, and redact progress messages on the public ops feed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3348)
Restore 0-based GraphNode line math, redact public job poll/error fields, fix omit-?repo= 400, hold the analyze lock across cancel-during-settle, and restore the Vercel shared compile.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): contain leftover-slot reclaim to the slot and keep --stale status honest
Preview and force now share one branches/ containment rule, nested junctions cannot walk a sibling index, and a mid-loop git failure no longer claims leftovers were not deleted after a successful rm.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(cli): share leftover-slot helpers without changing reclaim behavior
Pull the rolling I/O pool and owned-cwd storage lookup into one place so clean --stale/--branch and leftover listing stop restating the same ownership and concurrency paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3348)
Keep the omitted-repo snapshot instead of re-listing, stop citation and ops races, redact full public repo URLs, and restore fake timers in teardown.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3348)
Store graph node citation lines as 0-based offsets and correct the default-port origin comment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3348)
Redact public SSE progress text, stop citation append retries from
cancelling in-flight reads, and keep the ops dashboard from showing a
stale snapshot or clearing a failed Connect.
Note: pre-existing failure in incremental-index-extension-dml-gate and other lbug/env unit tests not addressed by this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3348)
Prune citation snippets when AI refs are cleared, and poll /api/ops at 2s so the fallback stays under the 60/min limit.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): apply prettier class order for format check (#3348)
CI quality/format runs root-only npm ci, so prettier-plugin-tailwindcss
sorts scrollbar-thin without the web Tailwind catalog.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3348)
- Require a unique suffix match for graph-backed citation paths so
ambiguous names like index.ts no longer open the first graph file.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3348)
- Require a path-component boundary so unique citation suffixes cannot match filename substrings like myindex.ts
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): redact filesystem paths in ops text
Unauthenticated /api/ops and poll replay worker errors. URLs were
scrubbed but home-directory and Windows paths still leaked.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): omit repoPath from public SSE frames
/api/ops lists job ids, so the unauthenticated progress stream
must not replay the analyzed filesystem path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): treat embed lock 409 as a busy error
Analyze and embed share the same lock string. Mapping that 409 to
embedding hid an in-flight analyze as a successful embed start.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): bound impact File path suffix matches
Unbounded endsWith let lib/foo.ts select src/mylib/foo.ts. Require
an exact path or a unique /suffix, matching resolveUniqueIndexedPath.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): keep canceled analyze slot until exit
Marking failed before the worker exited let a second POST start
cloneOrPull against a LadybugDB file still being written.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): skip Windows SIGTERM on job dispose
child.kill('SIGTERM') is TerminateProcess there. Ask over IPC first
and leave the 15s grace timer to SIGKILL.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): let process routes skip the analyze hold
GET /api/processes and /api/clusters always waited up to 300s.
?awaitAnalysis=false fails fast; default still waits like /api/repo.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): cover public ops and analyze SSE user flows
Lock the unauthenticated dashboard and analyze complete/fail/cancel paths so a leaked repoPath, token, or home path cannot ship unnoticed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3348)
Keep caller cancel reasons over the worker's generic IPC, skip publish while cancel is pending, and redact scp-style remotes on the public ops feed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address remaining PR review feedback (#3348)
Release the analyze slot when a worker fails to spawn, and omit branch refs from the unauthenticated ops feed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): do not reuse an analyze job that is pending cancel
A dying same-repo job still occupies the single slot; 202-reuse would
attach a new client to a cancel in flight.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): hold the analyze lock until the worker exits after cancel
Cancel error IPC used to drop the repo lock while the child was still
checkpointing. Abort settle immediately on pending cancel so the slot
is not held for a 60s disk poll.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): redact known repo paths with spaces in public ops text
Known repoPath/repoUrl literals are replaced first so a clone dir with
spaces cannot leak past the whitespace-bounded path regex.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(server): return the live job on analyze and embed DELETE
Hard-coding failed made clients retry immediately and 409 while the
child still occupied the slot. resolveRepo now returns not-found as
soon as that job fails instead of waiting out the hold timeout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): drive public analyze and ops through a live gitnexus serve
Spawn the real backend and observe requests instead of intercepting
them, so slot occupancy, redaction, and reconnect stay honest.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(web): reuse code-panel helpers and drop dead UI aliases
Citation fetches already had selectedNodeFileRange and snippetRepoKey;
the impact File suffix filter already handled exact paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3348)
- Skip createJob reuse after cancel IPC is consumed while the child remains
- Hold the repo lock until exit when complete IPC races a pending cancel
- Scrub full remote URLs before known repoUrl prefixes in public ops text
- Reject unique impact File suffix matches from a truncated LIMIT 10 page
- Make live e2e helpers bound probes, clean up failed startups, and wait out the cancel slot
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): open live e2e pages on the Vite host CI actually bound
Absolute 127.0.0.1:5173 navigation refused on Actions because wait-on
and Vite use localhost (often ::1). Honor FRONTEND_URL when set, else
pick the first of localhost / 127.0.0.1 that answers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3348)
Hold the analyze lock until worker exit when cancel aborts settle, and assert GitLab failure chrome does not leak host or path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): keep live analyze e2e under the analyze rate limit
POST /api/analyze allows 10 requests per minute per IP. The slot-free
helper re-POSTed every 400ms while a cancelled worker was exiting, spent
that budget, and the lock test's hold request got 429 instead of 202.
- postAnalyze waits out a 429 using the RateLimit reset and retries
- slot polling backs off to 2s and leaves a small POST budget for callers
- the slot probe is a clone that fails before any worker fork, so the
probe itself no longer holds the slot after reporting failed
- the lock test holds the slot with a real local analyze
- token and GitLab tests wait for a free slot before posting from the UI
- request fetches carry a timeout; teardown signals the serve process group
- an empty FRONTEND_URL falls back to the default base URL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#3348)
- ops view: a `?server=` link no longer auto-connects to another origin
while a deploy token is held; it prefills and waits for Connect
- analyze completion: a local-path run reconnects by the path this client
submitted, so duplicate basenames stay collision-safe without repoPath
on the public SSE frame
- stale slot cleanup: revalidate each nested directory (lstat + realpath)
right before readdir, so a mid-cleanup junction swap aborts instead of
walking an outside tree; list phases run sequentially so the slot-I/O
cap is global
- e2e: 429 backoff honours the caller deadline; the unreachable-backend
ops test navigates through the resolved frontend URL
- drop the unused `repo` member from the clean integration helper
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(server): reconnect after analyze by an opaque repo id
Public job views and the SSE terminal frame no longer carry repoPath, and
repoName is not unique, so a post-analyze reconnect by name could load a
same-named sibling. The server now issues `repoId`: an HMAC of the
canonical registry path under a per-process random key. It is set on
complete jobs (ops view, analyze poll, SSE terminal frame) and matches
the new `id` on `GET /api/repos` entries. The web client resolves it to
the exact entry path on completion; unknown ids fall back to the name.
This covers URL clones and folder uploads, and replaces the local-path
only fallback.
With reconnect off the label, public `repoName` for a branch-pinned URL
clone is the repository name, not the `<repo>__<branch slug>` registry
name, so the requested branch stays off the unauthenticated feed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#3348)
- stale slot cleanup: a descendant that vanishes before its unlink is
treated as removed instead of aborting the reclaim
- e2e teardown: escalate to SIGKILL on the process group when the live
backend ignores SIGTERM for 5s
- ops view: drop the dead initial value CodeQL flagged
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#3348)
- public redaction: a known repoPath now also consumes its descendant
tail, so `<repoPath>/src/secret.ts` becomes `[path]` instead of
`[path]/src/secret.ts`; a same-prefix sibling is left to the path scrub
- e2e: the cancel test waits for the analyze slot the previous failed
local-path job still holds; `fetchOps` carries the request timeout
- docs: SSE terminal payload and RepoAnalyzer `onComplete` describe
`repoId` resolution
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#3348)
- RepoAnalyzer: drop a completion that resolves after unmount, so a slow
/api/repos lookup cannot switch repos after the sheet was dismissed
- e2e: select the local-path input by test id (the placeholder differs on
Windows); the slot probe's job wait honours the caller's deadline
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(server): assert the public SSE terminal frame in analyze-api
The #2790 terminality tests still expected `repoPath` on the terminal
frame. This PR replaced it with the opaque `repoId`, so assert that shape
and that the analyzed path never appears in the stream.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(web): wait for the analyze slot between duplicate-repo setup runs
The server now keeps the single analyze slot until the worker exits, even
after its job reports complete. repo-path-identity posted the second
duplicate's analyze immediately and got 409 in CI. Use the shared
slot-aware POST, which also waits out a 429.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(lbug): self-heal read-only opens refused by an interrupted checkpoint
Homelab repro 2026-09-19 (image 1.6.10-20260917, @ladybugdb/core 0.19.x):
a wiki pod killed mid-CHECKPOINT left the engine's checkpoint artifacts on
disk (lbug.wal.checkpoint / lbug.shadow / checkpoint intent+apply locks),
and every later READ-ONLY open refused with 'Cannot open database in
read-only mode while checkpoint is in progress' — permanently, until a
writable open (any gitnexus analyze) happened to run.
Two defects, both fixed:
1. The refusal was unclassified. ensureReadOnlyConnectionUsable (direct
adapter) and openReadOnlyDatabase (pool) recovered missing-shadow and
shadow-replay errors but rethrew this one raw, so 'LadybugDB
unavailable for __wiki__' repeated forever. Add
isReadOnlyCheckpointInProgressError (LADYBUGDB-CONTRACT, live-verified
against 0.19.1) and route it through the same writable-open recovery —
including at OPEN time, where the refusal fires before any probe can
run (pool: init() moved inside the try; direct: doInitLbug catch).
2. The existing shadow-replay recovery was not durable. Reproduction
matrix on 0.19.1: the writable probe replays the WAL in MEMORY only —
without an explicit CHECKPOINT the engine drops the pages at close and
the follow-up read-only open silently serves the pre-checkpoint state.
Both recovery paths now CHECKPOINT after the probe, which applies the
replay, consumes the sidecars, and clears the checkpoint locks.
Verified end-to-end: real engine 0.19.1, killed-mid-CHECKPOINT state →
exact refusal → pool adapter self-heals → 80,800 rows intact, sidecars
consumed. The planted-signature integration test runs on any engine
version (refusal asserted only where the engine emits it, 0.19+).
* docs(architecture): list checkpoint-in-flight artifacts and the read-path self-heal
* fix(lbug): address review findings — shared cursor closer, tighten structural guard
Both findings from the gitnexus-check review on this PR:
1. pool-adapter.ts: the recovery CHECKPOINT closed its cursor with a bare
unawaited result.close?.(). Use the shared best-effort closer
(closeQueryResults) the repo already funnels both adapters through, so a
cursor-close failure stays cleanup and cannot escape as an unhandled
rejection.
2. sidecar-recovery.test.ts: the structural regex omitted the leading
negation, so as a substring match it also accepted the inverted
predicate (recover ONLY shadow-replay, exclude checkpoint) — the exact
regression the guard exists to prevent. Pin the full
'!isReadOnlyShadowReplayError(err) && !isReadOnlyCheckpointInProgressError(err)'
throw-through shape.
* test(lbug): wire the recovery plant into lbug-db/LBUG_NATIVE; force the refusal on any engine pin
Address the tri-review findings (all four):
P1 — the planted-signature integration test was collected by the parallel
'default' project and never by the serialized 'lbug-db' project, and
Windows/macOS CI never ran it: register it next to its sibling in
vitest.config.ts (lbug-db include + default exclude) and in
cross-platform-tests.ts LBUG_NATIVE, per TESTING.md's rule for native
@ladybugdb/core suites. Verified via 'vitest list --project lbug-db'.
P2 — on the committed 0.18.3 pin the plant passes as 'pool opens and
count(n)=300' without ever exercising the new classifier or recovery
CHECKPOINT. Add forced-refusal behavioral tests for BOTH adapters: a mocked
native layer whose first read-only Database refuses with the canonical
0.19 message, asserting the exact self-heal shape (ro-refused -> writable
open -> CHECKPOINT -> ro retry) and that a healthy db never triggers a
writable open. The direct adapter is lazy, so its refusal is scripted at
the first probe query rather than init().
P2 — the LADYBUGDB-CONTRACT header on isReadOnlyCheckpointInProgressError
claimed '^0.18.0' like its siblings; only 0.19.x emits this string (0.18.3
tolerates the state). State the first-observed version so a bump reviewer
validates the right binary.
P2 — the pool's writable replay recovery quarantined on missing-shadow
even when the error came from the post-probe CHECKPOINT, where the main
file has already changed: mirror the direct adapter's probeSucceeded guard
(replaySucceeded) and fail closed instead of parking a live sidecar.
Also assert walBuffer.byteLength > 0 in the plant so the fixture cannot
silently degrade into an empty shell on tolerant engines.
* test(lbug): fix hosted-CI failures — Windows handle release, version-gated plant, prettier
Address the CHANGES_REQUESTED review of the hosted run:
Windows blocker (Win32 Error 33, locked file region at the pooled reopen):
the fixture now makes handle release explicit — waitForFixtureRelease
probe-reads the db and its residual WAL with bounded retries after every
native close (plant, raw refusal probe, pool close), mirroring the
adapter's own Windows handle-release probing. The WAL is re-planted from
the captured bytes instead of renamed: a close-time auto-checkpoint can
consume the live .wal out from under the rename (ENOENT, second flake).
Version-gate the plant itself: on < 0.19 engines that tolerate the planted
signature, its synthetic sidecars are not a consistent staging state for
the old engine (double-apply replays surfaced as 'Person already exists in
catalog' — the third flake), and no refusal can be forced there anyway.
The plant now skips below 0.19 with that rationale in-file; the behavioral
contract on every pin stays with the forced-refusal units, and this native
suite remains registered in lbug-db + LBUG_NATIVE so Win/macOS exercise it
as soon as the pin moves off 0.18.3.
Also: prettier on the two flagged files (format gate).
* fix(lbug): keep failed checkpoint heals from re-entering CHECKPOINT
Wrap recovery failures without repeating native refusal text, skip already-wrapped errors in doInitLbug, and pin constructor-time heal plus the Windows reopen skip.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3340)
- Clean each forced-heal tmpDir in afterEach so earlier cases do not leak
- Compare major.minor when version-gating the interrupted-checkpoint plant
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3340)
Reset the pool forced-refusal mock Database sequence in native.reset() so later tests can still script the first construction as the checkpoint victim.
* Address PR review feedback (#3340)
Count every MATCH on the pool forced-refusal mock and assert the writable replay probe so CHECKPOINT-without-probe cannot stay green.
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(ingestion): tRPC pattern detection — procedures, curried calls, route extraction
Three changes to properly index tRPC router files:
1. HOC-in-pair patterns: detect procedures like
'create: procedure.mutation(async ({ input }) => {...})'
as named Function nodes (pair > call_expression > arguments > arrow_function)
2. Curried call detection: capture 'workflow(db)(input)' chained calls
where call_expression.function is itself a call_expression
3. tRPC route extraction: new route-extractors/trpc.ts detects
.query()/.mutation()/.subscription() procedures, maps to /trpc/* routes
with prefix inference from router variable names
4. Function/Const dedup: structural check skips Const nodes when
variable_declarator value is arrow_function/function_expression
5. Process-route linking: match by (filePath, methodName) instead of
filePath only, preventing shared flows across procedures in same router
(cherry picked from commit eced15e60a39a181a48bff8c5bbe278d26aa53b8)
* fix(route-extractors): line-by-line scanner for chained tRPC procedures
Original regex only matched direct patterns (list: proc.query()) but not
chained patterns (create: proc.input(z.object({...})).mutation()). Only
41/256 routes were detected on Jurialis.
Rewrote extractTrpcRoutes() as a line-by-line scanner that:
- Detects procedure keys starting with *Procedure builders
- Tracks currentProcedure forward until terminal .query()/.mutation()
- Handles .input() chaining naturally
- Deduplicates via seen Set on procedurePath
Result: 245 routes from 30 routers (was 41), 256 total after re-index.
(cherry picked from commit 49edf953e93e6b67b77932e866fcbfb8d089b1f3)
* feat(mcp): expose tRPC chains via context/query tools
Eight fixes to make the tRPC route->procedure->workflow->sub-workflow chain
visible through the standard MCP tools (context, query) that AI agents use,
instead of requiring raw Cypher queries.
- A context: order incoming/outgoing CALLS test-last, raise LIMIT 30->100
- B query: batched ENTRY_POINT_OF lookup, surface route URL on processes
- C query: mark process_symbols entry point with is_entry_point=true
- D entry-point-scoring: skip UTILITY_PATTERNS (get*/set*) penalty for
symbols in tRPC router files so framework boost (3.0x) is preserved
- E fts-schema: index Route nodes so /trpc/* URLs are keyword-searchable
- F tools: bump max_symbols default 10->25 to fit procedure->workflow chain
- G context: optional chain_depth (0-3) param walks CALLS edges in both
directions and returns layered chain field
- H LadybugDB bug workaround: WHERE r.type IN [...] silently drops edges
on relationship properties; replace with OR chains (7 occurrences in
local-backend.ts, pdg-impact.ts, graph-queries.ts)
Validated on Jurialis: setProviderCap procedure now appears as caller of
setProviderCapWorkflow, /trpc/cabinet.setProviderCap Route is searchable,
chain_depth=3 returns the layered call graph.
(cherry picked from commit c24df0adce91e80f191a5c5d2e8b14e9da677366)
* feat(mcp): surface is_entry_point + routes in context, raise query limit
context() is the mandatory pre-edit tool (AGENTS.md). Until now an agent
had to issue a separate query() call just to learn whether its symbol is
a process entry point or which HTTP route it handles. These two fields
make context() self-sufficient for the bmad-dev flow.
- context: query STEP_IN_PROCESS now returns p.entryPointId; new
ENTRY_POINT_OF lookup attributes routes only from processes where the
symbol is the entry point (middle steps do not own the route) plus any
direct Route->symbol handler edge (tRPC procedures outside any Process)
- context: new top-level is_entry_point (true only) and routes[] fields
({url, method?}), emitted only when non-empty to keep the diff additive
- query: raise default limit 5->10 so dense domains (tRPC action router
with 20 chains, data-export with 9 flows) surface more of their flow
set without an explicit param
(cherry picked from commit 004f5b0ba985fcef3dfac5e67a2f2f7262184215)
* docs(mcp): document chain_depth, routes, and entry-point flag; neutralize examples in comments
* fix(mcp): address code-review findings on tRPC entry points, queries, and dispatch
- entry-point scoring: optional-chain framework detection and normalize
path separators before router-pattern matching (P1 crash on .js routers)
- tRPC extractor: emit controllerName as null (callers resolve via
lookupClassByName; a router object stringified as name corrupted lookups)
- route dedup: key seenRoutes by method:url so GET/POST pairs survive
- legacy TS queries: drop curried-call patterns (arity-corrupting for
overload resolution) and require non-array callee on pair member calls
- registry-primary TS query: mirror the non-array-callee predicate on the
new pair member-expression patterns (fixes query compile error)
- group tool port: forward chain_depth into per-tool context args
* fix(mcp): nested tRPC router paths, controller-less route binding, query chain_depth
- trpc extractor: brace-depth nesting stack composes sibling router paths (bare router() import style included); merge-prefix dot normalization; strict publicProcedure allowlist gate; drop phantom router metadata
- call-processor: bind controller-less tRPC routes to same-file handlers via exact single-match symbol lookup; ambiguous or missing handlers skipped
- query/group query: optional chain_depth (0-3) enriches ranked processes with their context chain; fix _computeContextChain layer docstring
- tree-sitter TS/JS scope queries capture string-key function declarations; tRPC pattern scoring narrowed to server router files
- parse-cache schema bump to v103 for extractor and route-binding changes
- add trpc route extractor regression tests (9 cases incl. sibling nesting and merge prefix)
* Address PR review feedback (#3339)
Close remaining review threads: compact tRPC keys, comment-safe terminals,
quoted-key identifier HOC captures, group-query default alignment, and
LadybugDB label scalars on context chains.
* chore(autofix): apply prettier + eslint fixes via /autofix command
* test: pin PARSE_CACHE schema bump to 103 (PR #3339 review fixes)
* fix(ingestion): bind same-name tRPC handlers and surface HANDLES_ROUTE
Same-name procedures resolve by startLine, the extractor keeps nested paths
through multiline schemas, and context/query UNION HANDLES_ROUTE for leaf
procedures that never become Process entries.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Clamp group query bounds, drop HOC pair false positives, emit tRPC
terminal lines and hyphenated quoted keys, cap chain BFS concurrency,
and restrict the router utility exemption to accessor names.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Score JS/JSX tRPC routers like TypeScript, ignore inner db.query and
unrelated .merge calls, mask regex braces, and assert clamp tests invoke
the query mock.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* fix(ingestion,mcp): emit-side HOC callback guards + MCP schema prettier (PR #3339 round 2)
* Address PR review feedback (#3339)
Treat `/` after return-style keywords as a regex, drop the synthetic
appRouter path prefix, and bind the create mutation via an inline callback.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Qualify query() docs so is_entry_point is promised only when the entry
symbol is among the search hits.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Emit tRPC terminals only at procedure paren depth, drop the filename
prefix on bare appRouter = router(), mask regex after if (), and cap
groupQuery member fan-out.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
De-duplicate context-chain BFS nodes reached through multiple frontier
edges, and bind tRPC identifier callbacks (`.mutation(handler)`) to the
handler symbol instead of the procedure key.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Apply LIMIT 50 after DISTINCT neighbors in the context-chain BFS, and
treat the official lowercase `procedure` builder as a tRPC procedure key.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Keep the later duplicate tRPC object-literal key so route binding matches
the handler JavaScript actually ships.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Compose same-file identifier-mounted tRPC subrouters so admin: adminRouter
emits the live admin.list path instead of an unprefixed /trpc/list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Memoize tRPC identifier-mount paths so a depth-N chain stays linear, and
gate it with the build-free measure.mjs / baselines.json harness.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Compose identifier mounts without phantom /trpc URLs, bind wrapped and
multiline handlers, fail-close missing bench budgets, and reject query
page bounds before search.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Reject advertised MCP query bounds in group mode and chain_depth
instead of clamping or accepting non-integers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Reject invalid groupContext chain_depth and ignore non-callable
same-file tRPC handler candidates.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Memoize live tRPC mount paths so the depth-N chain bench stays linear,
and render invalid group/MCP bounds without throwing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Keep t.merge('prefix.', namedRouter) procedures live by recording a
zero-hop mount so the unmounted-router drop does not hide them.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3339)
Recognize type-annotated `const adminRouter: AppRouter = t.router(`
bindings so identifier mounts still compose.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(storage): classify leftover per-branch index slots
Operators need a shared enumerator for deleted-branch leftovers before clean --stale or doctor can reclaim or report them.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(storage): reclaim a per-branch slot and empty branches/
Named clean --branch now shares one rm-then-registry helper so the last leftover slot can drop the empty branches directory, and a failed rm still keeps the retryable summary.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli): add clean --stale to reclaim leftover branch indexes
Operators can drop per-branch slots whose recorded branch is gone without remembering each name, while a git-list failure stays a no-op.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli): report leftover branch indexes in doctor
Operators can see cwd orphaned per-branch slots and their size, then reclaim them with clean --stale, without doctor deleting anything.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): keep unreadable branch slots out of stale reclaim
A stat error other than ENOENT/ENOTDIR must not look like a missing
directory, or clean --stale --force drops the registry row and leaves
the slot on disk.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(cli): keep leftover-slot display helpers in the CLI layer
Preview and doctor share one size formatter and an i18n path for
registry-only rows, so storage no longer owns display copy.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): keep leftover reclaim moving after a registry drop failure
Catch removeBranchIndex rejections so --stale continues, match doctor
registry rows through canonicalizePath, and size leftover slots sequentially.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): match leftover-slot registry rows with canonicalizePath
Use the repo-manager path contract so clean --stale and --branch still
see registry-only leftover rows when cwd and the stored path differ.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(review): contain leftover-slot deletes and re-check live heads
Refuse symlink and junction escapes under branches/, unlink slot links
instead of removing through them, and skip --stale --force when a name
is a local head again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3338)
- Bound listLocalHeads spawnSync with GIT_PATH_LIST_MAX_BUFFER.
- Clarify that doctor leftover reporting is cwd-only, not registry-wide.
- Drop the MCP/serve assumption from clean --stale delete failures.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3338)
- Describe disk-only leftover slots in --stale help, not only recorded branches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): stop doctor reclaim copy when heads cannot be listed
Doctor was naming clean --stale for leftover rows even when git cannot
list local heads, which is a no-op. Print the retry-git message instead (#3337).
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert: drop Unreleased changelog notes from this branch
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): keep live branch pins when a tag shares the name
%(refname:short) disambiguates against tags, so clean --stale treated
still-local heads as leftover. Fail closed on obstructed slots and
unlistable branches/ directories.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3338)
Bound leftover-slot listing, revalidate paths immediately before delete, and keep registry rows when a stray disk-only directory claims a recorded branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): drop a folder onto the analyzer to upload it
The Local Folder panel of RepoAnalyzer is now a drop target. A dropped
folder is walked with the File and Directory Entries API
(DataTransferItem.webkitGetAsEntry), directories on the shared exclusion
list are pruned before they are read, and the resulting File objects are
handed to the existing filterRepoFiles -> uploadFolder -> trackJob path
with webkitRelativePath set to <folder>/<rest>, so the server receives the
same manifest shape the webkitdirectory picker produces.
Compared with the picker, the walk never enumerates node_modules or .git,
stops at the server's 20000 file cap and 64 path segments, skips
unreadable entries instead of failing, and reports progress while it runs.
Loose files and several folders at once are refused with a message (the
server accepts one top-level folder). The walk runs under the request
controller, so a mode switch or unmount aborts it; Analyze and the picker
are blocked while it runs. The panel-wide target also stops the browser
from navigating to a file dropped a few pixels off the button.
New strings in en and zh-CN; browsers without webkitGetAsEntry keep the
picker button and get an explanation.
* Address PR review feedback (#3315)
Clear readingCount only when this drop still owns the request controller, and skip oversized files before they count toward the 20k drop cap.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3315)
Count oversized files the drop walk skips in the summary droppedCount, and correct the 250-file batch-read comment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(swift): match repeated SPM target prefixes
* test(swift): cover repeated SPM target prefixes
* test(swift): cover valid prefix before later partial match
* docs(swift): clarify target grouping parity scope
* docs(swift): clarify target grouping parity scope
* docs(swift): clarify target grouping parity scope
* fix(swift): resolve imports from Package.swift targets, not path segments
Stop fabricating IMPORTS from import Foundation onto a same-named folder.
Declare modules from Package.swift when the manifest is usable; keep
Sources/* for grouping and fail-open folder resolve minus SDK names.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): keep empty Package.swift declarations and nested .target() deps external
An inferred Sources/* folder is grouping-only. A dependency .target(name:) is not a module. Treat both as unresolved so import Foundation cannot bind to a decoy folder.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(swift): keep implicit IMPORTS intra-group and gate linear Package.swift resolve
@_exported must not paint sibling files as implicit imports. A dedicated
bench pins declaration-only resolve and (t_4n/t_n)/4 linearity.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Honor member-only @_exported imports, skip comments while scanning
Package.swift factories, fail-open mixed helper-built target lists, and
block CoreData/CoreGraphics decoy folders on the inferred path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Match path: "." as the package root, skip block-commented Package.swift
factories, read import kind from the clause only, and skip capture tests
when the optional Swift grammar is missing.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): scan Swift import-kind without nested regex backtracking
CodeQL js/redos flagged the comment-skipping IMPORT_KIND_RE; a linear walk keeps the same kind tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): linear Package.swift factory scan and gate Swift context
parseSwiftPackageManifest re-walked every prefix for comments (O(n²) in factory count). Resume the scan and cover nested factories in one pass. Wire Swift into the import-target context arm now that resolveImportTarget is 5-arg.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Tighten Package.swift and import-text scanners: skip comments/strings, reject escapes, treat ident + [ as incomplete, and drop the unused factory-comment wrapper.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(ci): prettier the @_exported availability fixture
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Judge Package.swift completeness from Package(...)'s own targets: argument instead of raw-text regexes, nest block comments when reading an import kind, and strip leading ./ from declared target paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Collect Package.swift factories only from Package(targets: [...]), fail-open on computed array elements, and treat // after a label colon as a comment.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Ignore stray factories when Package() exists but omits targets:.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Address PR review feedback (#3105)
Require the Package-scan seen box so the always-true undefined guard goes away.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>