shockwave
CLAUDE.md192 starsChanged 4 months agoMITView on GitHub
The codebase is TS with a deliberately permissive posture: strict: true but noImplicitAny: false in tsconfig.json. Conversions favor "kill .jsx extensions" over fully-typed annotations — many components use any for state. Treat that as the migration baseline, not a target. checkJs is on and there are no unchecked source files left. src/, agent-core/ and api/src/ are TypeScript end to end; the only .js-family file is src/preload/preload.cjs, which is CommonJS because Electron's preload must be. Two gates, because esbuild strips types…
In this repository7 other files it gives its agents.
- agent-core/CLAUDE.mdCLAUDE.md
- api/CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- .github/workflows/CLAUDE.mdCLAUDE.md
- tests/CLAUDE.mdCLAUDE.md
- electron-devSkill
- shadcnSkill
The file
We split this CLAUDE.md into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 31
Every tests/*.test.js import names .ts (from '../src/renderer/linkIndex.ts').
- Very specific: line 93
Avoid: "page", "note", "document" (for .md files), "vault" (in new code/copy), "internal link" (call it a wiki-link), "session" (for a …
- Very specific: line 99
Link-index sync: Create/change → linkIndex.updateFile or applyParsedLinks.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
npm run dev— start electron-vite (electron-vite dev --watch --remoteDebuggingPort=9222). Builds main + preload, serves the renderer on :5173, launches Electron, and auto-reloads on anysrc/**change. CDP for the renderer is exposed on :9222.npm run build— production build toout/(main, preload, renderer).npm start—electron-vite previewagainst the production build.npm run dist— build thenelectron-builder(produces dmg/nsis/AppImage perbuildblock inpackage.json).npm test— run the test suite (node --test "tests/**/*.test.js", no install needed).npm run lint— ESLint oversrc/**+tests/**(config ineslint.config.js).agent-core/andapi/src/are not linted — they're covered by the two typecheck gates instead.npm run typecheck—tsc --noEmit(permissive TS; see below). This does not coverapi/— that tree has its own gate, below.npm run cli-tools— install the bundled CLIs the agent shells out to (cli-tools/, see below) and clean npm's self-link. A separate top-level step, never a postinstall: nested insidenpm ciit dies with EPERM on the Windows runner.npm run ui:diff— read-only upstream drift report for the vendored shadcn components (npm run ui:diff -- buttonfor one). See "Reusable UI primitives" insrc/renderer/CLAUDE.md.- Companion (
api/) is a separate server with its own scripts + Docker deploy — seeapi/CLAUDE.md. Local dev:cd api && docker compose up -d --build(postgres + api + traefik), or barenpm run build && node dist/server.js.cd api && npm run typecheckis its type gate — the root one can't see it (nothing undersrc/importsapi/), and esbuild bundles without checking. Schema tooling:npm run db:push/db:generate(drizzle-kit).
TypeScript
The codebase is TS with a deliberately permissive posture: strict: true but noImplicitAny: false in tsconfig.json. Conversions favor "kill .jsx extensions" over fully-typed annotations — many components use any for state. Treat that as the migration baseline, not a target.
checkJs is on and there are no unchecked source files left. src/, agent-core/ and api/src/ are TypeScript end to end; the only .js-family file is src/preload/preload.cjs, which is CommonJS because Electron's preload must be. Two gates, because esbuild strips types without checking them in both builds: npm run typecheck at the root (covers src/** plus everything it imports, which pulls in agent-core), and npm run typecheck in api/ (covers api/src/** plus agent-core again, against the companion's own dependency versions). Neither build checks anything — run both.
How imports are spelled — two rules, and the second one is not optional
Vite has an extensionAlias (in electron.vite.config.js) and esbuild does the same rewrite natively, so app-side imports are written ./foo.js and resolve to ./foo.ts at dev, build, and bundle time. That's why the source is full of .js import strings — don't "fix" them. The alias also maps .jsx → .tsx, which is why React components are still imported as ./App.jsx / ./FileTree.jsx while every one of those files is .tsx on disk. Same rule, same reason: the specifier is a build-time key, not a filename.
A module the test suite loads spells its own imports .ts. npm test runs node --test straight off the source with no build step — that's what makes the suite installable-free and fast — and Node resolves a specifier literally: it has no extensionAlias, so ./foo.js does not find foo.ts. Node strips types natively (22.18+), so the extension is the whole constraint.
Concretely, that means:
- Every
tests/*.test.jsimport names.ts(from '../src/renderer/linkIndex.ts'). - A tested module importing another tested module names it
.tstoo — todayrenameOps.ts,metadataCache.ts,settingsDiff.ts, andapi/src/keys.ts. - Everything else keeps
.js. Rewriting the whole app would be churn for nothing.
allowImportingTsExtensions: true (both tsconfigs, legal because they're noEmit) is what lets tsc accept the .ts spelling. This is why the parser, the rename logic and the credential declaration sat unchecked as .js for so long — being under test is what pinned them, not neglect. If you add a test for a module, change that module's imports to .ts in the same move.
Cross-process types live in src/shared/:
api.d.ts— the typedwindow.api.*surface (what preload exposes).settings.ts— typedSettingsshape. (Synced settings live on the companion — the source of truth; the desktop keeps noDEFAULT_SETTINGS. Only machine-local settings have desktop defaults:LOCAL_SETTINGSinsrc/main/api/localSettings.ts.)constants.ts— shared enums +APP_NAME.
For day-to-day workflow (when to restart, how to read main vs renderer logs, how to attach to the renderer via CDP for headless debugging, IPC discipline), use the electron-dev skill at .claude/skills/electron-dev/SKILL.md.
Architecture
Electron app with a Vite + React 19 renderer. The renderer is a markdown-workspace editor (CodeMirror 6) with wiki-links ([[name]]), backlinks, tabs, drafts, multiple workspaces, a force-graph view, a live-preview / raw view-mode toggle, an editor status bar, bookmarks, daily notes, quick search, image embeds, voice input, and a right-hand coding-agent chat sidebar (pi).
Markdown is the default, not the only thing it opens. The same tab strip also hosts other text/code files (any extension in OPENABLE_RE, plus an allowlist of extensionless dotfiles), image/video previews, and editable Excalidraw drawings. Which viewer a tab gets is decided by src/renderer/MediaView.tsx's isOpenable / mediaKind / isDrawing; see "Files that aren't markdown" in src/renderer/CLAUDE.md.
Process boundary
- Main (
src/main/): filesystem, dialogs, context menus, settings (a thin client over the companion API + a machine-local userData file — no local DB),nativeTheme, the file watcher + rename correlator, theapp://media/...protocol for serving workspace images, window-bounds persistence, the desktop host for the sharedagent-corecoding-agent runtime (one live pi session per chat, concurrent; the turn logic lives inagent-core, this side supplies I/O), the voice-token mint (AssemblyAI, Deepgram or ElevenLabs) and the voice list for the settings picker, auto-update. Entry:src/main/main.ts— ~3,000 lines, and every IPC handler in the app is registered in it (~110 of them), so expect a long file rather than a slim entry point; the subsystems it calls into are the separate modules beside it. Deep doc:src/main/CLAUDE.md. - Preload (
src/preload/preload.cjs): exposes a singlewindow.apisurface (typed insrc/shared/api.d.ts). The renderer never touches Node — every fs/dialog/agent call goes throughwindow.api.*. Also exposeswebUtils.getPathForFileso the renderer can resolve drag-dropped folder paths (skill import). - Renderer (
src/renderer/): React app rooted atmain.tsx→App.tsx.App.tsxcomposes 11 hooks fromsrc/renderer/hooks/(where the per-domain state lives) and owns what genuinely spans them: the save lifecycle, tree rename/move, the conflict view, and the tree/link-index helpers. It is ~2,500 lines — the hook extraction is real, but "thin" would overpromise; read it expecting an orchestrator with substance, not a shell. Vite root issrc/renderer/(configured inelectron.vite.config.js); build output goes toout/renderer/. Built main/preload land atout/main/index.jsandout/preload/index.cjs. Deep doc:src/renderer/CLAUDE.md. - Companion (
api/): a separate Node/Express + Postgres server the desktop talks to over HTTP — the source of truth for synced settings, secrets, workspaces, and chats, and the host that runs the coding agent for Telegram and cron. Runs in Docker (compose: postgres + api + traefik). Not part of the Electron app; the desktop reaches it viasrc/main/api/. Deep doc:api/CLAUDE.md.
Where things live
| Area | File(s) | Deep doc |
| Main-process internals (watcher, IPC, settings, app://, coding agent, voice token, GitHub sync engine) | src/main/*.ts | src/main/CLAUDE.md |
| Renderer internals (hooks, editor decorations, file tree, chat sidebar, voice, bookmarks, daily notes, quick search, sync UI) | src/renderer/** | src/renderer/CLAUDE.md |
| Settings pages (when a value saves, credential fields, per-section inventory) | src/renderer/settings/** | src/renderer/settings/CLAUDE.md |
| Companion server (settings/secrets/chats storage, server-side agent for Telegram + cron, Docker deploy) | api/** | api/CLAUDE.md |
| Shared coding-agent runtime (pi wrapper — bundled into BOTH the desktop and the companion) | agent-core/** | agent-core/CLAUDE.md |
| Attachments (any file the user sends, on either host) | agent-core/{attachmentPolicy,attachmentNotes}.ts, src/renderer/{chatAttachments.ts,ChatSidebar.tsx}, src/main/codingAgent.ts (stashChatFile), api/src/telegram/attachments.ts | the attachment entries in agent-core/CLAUDE.md |
| Voice, both directions (hearing you, and answering out loud) | agent-core/{voiceProviders,transcribe,speak,spokenText,voiceReply}.ts, src/renderer/settings/VoiceSection.tsx, src/renderer/voice/**, api/src/telegram/sendTool.ts | the voice entries in agent-core/CLAUDE.md |
| Reviews (the agent writes its own skills after enough work) | api/src/{backgroundSweeper,backgroundRun}.ts, agent-core/manageSkill.ts, agent-core/skill{Validate,Tool}.ts, agent-core/fuzzyMatch.ts, agent-core/defaults/reviewPrompt.ts | "Background runs" in api/CLAUDE.md |
| Memory (what the agent knows about you and this workspace) | agent-core/memory{Store,Tool}.ts, agent-core/defaults/memoryPrompt.ts, api/src/{backgroundSweeper,backgroundRun}.ts; the files themselves are MEMORY.md / USER.md at the workspace root | "Memory" in agent-core/CLAUDE.md |
| GitHub sync (merge-based; in-app conflict resolution) | src/main/sync.ts, src/main/syncEngine.ts, src/renderer/settings/WorkspacesSection.tsx, src/renderer/settings/AddWorkspaceDialog.tsx; conflict view in src/renderer/{App.tsx, SortBar.tsx, FileTree.tsx} | "GitHub sync" sections in both subdocs |
| Cross-process types + constants | src/shared/{api.d.ts, settings.ts, constants.ts} | this file, below |
| Bundled skills the agent gets out of the box | resources/built-in-skills/**, cli-tools/, src/main/cliTools.ts | resources/built-in-skills/CLAUDE.md |
| Cutting a release (desktop installers + the companion image, one git tag) | .github/workflows/*.yml | .github/workflows/CLAUDE.md |
| Tests | tests/*.test.js | tests/CLAUDE.md |
| Historical design records — not current | docs/*.md | docs/CLAUDE.md |
Terminology
The canonical names. Use these in UI strings, comments, docs, agent prompts — anywhere a human (user or contributor) might read them.
- File — a
.mddocument in the workspace. The user-facing noun for the thing you create / open / edit / delete. Never use "page" or "note" — both were earlier conventions that have been retired. - Basename — a file's name with no folder path and no
.mdextension. Fornotes/projects/Foo.md, the basename isFoo. This is what wiki-links use, and what the link index is keyed by. - Workspace — a GitHub repo plus its checkout on this machine. Everything inside the folder (files, images, other assets) is part of the workspace. A workspace cannot exist without a repo: the companion's
workspace.repo_owner/repo_namecolumns areNOT NULL(api/src/schema.ts,api/init.sql), and the two setup flows both clone. The repo identity lives on the companion; each machine's checkout path + sync toggle are machine-local (local-settings.json). Code sometimes still says "vault" (Obsidian-inherited); new code uses "workspace". - Wiki-link — the
[[Some File]]syntax linking one file to another by basename. The term comes from MediaWiki/Obsidian/etc. Variants:[[File#Heading]],[[File|Display]]. Resolution is workspace-wide, case-insensitive, basename-only — never include a folder path. The parser + index live insrc/renderer/linkIndex.ts(mirrored in main atsrc/main/linkParser.ts). - External link — the
[label](https://…)markdown form. Always means an off-workspace URL. Opens in the system browser. Not to be confused with wiki-links. - Chat — one conversation with the agent. The user-facing noun, and the name used in code, in the database (
chat/message.chat_id), and in the IPC channels (chat:list,chat:open, …). Never call a chat a "session". - Session — reserved for pi's own session: its
SessionManager, itsAgentSession, and the JSONL session file that is its working memory for a chat. Different thing, different lifetime — pi's session is rebuilt from the stored transcript whenever a chat moves between machines. - Review run — a chat the companion opens by itself, after a chat has done enough work, in which the agent reviews that conversation and updates its own skills. It is an ordinary chat (
source: 'review'), not a second agent and not a supervisor of anything. Closest existing thing is cron: same fresh chat, same unattended run, same check-in — only the trigger differs. - Memory — the two files the agent maintains at the workspace root:
MEMORY.md(what it has learned about working here) andUSER.md(who the user is). Written with thememorytool, loaded into the prompt at the start of every chat. Memory is not review: they are separate processes with separate triggers, separate counters, separate settings and separate prompts, and the only thing they share is the timer that wakes them. Do not describe one in terms of the other. - Memory run — a chat the companion opens by itself, after a chat has accumulated enough of the user's messages, in which the agent looks that conversation over for facts worth remembering (
source: 'memory'). Same shape as a review run, different trigger and a tool set of exactly one. - Backlink — a wiki-link that points at a given file from elsewhere. The link index maintains backlinks per file; the backlinks panel under the editor reads from that.
Avoid: "page", "note", "document" (for .md files), "vault" (in new code/copy), "internal link" (call it a wiki-link), "session" (for a chat).
Invariants when touching files/links
Any code that creates, modifies, renames, or deletes a .md file — whether through in-app actions or via the watcher path — must satisfy all of these. Skipping any one drifts the cache from disk:
- Link-index sync. Create/change →
linkIndex.updateFileorapplyParsedLinks. Delete →removeFile. Rename →renameFile. Thenbump()so consumers re-render. - Tree refresh. Any add/remove of a file or folder must result in
refreshTree()(in-app: callfileOps.treeAndIndexChanged(); external: handled by the fs watcher). - Folder rename re-keys nested files. Renaming a folder changes every nested file's path. The handler (
onTreeRenameinApp.tsx) walksgetOutgoingMap()for paths under the old folder, callslinkIndex.renameFile(oldP, newP)andrenameTabsPath(oldP, newP)for each, and shiftsselectedFolderPathif it pointed inside.onMoveItemsdoes the same for drag-and-drop moves. Without this, the index carries stale path keys until the watcher echoes per-file events, and open tabs inside the renamed folder break. - Parser parity.
LINK_RE/parseTarget/normalizeTarget/parseLinks/leadingWidth/collectContextmust stay identical betweensrc/renderer/linkIndex.tsandsrc/main/linkParser.ts. The watcher in main reuseslinkParser.ts, so this constraint is exercised on every external change.tests/parserParity.test.jsenforces this. - Save before mutating active file.
writeNow()first, awaited. See "Save lifecycle" insrc/renderer/CLAUDE.md. - Real mtimes everywhere — never
Date.now(). Every place that stores or compares a mtime — main'sfs:writeFile/fs:createFilereturn value, the watcher'sfs:changedevent, the renderer'slinkIndex.updateFilecall fromwriteNow— uses the file'sstat.mtimeMs(a float with sub-ms precision on macOS/Linux). The self-echo guard works because the value stored vialinkIndex.updateFile(path, text, mtime)is exactly the value the watcher's later stat returns for the same write, soevt.mtime > storedis false on the echo. MixingDate.now()(integer ms) with a sub-ms float makes every save look fresh to the watcher → editor reloads from disk mid-typing → keystrokes lost, cursor jumps. TheuseLinkIndex.updateFilewrapper MUST forward the mtime arg through; shipped briefly with arity 2 in v1.0.1 and triggered exactly this. - Workspace-scoped watcher. One watcher per app. Switching or removing a workspace must
watchStop()before doing anything else;loadWorkspacehandles this. Don't start a watcher without stopping the previous one. Starting also seeds the rename correlator (stat + hash every.mdunder the root) so unlinks fired immediately afterwatchStartcan still be correlated. - Idempotent watcher handlers. Every in-app write self-echoes ~350ms later. Handlers must be safe to re-run on the same data. The mtime guard is the primary mechanism. For
renameevents, the renderer's handler is also idempotent (linkIndex.renameFileof an already-renamed path is a no-op; the regex rewrite matches nothing because refs are already rewritten). - Watcher reloads the active file on external change. When
evt.path === activeFileand the event is fresh (evt.mtime > stored mtime), the renderer reads the new content, callseditor.setContent(text, viewState)to preserve cursor/scroll, then flashes the added text with an indigo accent pulse viaeditor.flashRanges(ranges)(reuses the AI-stream-done animation; see--ai-done-bg-*inapp.css). The diff is word-level (diffnpm,diffWordsWithSpace). Reload runs unconditionally — if a keystroke lands in the same instant the agent (or any external writer) modifies the file, the keystroke loses. The save-debounce window is 500ms so this is rare. The renderer's own writes don't trigger this path because the self-echo's mtime is equal to the stored mtime (see invariant #6). Seesrc/renderer/diffFlash.tsfor the flash extension. - Self-references are rewritten on rename.
renameOps.tsdoes NOT skipsrc === oldPath. IfFoo.mdcontains[[Foo]]and is renamed toBar.md, the on-disk file ends up containing[[Bar]]. Don't reintroduce the skip.
Cross-process constants
APP_NAMElives in two places:src/shared/constants.ts(single source of truth, imported by both processes) andpackage.json(build.productName— electron-builder uses it for.app/.dmgnames at build time). Comments in each file flag this.FILE_ACTIONS,FOLDER_ACTIONS,EDITOR_ACTIONS,SUPPORTED_PROVIDER_SLUGS,DEFAULT_PROVIDER_SLUG,isCompanionStale— all insrc/shared/constants.ts. That last one is a predicate rather than a constant and belongs there for the same reason the rest do: the desktop and the companion image are cut from one release tag, so a version mismatch means they disagree about the shape of the data they exchange, and main answers it to refuse every write while the renderer answers it for the toast, the sidebar icon, the settings gate and the chat composer. One declaration, five readers;'dev'(either side unversioned) is never stale, or every development session would be unable to save. See "The companion's version rides the same signal" insrc/main/CLAUDE.md. Bothsrc/main/main.ts(native context menus, provider filter) andsrc/renderer/constants.ts(re-export + renderer-only additions) import from there. Editor-menu items are built per right-click from what the renderer says applies, so an item that can't work is absent rather than present and broken:EDITOR_ACTIONS.SEND_TO_AGENTneeds the active file to have a path on disk (drafts opt out), andEDITOR_ACTIONS.REVEAL_IMAGEneeds anapp://media/image under the pointer (remote anddata:images have nothing on disk to reveal).- Renderer-only constants (
SETTINGS_SECTIONS,THEME_MODES,VIEW_MODES,SAVE_STATES,TREE_SORT_ORDERS,TREE_SORT_LABELS) live insrc/renderer/constants.ts. - The
Settingstype lives insrc/shared/settings.ts. Top-level keys:workspaces,activeWorkspaceId(both DERIVED, not stored scalars),appearance,codingAgent,agentSecrets[],transcription+speech+voiceKeys(the three voice JOBS — transcription, the microphone viatranscription.micProvider, and speech — plus the per-vendor keys they share; seeagent-core/voiceProviders.ts),sync,telegram(bot preference — the catching-up notice; identity and runtime state stay in the companion'stelegram_accountrow),timezone,chatSidebarOpen,chatSidebarWidth,sidebarWidth,viewMode,treeSortOrder,bookmarkFilterActive,showHiddenFiles,chatSources,openTabs,windowBounds. (Scheduled runs have no settings key: the companion executes them and its knobs are that server's env; job definitions live per-workspace in<workspace>/cron.json. See "Scheduled runs (cron)" insrc/main/CLAUDE.md.) Synced settings live on the companion (Postgres) — the single source of truth — and the desktop applies NO defaults on read, so an unset value reads as unset, never faked. Adding a persisted synced field means updating theSettingstype, adding its line tonormalizeSettings(src/renderer/settingsModel.ts— the one total function that builds the renderer's settings object; theSettingsreturn type makes a missing key a compile error), and adding a setter inuseSettingsif it needs one. Do not add auseStatefor it — the renderer holds ONE settings object and every field is a read off it; per-field copies are what made a setting from a previous companion stay on screen (see "Settings are one object" insrc/renderer/CLAUDE.md). There is no desktopDEFAULT_SETTINGSto extend. A synced field with no value is simply unset — required fields error at their consumer, optional fields fall back at the point of use. A credential is declared in exactly one place:agent-core/credentials.ts— add it there and all three consumers follow (the companion'sapi/src/keys.tsdecides what to encrypt → thesecret_valuetable, crypto columnsNOT NULL;src/main/settingsStore.tsdecides what to strip before the renderer;src/renderer/settingsDiff.tsdecides what not to send back).agent-coreis the only code bundled into both builds, which is why it lives there. Three copies of this fact is what it replaced, and a mismatch is not cosmetic — miss a field in the strip and it leaks to the screen, miss it in the send guard and a stale cached copy republishes over a fresher one. Pinned bytests/credentials.test.js(the list, by name) andtests/rendererSettingsDoor.test.js(no IPC handler may return an unstripped read). A stored credential is destroyed only by something asking to destroy it — never by an empty value in a save, never by a name missing from a saved list, and a rename re-files the value rather than recreating the entry. That rule lives on the companion (putSecret/writeAgentSecrets/ the twoDELETEroutes) because it has to hold for every writer, not just the renderer; the four ways it was got wrong, and what each one cost, are tabulated inapi/CLAUDE.mdunder "Destroying a credential is a REQUEST, never an inference". Pinned bytests/secretDeletes.test.js. Machine-local settings (window/view state, the active workspace, which chat sources the history list shows) are the exception: they live in<userData>/local-settings.jsonwith desktop defaults. Those are declared in exactly one place too —LOCAL_SETTINGSinsrc/main/api/localSettings.ts, a key→default map thatLOCAL_KEYS(write routing) andoverlayLocal(the read overlay) both derive from. Adding a machine-local field is that one edit; splitting the two lists is how a key ends up routed but undefaulted (reads asundefinedon a fresh machine) or defaulted but unrouted (silentlyPATCHed to the companion, so it syncs and fails offline). See "Settings persistence" insrc/main/CLAUDE.mdand the companion'sapi/CLAUDE.md.
Duplicate basenames + path-prefixed links
Duplicate basenames across different folders are allowed (clients/acme/Meeting.md
and clients/globex/Meeting.md coexist). Only same-folder collisions are rejected/
auto-disambiguated (" 1", " 2", …) — the filesystem forbids them anyway. fs:createFile,
fs:renameFileLiteral, fs:moveItem, and the renderer's findNameConflict
/ findTreeRenameConflict are all same-folder-only now. (uniqueInWorkspace /
collectMarkdownBasenamesLower are still exported from pathResolver.ts but nothing
imports them — the workspace-wide-uniqueness era is over. There is no fs:renameFile
channel; fs:renameFileLiteral is the only rename IPC and it forces no extension.)
Wiki-links resolve through src/renderer/linkResolver.ts (resolveLinkTarget):
- Bare
[[Meeting]]→ the file with that basename in the source's own folder, else the shallowest path (Obsidian-style tiebreaker).pageIndexisMap<basename, path[]>. - Path-qualified
[[globex/Meeting]]→ the exact folder match; if the path is stale (target moved/renamed) it falls back to basename resolution so the link still resolves. - Autocomplete inserts
shortestUniqueLinkFor— bare when the name is unique, the shortest disambiguating prefix when not — so ambiguous links are rarely authored.
Backlinks are basename-keyed but each entry carries targetParsed; getBacklinksForFile
resolve-filters the bucket so a link attributes to the correct duplicate. Rename
reference-rewrite (renameOps.ts) is resolution-aware (rewrites only links resolving
to the renamed file; handles path-qualified links).
Tests
npm test runs everything via node:test (no install). See tests/CLAUDE.md for the per-file coverage table and what's not covered by automated tests.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

