Imported from ckaznocha/vibes (
AGENTS.md). Install upstream withnpx skills add ckaznocha/vibes. Copyright stays with the author.
General Guidelines for working with Nx
- For navigating/exploring the workspace, invoke the
nx-workspaceskill first - it has patterns for querying projects, targets, and dependencies - When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through
nx(i.e.nx run,nx run-many,nx affected) instead of using the underlying tooling directly - Prefix nx commands with the workspace's package manager (e.g.,
pnpm nx build,npm exec nx test) - avoids using globally installed CLI - You have access to the Nx MCP server and its tools, use them to help the user
- For Nx plugin best practices, check
node_modules/@nx/<plugin>/PLUGIN.md. Not all plugins have this file - proceed without it if unavailable. - NEVER guess CLI flags - always check nx_docs or
--helpfirst when unsure
Scaffolding & Generators
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the
nx-generateskill FIRST before exploring or calling MCP tools
When to use nx_docs
- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
- DON'T USE for: basic generator syntax (
nx g @nx/react:app), standard commands, things you already know - The
nx-generateskill handles generator discovery internally - don't call nx_docs just to look up generator syntax
Workspace conventions (this section is NOT nx-managed — edit freely, do not delete)
This is a generic harness meant to keep growing with new, independently-published
projects. The root-level tooling (.claude/skills, .claude/agents, .claude/hooks)
is written to discover projects via Nx tags and the project graph, not by hardcoding
project names — a new project gets skills, agents, hooks, and lint/module-boundary rules
"for free" by carrying the right tags, with zero changes needed to root-level .claude/
content. Keep it that way: when you add root-level automation, make it query
nx show projects -p "tag:..." / nx show project <p> --json rather than naming a
specific project.
Tag taxonomy
Declared in each project's project.json tags array. npm:* tags and
metadata.description are free — Nx infers them from package.json keywords/
description, don't set them by hand.
| Tag | Means | Who reads it |
|---|---|---|
scope:<project> |
Standard Nx per-project isolation tag | @nx/enforce-module-boundaries in eslint.config.ts |
scope:shared |
Lib in libs/* meant to be depended on by more than one app (not owned by any single scope:<project>) |
@nx/enforce-module-boundaries; every scope:<project> app is permitted to depend on scope:shared libs, and scope:shared libs may only depend on other scope:shared libs |
type:app / type:lib |
Deployable unit vs. shared library | @nx/enforce-module-boundaries |
type:mcp-server |
Project is a stdio MCP server (@modelcontextprotocol/sdk + zod, main.ts wiring + registerTool per file) |
project-conventions, new-mcp-tool, security-reviewer |
publish:npm |
Project publishes to npm via a <project>@vX.Y.Z-tagged GitHub Release (see .github/workflows/release.yml) |
cut-release, release-gatekeeper, dependency-reviewer |
data-source:scraping |
Project parses uncontracted third-party HTML/JSON and should declare its live sources in a <root>/fixture-sources.json |
project-conventions, regen-fixture, scrape-resilience-reviewer |
safety:link-only |
Project must never perform the state-changing action it links to (only returns a URL) | project-conventions, security-reviewer |
Adding a new project
- Tag it appropriately in
project.json(table above) — this is what makes it discoverable, not a name added to some root config file. - If it's
type:mcp-server, followproject-conventions(auto-loads when you touch itssrc/) and usenew-mcp-toolto add tools. - If it's
publish:npm,.github/workflows/release.ymlneeds apublish-<project>job added by hand — the tag alone doesn't wire up CI (release-gatekeeperchecks for this). - If it's
data-source:scraping, add afixture-sources.jsonat its root soregen-fixturecan check it for upstream drift. - If it introduces a genuinely new category of thing (not just another instance of an
existing tag), extend the taxonomy table above and, if it constrains dependencies,
eslint.config.ts'sdepConstraints— don't invent an untagged, undocumented convention. - The
@nx/js/typescriptsync generator (registered innx.json) is supposed to keep roottsconfig.json'sreferencesarray up to date from the project graph, but every project in this workspace (apps and libs alike) overrides its inferredbuild/typechecktargets with hand-writtennx:run-commandsones inproject.json— the sync generator doesn't engage for a project once its targets are hand-written, so in practice it has never auto-updated this file. Add the new project's{ "path": "./apps/<name>" }/{ "path": "./libs/<name>" }entry to roottsconfig.jsonreferencesby hand; don't rely onnx build/nx typecheckto prompt for it. eslint.config.ts's typed-lintingparserOptions.projectglob already covers bothapps/*/tsconfig.spec.jsonandlibs/*/tsconfig.spec.json— a project under either directory gets typed linting for free; a project in some third top-level directory would need that glob extended.pnpm-workspace.yamldeclareslibs/*(added once the first lib —libs/mcp-tool-result— landed;sherif, seepnpm run lint:workspace, would otherwise flag a workspace glob matching zero packages as a smell). Alibs/<name>package needs amain/typesandexportsfield in itspackage.jsonpointing at./src/index.ts— these are TS-source-only libs with no build step (consuming apps' esbuildbundle: truebuilds read the source directly), so nothing else resolves the bareimport from "<lib-name>"specifier otherwise. A lib meant to be shared across more than one app'sscope:<project>gets taggedscope:shared(see taxonomy above) instead of a single app's scope tag. Use thenew-libskill to scaffold one — it encodes every gotcha in this item plus the two below, all discovered the hard way once already:eslint.config.ts'stype:appdepConstraintsentry must listtype:libin itsonlyDependOnLibsWithTags, not justtype:app— every project here is tagged bothtype:appandtype:mcp-serversimultaneously, and@nx/enforce-module-boundariesrequires every matching constraint to pass, so thetype:mcp-serverconstraint already allowingtype:libisn't enough on its own.@nx/dependency-checks(wired ineslint.config.tsforapps/*/package.jsonandlibs/*/package.json) is configured withbuildTargets: ["typecheck"]instead of its default["build"], because it only counts a workspace dependency as "used" if that dependency also has the named target — and libs here intentionally have nobuildtarget. Don't "fix" this back to the default; every project (apps included) hastypecheck, so that's the one target guaranteed to exist everywhere.
lefthook.yml'saffected-checkscommand already runsnx affected -t typecheck,testscoped to whatever you staged — nothing to add there for a new project either..vscode/launch.jsonis the one thing here Nx/pnpm can't make automatic: VS Code debug configs are inherently per-project (no glob support), so a new project that needs one still needs a hand-added entry, following the existingDebug <project> with Nxpattern (own inspector port, ownoutFilespath).- Before writing new logic in any
type:mcp-serverproject — new or existing — check whether it already exists elsewhere first. Grep siblingtype:mcp-serverprojects'src/(nx show projects -p "tag:type:mcp-server" --json, then read each match) andlibs/*for a function doing the same thing. If it's genuinely the same concern in two or more projects (not just superficially similar — see the litmus test below), extract it into ascope:sharedlib via thenew-libskill instead of writing a second copy, the same waylibs/mcp-tool-resultandlibs/resilient-fetch(retry/circuit-breaking/ throttling for outbound network calls — seeproject-conventions's "Network calls" section) got pulled out, andlibs/tmdb-client(later reverted — see next paragraph) did not. This applies equally when scaffolding a brand-new project: check what the existingtype:mcp-serverprojects already have before hand-rolling something they've already solved (tool-result wrapping, env-var parsing helpers, network retry/throttling, an API client with request caching, etc).- Litmus test — extract only if the concern is shared, not just the shape. Two
functions with a similar signature that happen to solve different problems don't
belong in one lib.
libs/tmdb-clientlooked identical tolibs/mcp-tool-resultin spirit (both were "cache an instance/format a response, used in ≥2 projects") but got reverted:tmdb-mcpwas created specifically to be the only place TMDB logic lives in this workspace, soletterboxd-mcpdepending on a shared TMDB client re-created the exact coupling that project split was meant to remove.libs/mcp-tool-resultsurvived the same review because MCP response formatting isn't "owned" by any one project — it's incidental plumbing everytype:mcp-serverproject needs alike. When unsure which case you're in, ask: does one project exist specifically so others don't have to implement this? If yes, don't extract — make the other project(s) call it instead (even if that means a second tool-call round trip), or ask the user first.
- Litmus test — extract only if the concern is shared, not just the shape. Two
functions with a similar signature that happen to solve different problems don't
belong in one lib.
- Every project's
package.jsonnameis scoped@ckaznocha/<project>(the Nx project name itself —project.jsonname, tags,nx run <project>:...— stays unscoped; only the npm-facingpackage.jsonname carries the prefix), and every project'spackage.jsongets"license": "MIT". This is enforced at the root, not per-project: rootpackage.json's ownnameis@ckaznocha/vibes, and Nx infers the org prefix for anything scaffolded with an@nx/*generator (e.g.@nx/js:lib) from that rootnamefield, so a fresh generator-created project is prefixed automatically. Projects scaffolded by hand (this workspace'stype:mcp-serverapps andlibs/*don't go through a generator — seenew-lib/project-conventions) need the prefix and license added by hand in theirpackage.json, and anyworkspace:*dependency reference or bare-specifier import of alibs/*package elsewhere in the workspace must use the scoped name (import { ... } from "@ckaznocha/mcp-tool-result", not"mcp-tool-result") —new-lib's template already reflects this.
nx.json's release block is configured for independent per-project versioning
(conventionalCommits, releaseTag.pattern: "{projectName}@v{version}" matching the tag
format release.yml already parses) and changelog generation, but version.git/
changelog.git are both disabled — nx release version/nx release changelog are
available as optional local helpers (e.g. to auto-compute the next semver bump or draft a
CHANGELOG.md entry), but they do not commit, tag, or push. The actual commit/tag/publish
trigger stays exactly as cut-release describes: a manually created GitHub Release whose
tag CI parses. Future direction worth considering as this grows: flipping version.git.tag
and wiring nx release publish into CI so tagging and publishing become fully automated
instead of the current manual cut-release steps — not done yet since it touches
CI/release infra and wasn't asked for.