This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CLAUDE.md and AGENTS.md are kept byte-identical. CLAUDE.md is what Claude Code reads; AGENTS.md is what vendor-neutral agent tools read — Codex, OpenCode, Cursor, Copilot, and whatever follows them. Two real files, deliberately not a symlink: not every tool resolves one.
After editing either file, copy it over the other — don't repeat the edit by hand:
cp CLAUDE.md AGENTS.md # or the reverse, whichever you just editedRetyping a change is exactly how the two drift; one reflowed line or reworded clause is enough. diff CLAUDE.md AGENTS.md must print nothing. If it ever does, treat it as a defect and fix it by letting one file win wholesale — never by merging them.
forgemap is a Node 24 / pnpm 11 CLI (built on citty) that manages a local repo layout of the form <root>/<forge.dir>/<namespace…>/<repo>.
The namespace is variable-depth — one segment on GitHub, as many as GitLab subgroups need. src/repos/layout.ts is the single place that bounds it (MAX_SCAN_DEPTH, the .git marker, and the per-forge-type depth limit); src/repos/scan.ts, src/repos/cache.ts, src/repos/evaluate.ts and src/slug/resolve.ts all read it rather than re-encoding a depth of their own.
- A directory holding a
.gitentry is a repo — file or directory alike, since worktrees and submodules use a.gitfile. The scan walks to the first marker and never descends into a repo, so a plain directory under a forge dir is not a repo and drops out oflist/status/pick;validatereports it as a hint. parseSlugstays pure. The last segment is the repo, everything before it the namespace; depth is checked against the forge type inresolveSlug, where the two are bound —githubstill rejectsa/b/c.
Commands are registered in src/cli.ts — treat that file as the list, not this one:
- Acquire —
clone,import(adopt existing checkouts),cleanup,delete. - Locate —
cd,path,open,list,pick(fuzzy picker,fuse.js). - Inspect —
status,sync,validate. - Plumbing —
completion,shell-init,config(init/show).
list is the single command for both listing and searching — bare list prints every repo, list <query> fuzzy-matches. There is no separate search command (it was replaced by list).
Forge support is per-type, dispatched by src/forges/registry.ts:
github— shells out togh(clone + a batched GraphQL remote-identity check).gitlab— shells out toglab, same shape: batched aliased GraphQL plus one REST call per miss to tellmovedfromgone.glabis required, never optional — a silent fallback to plain git would make clone behaviour depend onPATH. Every invocation setsGITLAB_HOST=<forge.host>so nothing depends on the user's globalglab config set host.git— vanillagit clone, ssh by default (protocol: 'https'to override). Namespaces may nest here too: it is the documented alternative for a GitLab-shaped remote without a second CLI.gitea/codeberg— declared inForgeType, not implemented —getForgeAdapterthrows and points attype: 'git'as the fallback.
Config is loaded by c12 from forgemap.config.{ts,mts,cts,js,mjs,cjs,json} — resolution order: --config flag → FORGEMAP_CONFIG env → walk up from cwd → global $XDG_CONFIG_HOME/forgemap (or ~/.config/forgemap). Shape is { root, defaultForge, forges: Record<string, { type, host, dir }> } (src/config/schema.ts).
- User-defined
forgesreplace the defaults wholesale — they are deliberately not deep-merged (src/config/load.ts). c12'sdefaults:would leak the built-ingithubforge into every custom layout and makevalidatedemandghwhen no github forge is configured.
| Command | Purpose |
|---|---|
pnpm install |
Install deps and wire husky hooks (prepare script runs husky). |
pnpm build |
vite build → dist/ (dev rebuilds on watch). |
pnpm lint |
oxlint . --deny-warnings |
pnpm format |
oxfmt --check . |
pnpm typecheck |
tsc --noEmit |
pnpm test |
vitest run (test:watch, test:coverage). |
pnpm check |
lint + format. |
pnpm check:fix |
Auto-fix lint + format. |
pnpm bench |
Scan benchmark (bench/scan.ts). |
pnpm taze |
Show available dependency updates (taze:w to write). |
pnpm check is not the full CI gate — CI also runs typecheck, test and build. The full local equivalent is pnpm check && pnpm typecheck && pnpm test && pnpm build.
This repo mirrors the kirchDev scaffold (../scaffold/). Treat that repo as the source of truth for tooling drift — when bumping configs, diff against it first.
- oxlint + oxfmt only. No ESLint, no Prettier. Both are pinned exactly in
package.json(no^) — bump them deliberately, and diff against the scaffold when you do. - oxfmt formats Markdown too,
CLAUDE.mdandAGENTS.mdincluded — a doc edit can failpnpm format. Runpnpm exec oxfmt <file>after editing prose. - Conventional Commits enforced. Commitlint runs in the
commit-msghook. Usefeat:,fix:,chore:,docs:, etc. Breaking changes:feat!:orBREAKING CHANGE:in the body. - Husky hooks are required. Don't
--no-verifyunless explicitly asked.pre-commitruns lint-staged (oxlint + oxfmt on staged files);commit-msgruns commitlint. README.mdis excluded from oxfmt (.oxfmtrc.jsonignorePatterns). The kirchDev README house style relies on centered HTML blocks that the formatter would mangle — don't try to reformat it.- pnpm settings live in
pnpm-workspace.yaml, not.npmrc. pnpm 10+ reads only auth/registry settings from.npmrc; everything else (nodeLinker: isolated,preferFrozenLockfile: true, …) is camelCase YAML inpnpm-workspace.yaml. Always commitpnpm-lock.yamlchanges. minimumReleaseAge: 4320(3 days) inpnpm-workspace.yaml— fresh package versions are blocked from install. If a new release is needed urgently, lower or override locally; don't change the global default.- Vitest config lives in
vite.config.ts(thetestkey), not a separatevitest.config.ts. Tests live intests/, mirroringsrc/— not colocated. Coverage thresholds are enforced there and fail the run, so a newsrc/file generally needs tests.
Automated via release-please on push to main. feat:/fix: commits drive the next version; release-please opens a PR with the bump + CHANGELOG entry. Merging tags the release.
release-please-config.json—release-type: node,include-v-in-tag: true, pre-1.0 rules (bump-minor-pre-major: true,bump-patch-for-minor-pre-major: false)..release-please-manifest.json— current version source of truth. Keep in sync withpackage.jsonversionwhen manually bumping.devis the integration branch. Work lands ondev;dev-pr.ymlkeeps a draftdev → mainrollup PR open. Merge it with a merge commit, not squash — squashing hides the individualfeat:/fix:commits from release-please.
ci.yml— four parallel jobs on PR tomain/dev(skips drafts): Lint & Format, Typecheck, Test, Build. The Test job runspnpm test:coverageand posts a sticky coverage comment; the coverage gate itself is vitest'sthresholds, not the action.codeql.yml—javascript-typescript+actionsanalysis, weekly (Sun 23:34 UTC) + on every push/PR tomain/dev(no path filter) +workflow_dispatch.release-please.yml— triggers on push tomain/dev, but the job is gatedif: github.ref_name == 'main', so onlymainreleases. Publishes with a GitHub App token minted from a Bitwarden-held PEM.dev-pr.yml— on push todev, opens/refreshes the draftdev → mainrollup PR.
Dependabot groups npm patches/minors weekly and GitHub Actions monthly, targets dev (not main), and mirrors the 3-day minimumReleaseAge via cooldown (.github/dependabot.yml).