npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@neuralis/brain-core

v0.3.2

Published

The brain of Neuralis: any system a connector can reach — local directories, repositories, machine sandboxes, the vector memory itself — joins ONE URI-addressed, vector-indexed, project/user/agent-scoped virtual filesystem, chunked and indexed on sync wit

Readme

@neuralis/brain-core

The brain of Neuralis: any system a connector can reach — local directories, repositories, machine sandboxes, the vector memory itself — joins ONE URI-addressed, vector-indexed, project/user/agent-scoped virtual filesystem, chunked and indexed on sync with optional embedding, revertible on write, policy-governed per path. A synced source pays back twice: its content becomes searchable memory, and its markdown becomes source packages. Concretely: URI-native filesystem, vector memory, source connectors, and the Files UI.

Responsibility

  • Read, create, modify, list, and search files across every registered source through one URI vocabulary.
  • Manage project-, user-, and agent-scoped source configurations and persist them in the platform zone.
  • Host the connector registry contract: source connectors declared by other packages plug in via SourceKindRegistry.
  • Sync connector content into a Qdrant-backed payload/full-text index, with optional chunked embeddings.
  • Expose the Filesystem package routes (/api/packages/brain-core/*) and the agent tools (fs_read, fs_write, fs_list, fs_search).
  • Render the workspace Files widget and the filesystem.change agent-card.

URI vocabulary (the four-tuple coordinate)

Every file artifact in Neuralis lives at the coordinate (uri, connector, os_uri?, container_dir?):

  • uri — <source>://<path> where <source> is the source slug (URI scheme).
  • connector — the ConnectorPort bound to that source slug at boot.
  • os_uri — optional full host-OS path, produced by connector.resolveOsUri(uri). LocalDiskConnector.resolveOsUri throws OsUriUnresolvableError (code: 'os_uri_host_pair_missing') when running in Docker without a hostRoot configured — vectors and UI persist os_uri: undefined rather than fake paths. The shared helper domain/paths.ts:resolveOsUri swallows that throw.
  • container_dir — optional container-path counterpart for skill / shell execution, threaded through runtimeStack.

Connector kinds shipping with brain-core: local (full filesystem capabilities incl. mkdir/rmdir for real folder create/remove and stat, the content-free existence probe a caller uses instead of reading a whole file to learn it is still there), brain (Qdrant-backed internal memory; reads go through FilesystemService, not the placeholder connector). Other packages contribute more kinds — webtop from machine-core, and host from agent-core (a read-WRITE view of a paired device — the server or a person's own machine — through its broker over the device link; 13 declared capabilities including write/delete/move/mkdir/rmdir and rawRead; containment comes from the device owner's ceiling, never from a missing capability. See the host-plane rules below).

A connector's manifest id is not its kind, and only the KIND addresses the registry: brain-core's on-disk connector is declared as id local-disk with "kind": "local", and source_kind: 'local-disk' is additionally a UI-side rename applied by FilesystemService. GET /sources/kinds/:kind/schema takes the registry kind — local — and answers 404 Unknown kind "local-disk" for every caller, at every role, when handed the id instead. Enumerate the live set with GET /sources/kinds rather than deriving it from a connector id or a UI label.

A mutation against a connector that cannot perform it is a 400, never a silent success. applySourceMutation checks the connector's declared capability SET before every write/delete/move — an emulated move needs both write and delete. Optional-chaining these calls (the previous shape) made a read-only connector report a write that never happened, while the journal and the vector index recorded it as landed, so the indexed content and the real file diverged with nothing to signal it.

The Files widget offers VS Code-style structural ops — create file/folder, rename, move (incl. cross-source), copy/paste, delete (files + recursive folders) — each a package capability the agent ALSO has: fs_write carries mkdir and copy as verb keys (and recursive folder delete + cross-source move), all behind the one write route. A rename or move onto a uri that already holds a file or folder is refused (DESTINATION_EXISTS, still HTTP 200 — the outcome is in ok): a relocation never overwrites, on any connector kind. Create, copy and upload land BESIDE an existing file under a suffixed name. The occupancy answer is ONE primitive (uriOccupied in usecases/write/occupancy.ts: the index at any status ∪ the connector — the disk decides, never the user-scoped index alone). A HUMAN's structural op commits immediately; an AGENT's stays pending_review (the interactive marker — see src/routes/README.md + app/filesystem/ui/README.md). Pending pages expose visible-id continuation and complete/partial/restart state. Full projected vector reconstruction plus canonical URI twins occurs before slicing, so page size bounds output rather than scan cost. Journal retention defaults to 32 MiB; oversize journals rebuild per operation. Repair defaults to 50 visible records per batch with fresh per-record authorization.

URI schemes you will see in production:

| Scheme | Connector kind | Source of slug | Notes | |---|---|---|---| | brain:// | brain | builtin | Vector-only memory, agent-scoped by default | | data:// | local | builtin | Project data zone ({neuralisHome}/projects/{pid}/data/) | | data.X:// | local | user | Additional local mount with a named slug | | packages:// | local | builtin | Project's _packages/ directory | | app:// | local | builtin (privileged mount — sources.privileged) | Platform app zone (~/.neuralis/app/); writes from agents are denied | | neuralis:// | local | user (pnpm neuralis:mount add … --neuralis) | The deployment's OWN neuralis/ folder, live from the host. Surfaces as a local-kind source — there is no dedicated neuralis connector kind. The switch is --neuralis, not --slug neuralis (that is the ordinary data mount at /mounts/neuralis, without the two anonymous volumes that keep _runtime/ and node_modules/ from being shadowed). | | neuralis-skills:// | local | user (same overlay, rooted at <root>/skills) | The narrower default: only the shipped operator skills. Preferred over neuralis:// for giving an agent the operator-command vocabulary — the overlay root holds .env, and a source root is a READ boundary in a way a sync include is not. | | machine:// | webtop | machine-core | Webtop sandbox; declared by machine-core, not brain-core. machine is the slug / urlScheme — the registry KIND is webtop, and only the kind addresses GET /sources/kinds/:kind/schema. | | os://<key> | — | decorative only | Renders inside the system prompt's <runtime_stack> block as a label for NEURALIS_MOUNT_<KEY> mounts. No connector handles os://. Agents addressing files inside a mount use the local-kind source slug attached to that mount. |

The git MCP connector (first type:'mcp' in the repo)

package.json declares the repo's first PackageConnector of type:'mcp', transport:'in-process' — { id:'git', name:'Git', icon:'GitBranch', category:'mcp', requiredScopes:['project'] }. Unlike the type:'source' connectors above, an in-process type:'mcp' descriptor is inbound-host: it advertises brain-core's OWN hosted git tools through PackageContributionHost / GenericMcpServer, it is NOT an outbound MCP server. Because of that direction, both ConnectorLifecycleManager.registerFromPackage and PackageLoader skip transport==='in-process' (alongside type==='source') — it is never persisted to ConnectorConfigStore and never auto-connected, so it does not produce a phantom "Git connected" row from GET /connectors. It is accepted only for trust >= trusted (in-process is in-host execution, the same floor as stdio/sidecar; untrusted/WASM is rejected). The two advisory fields (defaultToolFilter, requiredScopes) are metadata only and GATE NOTHING — the real gates stay mcpHosting.enabledPackages + each hosted tool's requires.features + source uri-policy. There is no credentialScopeHint: a connector's secrets are declared once in neuralis.credentials[] and resolved through the injected CredentialResolver. Today the descriptor is metadata only: no git tools are hosted yet, so it appears in the <packages> overview (PackagesOverviewBuilder renders - Git (mcp)) but NOT in the MCP-hosting config panel — the hosted tools + the enable-semantics land later.

Git engine (src/git/GitRunner.ts)

GitRunner wraps simple-git over the container's baked-in git binary (reused via resolveSearchBinaries, the same path as gitGrep). It is keyed on a resolved checkout root (resolveLocalRoot().resolvedRoot) — the SAME root the local connector mounts and the shell uri-policy gate keys on — and enforces four containment floors: (1) cwd = root + a sub-path guard + a git rev-parse --show-toplevel --absolute-git-dir clamp, so a .git ABOVE the source and a .git FILE/symlink whose git-dir redirects OUTSIDE the source are both rejected (GitOutsideSourceError, one memoized spawn — the containment close; the git-dir is compared against the CANONICALIZED root, so a legitimately symlinked mount path never false-rejects. Limit: a standalone-mounted LINKED WORKTREE, whose git-dir lives in the main repo outside the source, fails soft — no badges); (2) reject leading-dash refs/paths (option-injection) and require -- before path operands; (3) -c core.hooksPath=/dev/null -c core.fsmonitor=false -c core.pager=cat on every call, defeating a malicious repo-local .git/config (closes the malicious-checkout RCE class); (4) strip EVERY ambient GIT_* env var, then set GIT_CONFIG_GLOBAL=/dev/null, GIT_CONFIG_SYSTEM=/dev/null, GIT_TERMINAL_PROMPT=0 and GIT_OPTIONAL_LOCKS=0. Simple-git v4 requires top-level allowEnvironment naming exactly those four fixed defensive overrides, together with the existing explicit unsafe:{…} opt-ins for floors (3)/(4). It blocks those same flags/env as attacker-injection vectors — stripping ALL ambient GIT_* (not a denylist) remains the robust rule. It exports the pure helpers classifyGitOp(op, args) → 'read'|'mutating'|'destructive' (for the later approval gate) and normalizeStatus. Read primitives are fully implemented; local-mutating ops are implemented but NOT yet tool-exposed (they wait on the uri-policy write gate + approval guard).

Remote ops. fetch/push/pull/mergeFetched run through a SEPARATE #spawnGitRemote variant — raw node:child_process, NEVER simple-git (the remote -c set — credential.helper, core.sshCommand, protocol.* — is exactly what simple-git's vulnerability check blocks, so routing it through the simple-git ctor config: would be boot-fatal). In addition to the three local hardening flags it prepends the full protected-config-aware remote-hardening -c set (only 4 git config keys are protected; everything else is honored from a hostile repo-local .git/config, and -c = command scope wins): credential.helper= (empty RESET first — a hostile repo-local helper is BOTH a !shell RCE AND receives the pushed token on git's unconditional store broadcast) then credential.helper='!<constant helper>'; core.askPass= + credential.interactive=false (a repo-local core.askPass is consulted before the GIT_TERMINAL_PROMPT=0 gate); protocol.{file,ext,git}.allow=never; pinned remote.<n>.{upload,receive}pack; core.sshCommand=/core.gitProxy=. The credential helper snippet is a CONSTANT string (no interpolation of ANY dynamic value — username/host/token arrive via child-env NRS_GIT_{USER,HOST,TOKEN}, which the shell reads but never re-evaluates as code); it reads protocol=/host= from stdin and releases the token ONLY when protocol is https AND host equals the BINDING host (NRS_GIT_HOST) — this stdin host-match, NOT any URL derivation, is the redirect-class defense (insteadOf is un--c-clearable). HTTPS-only (SSH fails closed). The token is present ONLY on the network-only invocations (fetch/push); pull = fetch (token in env) + mergeFetched (merge --ff-only, NO token in env) so the token never rides a working-tree checkout/merge (a hostile filter/textconv/merge driver is attacker-NAMED ⇒ un--c-clearable, closed only by keeping the token out of that process). No clone, no force-push. The push token is the CALLER'S OWN git.<host>.pat/.oauth.accessToken, resolved per-request via src/git/tokenResolve.ts (resolveGitToken) over the kernel-injected ctx.credentials cascade at the ensureAccess-validated scope, and never cached on GitRunner. status(subPath?) is pathspec-scoped when given a folder (the per-file badge path) — git status -- <subPath> so it costs O(subtree), never O(repo) — and an optional GitRunnerOptions.timeoutMs caps a slow scan (kills the child → GitTimeoutError). That timeout is an INACTIVITY timer — it resets on every output chunk, capping silence, not wall-clock — so the potentially-large read primitives (diff, logGraph, show, cat-file type/size) run on a direct-spawn path with an engine-level output accumulation bound (~4 MB combined stdout+stderr; the child is KILLED past it → GitOutputLimitError): every byte is buffered in-process before any route cap can trim it, and a continuously-streaming command trips no inactivity timer, so the byte bound is the real memory floor beneath the route caps. blobType/blobSize (git cat-file -t/-s) let a caller refuse a non-blob or oversized object BEFORE buffering its content; the <rev>:<path> colon form is composed inside the engine (the literal rev index is the sentinel for the staged blob, :0:<path>) — caller text never enters it raw. The per-commit / outgoing file lists ride ONE two-tree form: treeDiffFiles(from, to) = git diff-tree -r --name-status -M -z <from> <to> (NUL-safe parseNameStatusZ; a rename record is R<score> NUL <old> NUL <new>), with commitFiles(sha, {hasParent}) composing <sha>^ ↔ <sha> (first-parent, merge-correct) or EMPTY_TREE_SHA ↔ <sha> for a root commit — the caller answers the parent question via commitParents(sha) (rev-list --parents -n 1), never by catching errors. The single-arg diff-tree -r <sha> and the <sha>^! forms are EMPTY on merge commits (live-proven) — never use them, and never --root (it prepends a header record the -z parse would have to skip). The outgoing readers are outgoingShas(cap=500) (rev-list @{u}..HEAD, null — never a throw — when no upstream/any failure) and resolveUpstreamHead() ({baseSha|null, headSha}, server-side ref resolution so no route ever takes caller refs; baseSha is merge-base(@{u}, HEAD), NOT @{u} — a two-tree diff against a DIVERGED remote tip would phantom remote-only files as Deleted, disagreeing with the two-dot @{u}..HEAD commit set; on an ahead-only branch the merge base equals @{u}, on unrelated histories it degrades to EMPTY_TREE_SHA); both are LOCAL-ref reads — zero network; the network reach is the remote-ops surface (fetch/pull/push, below).

Git-aware file tree — per-file badges

The first consumer of GitRunner: FilesystemService.list stamps a git working-tree letter (M/A/D/R/?, the 2-char porcelain XY code) onto dirty files under a git-backed local source, surfaced as a new git? field on each ListFolderResponse file. Both the fs_list tool (a bracketed [M] glyph in the tree/flat markdown + legend) and the Files-widget tree (a colour-coded GitStatusBadge) render it as a third, orthogonal decoration axis — a file is simultaneously synced (sync tone) AND M (git) — never folded into the sync tone / getNodeVisualState precedence. Computation lives in src/git/sourceGitStatus.ts. Folders with dirty descendants get a rollup dot (rollupFolderGit picks the most-significant descendant), so changes are visible from the root without drilling in. Both client list paths carry the git field — the root load (listSlice) AND the lazy folder-expand (filesystemStore.ensureFolderLoaded, what the tree uses when you open a folder); a server-sourced FileNode field is dropped if only one is wired.

Scaling (million-file safe). The filesystem serves huge sources by paying only for the folder being viewed, so git enrichment does too: sourceGitStatus runs git status pathspec-scoped to the listed folder (never the whole repo), behind an inactivity timeout (bounds git's silent whole-tree scan — the huge-repo lstat storm — so a too-slow scan is skipped, not blocked), a short per-(root,folder) TTL cache, and a cheap .git stat pre-gate (non-git sources — the majority — pay one stat and skip). It is strictly additive and fail-soft: any problem yields no badges, never an error and never a slowdown of the vector/connector list paths.

Security + limits. Badges are attached AFTER the source-level access filter, so a badge never lands on a file from a source the caller cannot see. The status is read-only (no creds/network/mutation); the listed folder passes GitRunner's existing path guard. UX limit: per-file badges require the source ROOT to BE the git checkout toplevel — a source that is a subdir of a larger repo shows no badges (the toplevel clamp fails soft). Files INSIDE the nested checkouts of a multi-repo source (see the multi-repo model below) now DO get per-file badges/rollups: when the source root has no .git the badge path enumerates the discovered nested roots (the injected gitRepos port = discoverSourceRepoRoots) and runs a pathspec-scoped git status against each in the listed scope, rebasing the match keys by the <rel>/ prefix — up to the ≤25-repo discovery cap of nested git status on a root view (each timed + cached, the documented first-view cost). Untracked files list individually (--untracked-files=all, VS-Code parity; .gitignore still excludes ignored trees).

File tree — lazy hydration and convergence

The tree loads in two independent stages.

Stage 1 — source headers, no walk. The root call asks for the source REGISTRY only (sources_only), so every header paints before a single connector is touched. Nothing about one source's reachability can delay another's row. Counts stay opt-in and belong to the Sources panel.

Stage 2 — content on expand. Opening a source branch (or a folder, or following a deep link) lists exactly that folder, non-recursively. Each source carries its own fetch state — idle / loading / ready / error — shown as a dim + an in-place spinner on its header. That state is the CLIENT's; it is orthogonal to the source's connector liveness (live), which dims the same row for a different reason and says so in the tooltip. A listing that failed is not retried automatically; the header's refresh control retries it.

Converging on server truth. Refresh is deliberately more than a re-fetch. The plain source/root load is upsert-only — it merges what came back and leaves the rest alone, which is right for a partial or truncated response but means a row the server no longer has can never disappear through it. So the toolbar Refresh re-lists the visible folders of every hydrated source and applies absence semantics: an external-only file that is gone is pruned; a tracked file that is gone becomes a struck-through, still-revertible tombstone; a folder is pruned only when it is external-only and parents nothing live. A truncated listing never prunes (absence is not deletion there) and instead flags the source as possibly-stale on its header. Every mutation converges the same way, but scoped to the parents it actually touched.

Per-source refresh. Each source header carries its own refresh control, in the same slot the spinner uses. It works on a collapsed source too — an explicit click is a user intent, unlike the automatic event-driven loads, which never list a collapsed source — and it opens the branch so the result is visible.

When events go missing. The filesystem event channel is live-only: no replay, no cursor. Anything emitted while the connection was down is gone for good. The widget therefore re-converges automatically after a connection flip, which is what keeps Refresh a deliberate action rather than routine repair.

Search view

The Search sidebar view replaces the tree with a search surface. The query input rides INLINE in the sidebar header (title flips to "Search"; the toggle — or Escape — clears the query and closes even with text present). Three modes: Keyword (default — the indexed filter engine, term-frequency ranked), Semantic (embedding/hybrid), Grep (literal scan of a live connector scope; disabled when only brain:// is reachable). A separate filter row (filename glob, category, status, tags, created range, scope-to-folder) combines with Keyword AND Semantic; grep uses the filename + scope filters. Results are a single list with match badges [H]/[S]/[K], scores, grep uri:line, and per-mode loading/error states; backend error rows (e.g. grep scope guidance) render inline. The wire body is the tool vocabulary (search_mode) — pinned by test/ui/searchApi.wire.test.ts.

Git Control panel — source-control view

The Git Control sidebar view (a top-level sibling toggle beside Explorer and Search — sidebarView: 'tree' | 'search' | 'git' on the Files store) replaces the file tree in place with a source-control surface for one focused git repository. It shows a fixed header zone — a repo-switcher over the caller's accessible git REPOSITORIES (a root-repo source contributes one entry; a multi-repo source lists every nested checkout, lazy-fetched from GET /git/repos — the multi-repo model below; header + rows render the two-part slug:// Name label from git.repoName / the nested repo's name — dim-mono source coordinate + the repo's real name; slug-only fallback for older cached payloads), the current branch (a quick-pick to switch / create / delete), a collapsible commit box behind a canWrite-gated Commit toggle in the branch bar (default-collapsed so the change lists get the vertical space, staged-count pill on the toggle, open/closed state persisted component-local like the section prefs, Cmd/Ctrl+Enter in the textarea commits), and an always-visible tree/list toggle — over a VS-Code-style pane stack (the generic app/filesystem/ui/StackedSections.tsx): collapsible, drag-resizable sections with always-visible fixed-height headers (uppercase title + chevron + count + hover-revealed bulk stage / unstage / discard actions; trailing collapsed headers pile at the bottom, and each expanded body scrolls itself). Resizing is VS-Code-sash style: the section HEADER itself drags the boundary above it (pointer travel past a 4px threshold = resize; a short press stays a click-to-toggle — the toggle fires from pointerup, NOT the button's onClick, because pointer capture retargets the post-drag click; keyboard activation detail===0 keeps the button path), plus a thin dedicated separator strip above every section whose predecessor is expanded — including a COLLAPSED one, so a collapsed pane shows a grabbable divider. A section's header (and that strip) is a drag handle whenever the section directly above it is expanded: an expanded section resizes the boundary, and a collapsed section OPENS + sizes itself in the same gesture — so a collapsed pane is resizable without the open-resize-close dance (owner fixes, 2026-07-14). Two layout invariants keep collapsed headers where the user expects: expanded-body flex-grow factors are normalized so their sum is always ≥ 1 (growScaleForSum) — a CSS quirk otherwise leaves a lone small weight filling only a fraction of the pane and stranding collapsed headers mid-stack instead of at the bottom — and re-expanding a section dragged to the min-body floor RESCUES its starved weight to the default (MIN_USABLE_WEIGHT). Gestures starting on the hover actions never drag (data-section-actions guard). Each change row is the VS-Code layout: a shared file-type icon LEFT (the same pickFileIcon table as the Files tree, extracted to app/filesystem/lib/fileIcon.tsx), the filename (always visible), the dimmed dir path (truncates FIRST), and the status letter RIGHT — the per-row Stage/Unstage/Discard cluster is a hover-revealed ABSOLUTE overlay that reserves no width. The THREE sections: Changes (always renders, empty-state text at 0 — and INCLUDES untracked files as a green U, VS-Code parity: untracked rows get Stage only, no Discard), Staged Changes (vanishes when empty), and Graph (always present, collapsed by default, defaultWeight: 2 so the first expand is usable without an immediate resize) — the commit DAG renders INSIDE the panel; the former FileEditor Graph tab ('git-graph' / gitGraphSource) is REMOVED. Section {collapsed, weight} prefs persist to component-local localStorage (neuralis.filesystem.gitPanel.sections, same guards as the sidebar width); collapsing a section unmounts its body, so the graph only fetches while open. The graph itself is scroll-paged and virtualized: 200-commit pages append as an IntersectionObserver sentinel nears the bottom (skip + the exact hasMore probe, de-duped by sha so a concurrent commit shifting the pages produces a dropped duplicate, never a gap; a load that leaves the sentinel still visible re-pages on its own — the observer only reports status changes — and a FAILED fetch keeps the already-loaded pages behind an error strip instead of wiping the graph), and rows render through @tanstack/react-virtual as fixed-height per-row SVG slices — the pure gitGraphLayout pre-splits every lane line and fork/merge curve at the row edges so the DOM stays bounded on unbounded histories. It also live-refreshes off the existing git-badge SSE with no new channel: the debounced badge path bumps a store gitLogNonce for the FOCUSED repo only, and the graph re-fetches its head window and REPLACES the loaded prefix (decorations move on checkout, so prepending new SHAs would go stale); the graph's own header refresh button forces the same head-window path and is THE graph refresh (the old Graph section-header remount button was dropped as a duplicate). Graph rows are commit drill-ins: the rows render flush (no card background) to fill the width; hovering a collapsed row floats a full-message card (subject + the full commit body — %b, carried on GitGraphCommit.body, a NUL-terminated logGraph record so a multi-line body is parsed safely, server-capped — plus sha/author/relative-date), anchored to the graph ROOT and pointer-events-none so it never steals the hover. Clicking a row expands it inline (the virtualizer switches that row to dynamic measureElement height; collapsed rows keep the fixed-height fast path) with the body + sha/author/relative-date and the commit's file list — lazy-fetched ONCE per sha from GET /git/commit and cached client-side; a file row opens the commit↔parent side-by-side diff, and a hover Open Changes action (an absolute overlay) opens the multi-file stacked tab (GitCommitChangesView, one lazy diff card per file — the PendingChangesPanel pattern). Outgoing indication (LOCAL refs — the network reach is the remote-ops popover below): BLUE is reserved as the UNPUSHED language — commits not yet on the upstream render a HOLLOW blue dot (OUTGOING_COLOR; the HEAD ring follows the dot color; pushed dots keep their lane color, and the lane palette moved blue to its LAST slot so an early fork lane can't collide with it), an expandable "Outgoing changes" divider row (blue family) sits above the first outgoing run (expanding shows the aggregated merge-base↔HEAD file list from GET /git/outgoing; its Open Changes opens the same stacked tab over the range), ref chips are explicitly classed (HEAD/local = violet filled — violet stays the local-ref language, origin/* = outline + cloud glyph, tags = amber), and the branch bar shows ↑ahead ↓behind chips whenever /git/status carries an upstream — per-pair arrow/number spans, the ahead pair blue (it counts exactly the outgoing commits), the behind pair neutral. For a writer (canWrite) the chips ride inside the fetch/pull/push trigger button (a sync glyph when there is no upstream); clicking it opens the remote-ops popover — Pull / Push / Fetch when the source grants uri-policy exec (canExec), otherwise a precise "grant exec" affordance. A read-only caller sees the display-only chips. No upstream ⇒ the chips don't render (the trigger stays for a writer). Clicking a change row opens a side-by-side diff (below). The focused repo is the store PAIR (activeSource, activeRepoRel) — the source slug plus the nested checkout's dir name ('' = the source root — the multi-repo model below) — never a second slug concept; the graph derives its source/repo props from it and remounts on the pair, so pages/decorations never leak across checkouts. (Agent-facing git tools + the PendingInteraction approval ladder are a SEPARATE later increment — the panel here is the HUMAN face.)

Source-level git field. Source rows now carry an optional git?: { isRepo: boolean; repoName?: string; repoNames?: string[]; branch?: string; canWrite?: boolean; containsRepos?: boolean }, present when the source's LOCAL root is a git checkout — or, for a multi-repo source, stamped { isRepo: false, containsRepos: true, repoNames? } when a NON-repo root contains nested depth-1 checkouts (that row carries NO branch/canWrite — per-repo state lives on GET /git/repos). It drives a small GitBranch overlay + a repository-NAME hint on the source header in BOTH the file tree (SourceBranchHeader) and the Sources config panel (SourcesPanel), and populates the Git Control repo-switcher. repoName is the repository's REAL name — the git remote origin's basename (repoNameFromOrigin, mount-independent: a checkout mounted at /mounts/repo still reads Neuralis, not repo), falling back to the checkout dir basename (repoNameFromRoot) for a no-remote repo / linked worktree; a NAME only, never the URL/host/org (scrubHostPath discipline). The file tree shows the repository NAME(s) beside the source name, not the branch (a source can hold several repos; the branch lives in the Git Control panel): a root-repo source its repoName, a containsRepos source its nested repoNames (the checkout DIRS, first two + +N). Those nested names are per-path READ-gated server-side in BOTH summary builders (filterVisibleRepoNames, resolvePermissions(<source>://<rel>/).read — "per-file uri-policy wherever file NAMES are revealed"; a nested checkout the caller cannot read is absent). The /git/repos route applies the SAME filterVisibleRepoNames per-path gate (tree and route are unified — no name residual is left), keeping only the root rel:'' row unconditionally. repoName also sits behind the two-part slug:// Name label on the panel header, switcher rows, and graph header; absent (older cached payloads) ⇒ slug-only fallback. It is orthogonal to the sync tone — a source is simultaneously synced (blue) AND a git repo — rendered alongside, never replacing it. The field is CHEAP and bounded O(1)/source (a .git stat + an O(1) .git/HEAD branch read via getSourceGitSummary, TTL-cached — never a whole-repo status on the list path) and is stamped in the two source-summary builders: FilesystemService.list's filteredSources (the tree) and GET /sources (the config panel), both AFTER the source-level access filter so a non-member never infers a project's git repos. dirty stays deferred off the list path; ahead/behind now ride the ON-DEMAND panel status (below) — the source-ROW summary itself stays O(1) and tracking-free. Refreshing the remote-tracking refs (fetch + credentials) is the remote-ops surface above.

Multi-repo sources — nested checkouts. A source does not have to BE a checkout to be a git surface. The model: root .git wins — a source whose root IS a repo behaves exactly as before (one repo, no scan); only when the root is NOT a repo does brain-core run a depth-1 discovery (discoverSourceRepoRoots, src/git/sourceGitControl.ts): direct children with their own .git, found by a STREAMING fs.opendir scan (≤ 500 dirents examined — a 100k-child source never pays a full readdir; ≤ 25 repos reported, name-sorted), zero git spawn, its own 30 s TTL (separate from the 2.5 s summary TTL — discovery carries no branch payload and checkout creation is rare; deletion is covered by the route's fresh re-check). This ONE discovery cache feeds three consumers — the source summary's containsRepos + repoNames, the GET /git/repos route, and the watcher expansion. Containment is enforced AT DISCOVERY, not only in the route ladder (the load-bearing rule): hasDotGit uses stat, which FOLLOWS SYMLINKS, and two consumers — the GET /git/repos listing and the watcher expansion below — consume the discovered set WITHOUT ever passing the route ladder, so a symlinked child pointing at a foreign repo would otherwise leak that repo's existence + branch and plant a host-side watcher on a foreign .git. Every candidate must therefore realpath-resolve inside the source root — the child dir AND its .git entry (a contained child whose .git is itself a symlink onto a foreign repo is dropped the same way; a regular .git dir or a linked-worktree .git FILE stays admitted). The containsRepos summary probe applies the SAME predicate with a stop-at-first-find short-circuit — the ONLY new list-path cost, fail-soft. Residual: a .git FILE whose gitdir: line points outside the root would need a git spawn to detect (banned on this zero-spawn path) — a known, logged residual.

Every git/* route then accepts an optional repo selector (query for GET, JSON body for POST) naming ONE discovered child checkout; a root-repo source rejects any repo. The shared resolver validates it as belt AND braces — syntactic whitelist (one path segment, 1–128 chars, no ./../.git/leading ~; a malformed value is a request-shape 400 BEFORE any access check, uniform whether or not the source exists) → membership in the TTL-cached discovered set (no arbitrary-subdir probing, even syntactically valid names) → a FRESH .git stat + realpath containment re-check on the nested root AND its .git target (closes discovery-TTL races and case-aliasing) — and every failure past the belt maps onto ONE constant 404, never revealing whether the child exists. With repo set the git ops re-root at the nested checkout and every policy URI carries the <repo>/ prefix, composed ONCE in the resolver (policyPrefix) so no gate site can drift: root-gated routes gate <slug>://<repo>/ (trailing slash — the exact root form a <repo>/** path rule matches, pinned by test), per-file routes gate <slug>://<repo>/<path>, and guardWritePaths prefixes EVERY gated write path — the whole-file paths[] AND the hunk-extracted ones (a +++ b/<path> patch header gates as <repo>/<path> through the nested form). Without repo everything is byte-identical to the single-repo behavior.

GET /git/repos?source=<slug> is the repo-picker listing: { rel, name, branch?, canWrite, canExec } rows — the realpath-FILTERED discovery, branch-enriched per nested root, plus a per-repo canWrite (the two-plane gate evaluated at <slug>://<repo>/ — root-level write capability is the wrong granularity in both directions: a <repo>/** write-deny flips that row read-only while siblings stay writable, and a per-repo allow is not hidden by a stricter root) and a per-repo canExec (the uri-policy exec capability at the same granularity — whether the remote push/pull/fetch ops are enabled, so the panel renders the popover or the grant-exec affordance without a click-then-403). Names-only ⇒ per-path READ-gated (filterVisibleRepoNames, <slug>://<rel>/.read — keyed on the DIR, never the display name, which is the origin basename when the checkout has one, the same order as repoName): a nested repo whose name is read-denied is DROPPED from the listing — the SAME gate the file-tree repo-name stamp applies, unified onto this route (resolveGitSource runs with ignoreRepo, so the listing spans all nested repos of the source, each then name-gated). The root repo (rel:'') is ALWAYS kept and source-root-gated (a <slug>:/// form would mis-gate it), so the ONLY residual is a fully read-denied ROOT-level repo still listing its own name + branch (source-root name-level exposure, the same family as /git/status). Rows carry rel/name only; the server-side nested root (a host path) is never serialized.

The .git badge watchers expand per nested repo: SyncWatchService derives its git-watch desired set from the FILTERED discovery — one .git watcher per nested checkout of a containsRepos source (root-repo sources keep exactly one), so external commits in ANY nested repo refresh the panel/graph live. The badge event stays SOURCE-scoped (an in-source over-refresh is fine, never a leak), and cache invalidation is already exact (the TTL caches key by resolved root, so a nested-root write invalidates only that repo).

git.canWrite — the write-UI visibility gate. canWrite is computed SERVER-side per-caller as the SAME two-plane gate the write routes enforce: the caller HAS the files.write FEATURE and the caller's RESOLVED uri-policy write on the source root <slug>:// is allowed (resolveGitCanWrite, deny-by-default). The panel shows the Stage/Unstage/Discard/Commit/branch-write affordances IFF git.canWrite === true — or, with a focused NESTED repo, IFF that repo's /git/repos row carries canWrite: true (the per-repo granularity above; the root row's flag is not consulted). This must NOT be derived from SourceInfo.permissions.write (the source's DEFAULT permission block) — that hid the owner's write UI on a default write:false source where the owner gains write via byRole/byUser, and would also show buttons to a files.read-only role. The server route is the deny-by-default floor regardless of this UI gate.

The git/* routes (src/routes/git.ts, file-level feature='files.read'):

  • GET (read, files.read): status?source=<slug> (whole-repo change-set + current branch, plus upstream/ahead/behind when the branch tracks an upstream — all three absent otherwise), branch?source=<slug> (local branch list, for the switcher), diff?source=<slug>&path=<repoRelPath>&(staged=<0|1> | commit=<sha> | base=<sha>&head=<sha>) (the unified diff for ONE file — three MUTUALLY-EXCLUSIVE modes: worktree WT↔Index / Index↔HEAD, commit↔FIRST-parent (a root commit diffs from the empty tree), or an explicit sha pair (the outgoing per-file form — the commit mode composes to it internally); 400 on any combination or half pair, every sha whitelisted before the engine), log?source=<slug>&maxCount=&skip=&ref= (ONE page of the commit DAG with parent SHAs for the SVG graph — maxCount is the page size, default 200, clamped to 1999 (one below the 2000 engine hard cap so the server's maxCount+1 probe always fits); skip (≤ 100 000) offsets the page from the tip — a skip PAST that window answers hasMore:false (the clamp is the END of the pageable window, never an infinite repeat of the deepest page); the response's hasMore is otherwise an EXACT next-page probe, not a heuristic — it replaced truncated on this route only; plus outgoing?: string[], the not-yet-pushed @{u}..HEAD sha set, newest first, capped at 500, ABSENT — not [] — when the branch has no upstream), commit?source=<slug>&sha=<sha> (the files ONE commit changed as {path, status, renamedFrom?} rows, FIRST-PARENT semantics; an unknown sha is a typed {ok:false, code:'unavailable'}, never a fake empty list), outgoing?source=<slug> (the aggregated committed-but-not-pushed file list + the resolved {baseSha, headSha} pair; the server resolves BOTH refs ITSELF, the route takes NO refs, and baseSha is merge-base(@{u}, HEAD) — never @{u}, which would phantom remote-only files as deletions on a diverged branch; no upstream → {baseSha:null, headSha, files:[]} fail-soft; LOCAL refs only, zero network — staleness is "since the last fetch", and the network reach is the fetch/pull/push remote POSTs), and show?source=<slug>&path=<repoRelPath>&rev=<sha|index> (the blob CONTENT of one file at a commit sha or the index — the diff-expander data source; blob-ONLY with typed refusals, below), and repos?source=<slug> (the in-source repo-picker listing {rel, name, branch?, canWrite}; names-only, source-root read gate + per-nested-name filter — see the multi-repo model above).
  • POST (write, additionally files.write-gated): stage ({paths[]} whole-file add OR {hunk} git apply --cached), unstage ({paths[]} reset -- <p> OR {hunk} reverse-apply), discard ({paths[]} restore WT from index; typed rescue? returned), commit ({message, amend?}), branch ({action:'create'|'delete', name, startPoint?, force?}; delete returns a typed rescue?), checkout ({ref} — PLAIN switch, no -f; git refuses a clobber → 409), and restore ({rescue:{kind,ref,name?}} — one-click UNDO: git stash apply / git branch <name> <ref>, re-snapshots first).

Every route resolves the source deny-by-default (non-accessible → 404, never a leak) and returns repo-relative paths only (never host / os_uri paths); git op-error messages are host-path-scrubbed. Responses are size-capped for MEMORY/response bounding, with a truncated flag on /git/status + /git/branch + /git/diff (/git/log instead PAGES — skip + hasMore, above) — a TWO-cap split: the human-panel /git/diff cap is 2 MB (MAX_DIFF_BYTES_UI, whole-line truncation; the confidentiality boundary for these routes is the per-path read policy, not the cap), while the boundToolResult 20 KB floor stays the MODEL-facing tool-result invariant for the planned hosted git tools. A diff past the engine's ~4 MB output accumulation bound is KILLED inside GitRunner (there is no serveable prefix), so /git/diff returns a typed {ok:false, code:'too_large'} refusal on a 200 (the /git/show dialect) instead of an empty diff that would render as a false "no changes" for a file the panel lists as Modified. diff (every mode — historical content is content)/show/stage/unstage/discard gate the uri-policy check on the SPECIFIC file (<slug>://<path>) so a per-path deny is honored, where status/branch/log/GET commit/outgoing/POST commit/checkout gate the checkout root (the GET commit/outgoing file lists are NAMES-only — the names-only dialect, consistent with /git/status). With the multi-repo repo selector BOTH forms carry the <repo>/ policy prefix (root-gated → <slug>://<repo>/, per-file → <slug>://<repo>/<path> — see the multi-repo model above). Names-only residual (documented, not yet closed): a per-path read-DENIED file's NAME — including historical names — appears in the root-gated listings (status, the commit/outgoing file lists, log decorations) while its CONTENT 403s on the per-path-gated diff/show; closing the names must close status+commit+log-decorations together. Diff direction: a Changes row is WT↔Index (staged=0), a Staged Changes row is Index↔HEAD (staged=1); an UNTRACKED (brand-new) file has no index entry so its WT↔Index diff is empty — it is rendered fully-added via git diff --no-index -- /dev/null <path> so the whole file shows as added. Whole-repo status/log are O(repo) so they fire ON DEMAND only (panel open / repo select / graph open), TTL-cached, for the SELECTED repo — never the fs_list hot path. The panel readers split their inactivity timeouts two-tier: status/branches keep 5 s, while the HEAVY readers (diff / log / show / commit files / outgoing) get 15 s so a 2 MB diff or a 2000-commit log on a slow volume isn't killed early (both tiers cap silence, not wall-clock — see the engine section above). GIT_OPTIONAL_LOCKS=0 (set in GitRunner.sanitizedGitEnv()) makes read queries truly index-read-only, closing the badge → re-list → git status → index-rewrite watcher feedback loop (see the live-badge note below).

GET /git/show — blob-only, refuse-don't-truncate. The diff-expander data source returns the CONTENT of one file at a rev. rev is whitelisted at the route BEFORE the engine (the literal index or a 7–40 lowercase-hex commit sha; everything else is a 400 — defense-in-depth ahead of the engine's own ref guard). It never truncates — refusals come back typed (ok:false) on a 200, fail-soft, so the client degrades expanders instead of erroring: not_a_blob (a directory target would print a tree LISTING — an embedded name-listing side door around the per-path read policy), too_large (> 2 MB, size-checked via cat-file -s BEFORE buffering the content), binary (NUL in the head), unavailable (non-repo / path missing at the rev). The result is deliberately UNcached — a stale blob would corrupt expanded diff context.

Write model — human consent + SERVER unconditional auto-snapshot. The write POST routes are the HUMAN face of the git engine: the caller's authenticated session IS the consent (files.write feature + uri-policy write), NOT the PendingInteraction agent-approval guard (that is the additive AGENT face, a later increment; both faces drive the same GitRunner mutating methods). Before ANY destructive op (discard, branch delete) the SERVER UNCONDITIONALLY snapshot()s a reflog-recoverable rescue point (a refs/nrs-rescue/<label> anchor + a git stash create of working-tree+index content) — never gated on a client confirmed flag (the UI confirm dialog is UX-only; a client can POST /git/discard directly). The op returns a TYPED, CONDITIONAL rescue carrier — {kind:'stash', ref} on a dirty-tree discard, {kind:'branch', ref, name} on a resolved-tip branch delete (absent when there is nothing to recover) — which drives the one-click UNDO: POST /git/restore {rescue} re-snapshots first (a restore is itself a mutation) then git stash apply / git branch <name> <ref> (non-force → 409 on an existing name), same files.write + uri-policy write floor, no PendingInteraction. The panel's rescue banner grows an Undo button beside Dismiss. Every successful write publishes the IN-APP changeType:'badge' event with the actor's REAL (userId, projectId, agentId) via publishInAppGitBadge (the ONLY in-app publish path — an empty userId would broaden delivery) so the panel + badges update live through the ordinary user/agent-equality SSE filter.

Side-by-side diff-on-click — full content + hidden-region expanders. Clicking a change-set row (or a file inside an expanded commit / the outgoing divider) opens a diff as a FileEditor view (tab id git-diff, driven by the gitDiffTarget store descriptor — a discriminated union mirroring the route's three modes: {source, path, staged} working tree, {source, path, commit} commit↔first-parent, or {source, path, base, head} the explicit outgoing pair — because a bare activeWorkspaceTab id can't carry the target). GitDiffView is a THIN adapter: the fetch-and-render core is the prop-driven GitFileDiff (app/filesystem/ui/GitFileDiff.tsx, mode vocabulary in the pure gitFileDiffMode.ts), shared with the multi-file card view below so a diff fix lands in both surfaces at once. It fetches GET /git/diff, parses the unified diff with react-diff-view (MIT, no network — it only parses the diff string), and renders a split (side-by-side) view of the WHOLE change: the diff is size-capped by the SERVER alone (the 2 MB MAX_DIFF_BYTES_UI route cap — the ONE cap owner; the client keeps no line/byte re-cap and only renders the server truncated flag as a banner). Unchanged regions between hunks collapse into clickable "⋯ N hidden lines" expander rows (per-gap, plus an "Expand all" header button); the omit-gutter stripe is transparent — the expander rows ARE the affordance. Expansion lazily fetches the NEW-side full source on first click (unchanged gaps are identical on both sides), routed per mode: an unstaged diff reads the working-tree file via the read API, a staged diff fetches the index blob via GET /git/show?rev=index, a commit diff fetches rev=<sha> and a range diff rev=<head> (never <sha>^ — the show route rejects it); the result is cached per (source, path, rev) and any refusal (not_a_blob/too_large/binary/missing file) degrades the expanders to static "N hidden lines" text — fail-soft, never an error banner. Staging renders in the WORKING variant ONLY — a commit/range (historical) diff never shows stage buttons; the gate is the mode variant, never staged truthiness. Hunk staging rides a DUAL-MODEL (app/filesystem/ui/gitDiffExpand.ts): the raw server diff is parsed ONCE into ORIGINAL hunks — the sole staging source (the same raw text feeds gitDiffPatch) — while expansion produces a separate RENDER-ONLY hunk list (expandFromRawCode MERGES and REWRITES hunks, so rendered indexes stop mapping 1:1 onto the originals); stage/unstage buttons anchor by ORIGINAL-hunk identity (old-range containment — a rendered hunk that swallowed N originals renders N buttons), so the staged payload is identical before and after any expansion. On a server-TRUNCATED diff both staging AND the expanders are disabled — a cut final hunk would apply wrong, and gaps computed from the VISIBLE hunk headers would fabricate "unchanged" rows over regions the dropped hunks own. The server's typed too_large refusal (>4 MB engine-killed diff) renders as a dedicated too-large state, never the "No … changes" empty state; the flag states reset on every target switch so a failed fetch can't inherit the previous target's banner. FileEditor's showFilePane guard EXCLUDES the git views (&& !showGitDiff && !showGitCommit) — otherwise the file-content layer renders ON TOP and blocks the pane (the same overlay trap the Pending view hit).

Multi-file "Open Changes" tab (GitCommitChangesView). The second git FileEditor view (tab id git-commit, descriptor gitCommitTarget — {kind:'commit', sha, subject} from a graph row's Open Changes action, or {kind:'outgoing', base, head} from the outgoing divider's) renders one commit's — or the whole outgoing range's — change-set as a vertical stack of collapsible per-file diff cards (the PendingChangesPanel pattern): the file list comes from GET /git/commit / GET /git/outgoing, and each card mounts the SAME GitFileDiff (card variant) on expand only — conditional render, so a collapsed card fetches nothing. Every change-set starts logically expanded; only visible expanded cards mount and fetch through the shared content lanes (three at a time, answers cached per scroller), including histories above 200 files. A per-path read-DENIED file renders a fail-soft LOCK row inside its own card (the per-file diff gate is per-file; the names-only list is root-gated — the names-only dialect above), never a view-level error. Clearing the descriptor (pill close) falls the active tab back to file — the store centralises the fallback so a git-commit tab id is never stranded without a target — and BOTH git targets reset on an agent/project switch.

Live changeType:'badge' SSE channel + .git/HEAD watcher. The previously-dead changeType:'badge' filesystem-event channel is now LIT so git badges + the panel + the branch indicator update live. A per-checkout GitWatchRegistry (src/git/GitWatchRegistry.ts) — reconciled over the DISCOVERED checkout set: one watcher per root-repo source, one per nested repo of a multi-repo source (see the multi-repo model above) — watches each checkout's .git/ root for external git ops (a terminal git commit/add/checkout the sync watcher can't see) and publishes a source-scoped badge on the per-project event bus (git-state only — it NEVER feeds the vector index / sync pipeline; the .git/-sync-ignore floor is unchanged). It acquires the .git directory as a SEPARATE FsWatchHub root from the source root (a second ignored predicate on a shared root is inherited, not applied — so the source watcher's .git-ignore would otherwise blind it), reacts NARROWLY to HEAD/packed-refs/refs/**/ index, and prunes the high-churn objects/+logs/ subtrees; it is reconciled + torn down by SyncWatchService's per-source lifecycle. On the client the changeType:'badge' hook branch (useFilesystemSSE.ts) is an EARLY short-circuit (before incrementFolderBadges/upsertFileNodeFromEvent) that debounces + re-lists the source subtree (recomputing FileNode.git) and refetches the panel status + SourceInfo.git. Changes-section live stream. The badge watcher above only fires on .git ref/index moves — so Staged streams (staging writes .git/index) and the graph streams (commits write .git/HEAD), but a plain WORKING-TREE edit (a new untracked file, an unstaged modification) touches neither and emitted NO badge, leaving the Changes section stale until a manual Refresh. Now useFilesystemSSE also refetches the FOCUSED repo's /git/status from the two working-tree event branches (the external:true reconcile branch + the per-file created/updated/deleted/reverted switch), focused-source-gated + per-source trailing-debounced (gitStatusSeq drops out-of-order responses) — so Changes streams too. Badge delivery is an AUTHORIZATION decision, not a filter tweak — the external watcher's badge carries NO acting user, so subscribeFilesystemEventsImpl has a dedicated changeType:'badge' branch that authorizes delivery by SOURCE-ACCESS (project gate + payload.source ∈ the subscriber's accessible-source set, precomputed once at subscription), deny-by-default, NEVER a blanket project broadcast — a user/agent-scoped source's badge stays scoped to its owner. IN-APP badges (published by the write routes via publishInAppGitBadge with the actor's REAL identity) skip that branch and reach the actor through the ordinary user/agent-equality filter, unchanged.

Public Surface

Lifecycle exports (src/lifecycle.ts):

  • init, start, onBootstrapComplete, drain, stop, api, onCredentialChanged, health, onPrincipalRevoked, provisionProject (usecases/provisionProject.ts — a NEW project's declared seed:'auto' sources, written complete through the same create path as the boot repair; see Declared sources), deprovisionProject (usecases/deprovisionProject.ts — on a PERMANENT project delete, ALL of the project's vector points leave the shared index in ONE bounded deleteByFilter({ must: [{ field: 'projectId', equals: id }] }) — chunks + folder-ancestor points covered by the same keyword-index filter. MUST-HAVE: a failed delete throws, and so does a degraded boot where Qdrant is configured but the in-memory index is wired (a delete there would leave every real point in Qdrant); the host then stops the purge before any file is removed. It runs BEFORE the fs tree is removed, so resolving the brain infra cannot re-materialize the project dir).
  • init state exposes services["package-source-roots"] for kernel registration; its listPackageSourceRoots delegate is the same function as the direct API method, and its subscribePackageSourceRoots(listener) calls listener(projectId) after every committed source-config write of the app root (SourceConfigMutation.onChanged — fired outside the write lock, a no-op write never fires, a throwing listener is logged and never reaches the writer), so the host re-derives that project's flagged package roots at once.
  • BrainCoreApi — getProjectServices, getProjectInfra, getFilesystem, upsertSourceConfig, listShellSourceRoots(projectId) (every shell-capable source with its persisted permissions; feeds agent-core's shell uri-policy gate and the terminal's host-tab resolver; NOT access-filtered — consumers evaluate the policy against the caller's own SessionContext. Rows carry an explicit plane: container sources contribute their containerRoot, resolved by the canonical resolveLocalRoot; host-plane sources contribute a raw hostRoot and an intentionally EMPTY containerRoot — no container path exists for them, and the empty value is load-bearing, because the consumer's container-plane filter drops rootless entries so a host source can never leak into a container-plane lookup even if plane were ignored. The plane is derived from the connector KIND, never from the slug string — an ordinary local source may legitimately be named host-notes. Kinds that host no shell at all (brain vectors, webtop) contribute nothing), and listSourceRuntimeSummaries(projectId, { userId, agentId, role?, grantedFeatures? }) (the READ sibling of listShellSourceRoots, feeding agent-core's <runtime_stack> source rendering — all connector kinds, access-filtered (host-plane rows carry containerDir: null — their root is a path in the OPERATOR's filesystem with no container equivalent, and the local-shaped derivation would otherwise print a container path that does not exist, baked into every agent's cached system prompt) via canAccessScope → files.readOthers/deny-by-default, each row carrying connector, os_uri/container_dir, the caller-effective uri-policy, description, and the cached index snapshot; reads persisted source-config JSON ONLY — no live walk / no vector count — so it is safe to call once per stream. Agent-core renders the stable structure in the cached <runtime_stack> prefix and the volatile counts + on/off in <workspace_live>. Disabled sources are INCLUDED, marked enabledEffective:false). listPackageSourceRoots(projectId) (enumerate every project-associated source as a runtime package-source candidate { slug, uriRoot, containerRoot, scope, recognizesPackages, enabled, kind }; identity-free like listShellSourceRoots, ALL candidates returned so the host applies the floor — project-scope + flagged + enabled + local; the host's projectPackages consumes it to scan + hot-reload flagged source dirs alongside _packages/). Filesystem events reach the /brain-core/events SSE route and the discovered filesystem channel of the host /api/events hub through ONE authorization adapter: it takes the real session, authorizes EACH requested agent deny-by-default (a foreign agent yields zero events, never an existence-confirming error), and filters events by user/project/types and the authorized agent or source.

Cross-package writes go through FilesystemService itself, and its permission check is the ONLY gate on that path. getProjectServices(projectId).filesystem hands back the live service, so search / read / list / write / writeUpload are all reachable from another package via getPackageApi() — and such a call skips the HTTP route layer entirely, meaning the route-level assertUriCapability never runs for it. Every one of those methods opens with #checkPermission(ctx, uri, …); that is what stands between a cross-package caller and the filesystem. Two consequences for anyone mirroring the request types into another package (agent-core does, in contributions/brainApiTypes.ts — it cannot import brain-core, which would be a cycle, so that mirror is pinned instead by a compile-time guard against brain-core's shipped .d.ts): the mirrored type must expose neither review nor allowEmpty — both are INTERNAL-only governance escapes deliberately absent from every tool schema and route body — and the caller must gate on its own feature (files.write) before it gets here, because #checkPermission answers "may this URI be written", not "may this caller write at all".

The reverse direction is a real type edge, not a mirror. brain-core reaches agent-core in two places — routes/git.ts for the caller's own git remote binding and the lifecycle.ts verifyAgent closure — and each imports the canonical AgentCoreApi (import type, so no runtime edge is added to a package agent-core does not load). The ?. stays on the API OBJECT, because getPackageApi answers null when agent-core is not loaded and every site keeps its own "unavailable" arm; it is GONE from the members, which are required on AgentCoreApi. A member agent-core renames is now a brain-core compile error instead of a silent undefined. Type-only use means no brain-core declaration surfaces an agent-core type — dist/**/*.d.ts carries no @neuralis/agent-core reference.

Source-package discovery (listSourcePackages)

FilesystemService.listSourcePackages(ctx) (reached via getProjectServices(projectId).filesystem) lists source-contributed packages from the vector index — the discovery half of the source-package pipeline (agent-core consumes it per stream). The service is a thin delegate: the pass itself is src/usecases/listSourcePackages.ts, and the raw inventory it shares is src/usecases/sourcePackageInventory.ts. Four invariants:

  1. No connector I/O and no OS walk. The pass is index-only: pass 1 scrolls the bounded structural trigger categories (skill, instruction, rule, agent, package) project-wide; pass 2 fetches docs for every detected package root and the skill-bundle siblings of every detected skill through the keyword-indexed folder_ancestors field. Content hydration is the caller's job (per-stream, after gates).

    Pass 2 is batched, not per root: one paged folder_ancestors matchAny(active roots) query answers every root at once, and a root whose raw cap is full leaves the active set so later pages stop carrying it. Three properties of that shape are load-bearing — the raw cap stays per root and before the privacy/policy filters, exactly where the per-root query used to stop paging; a point under two NESTED roots is offered to BOTH (the nearest-root rule then decides); and the cursor is a point ID scanned in ID order, which is the only reason narrowing the filter between pages is safe.

    Every pass projects its payloads (uri, category, agent_owner, manifest_usable, frontmatter, os_uri, content_hash, plus folder_ancestors in pass 2) — never content. A manifest's display name comes from its own bounded batch, for the manifests the caller may actually read. Measured on 70 skills across 8 roots: 79 backend round-trips → 5, and 1.44 MB of payload → 88 KB.

  2. Category-driven grouping. The sync indexer assigns the trigger categories (the ONE shared categorizeContributionPath table in @neuralis/package-system/source, kept only for files the ONE shared admitsContribution rule admits — a filename-identity form, or frontmatter id/name; an undeclared agents/references/*.md is a plain file); the query groups the trigger nodes with the ONE shared detectPackageRoots rules module (usable-manifest anchor → marker dot-dir (.claude, .cursor, …) → category-dirs root → bare-skill dir → RESTRICTED implicit source-root). Grouping is query-time and in-memory — layout changes self-heal on the next call, no payload migration.

  3. All caller scoping is post-filter. The vector filter is projectId-only — index points carry the SYNCING identity, a userId equals filter would under-report. The caller is scoped by: #listAccessibleSources (enabled + scope + source-level read), the source's persisted uri-policy evaluated per package root AND per file (denied ⇒ absent — member non-leak extends to file and bundle-member names), and the agent_owner privacy marker (data://agent-core/<agentId>/… subtrees belong to that agent only). A source without a persisted config is skipped (fail closed). An absent ctx.role returns [] (weakest interpretation — never a 'member' default).

  4. The shared inventory holds RAW index points, never a caller's answer. Root detection runs AFTER the agent_owner filter and the accessible-source set, so the roots, the raw caps and the package tree a privileged caller produces are not the ones a member produces — freezing any of that into a shared entry would hand one caller's view to another. Every pass therefore re-reads the persisted source config and re-evaluates source enable, enabled:'auto', scope, role/priority and every uri-policy row; a revocation bites on the NEXT request, including one issued while a scan is in flight. Discovery deliberately bypasses the brainAccessibleCacheTtlMs window that the hot list/read paths use, in both directions — it neither reads that cache nor seeds it.