Conventional commits, enforced by commitlint through a commit-msg hook; the same hook also rejects a commit message carrying a Claude-Session: trailer, since that line is agent bookkeeping rather than project history. pre-commit additionally rejects a merge commit on main and a squash merge (detected via a lingering SQUASH_MSG) — history stays linear by rebasing, never merging or squashing — before running ESLint over staged files in the package that owns them. pre-push runs typecheck and unit tests across the workspace, both through turbo, so the cost is proportional to what actually changed. Work in whichever package's directory the change belongs to; each package's own README documents its architecture and the format-specific decisions behind it.
See the root README for how the workspace, task pipeline, and release orchestration fit together before making a structural change.
Formatting is Prettier's, enforced through ESLint rather than a separate command: prettier/prettier is an ordinary lint rule, so pnpm lint reports a formatting difference and the pre-commit hook fixes staged files automatically. prettier.config.ts sets one option (singleQuote: false); everything else is Prettier's own default deliberately, so the config states a decision rather than restating defaults.
One local step is worth doing once per clone:
git config blame.ignoreRevsFile .git-blame-ignore-revs.git-blame-ignore-revs lists the whole-tree reformat commits, so git blame attributes each line to the change that actually wrote it rather than to the reformat. Git does not read the file on its own — the config above is what opts in. GitHub's blame view reads it automatically, so this only affects blame run locally.
Bringing a new package into packages/* — whether newly written or absorbed from a former standalone repo — needs more than a directory and a pnpm-workspace.yaml glob match to release cleanly. In order:
-
Create
packages/<name>/with its ownpackage.json, matching the sibling scripts convention:build/_build,lint/_lint,typecheck/_typecheck(plus_typecheck:nodeif the package needs a secondtsconfig.node.jsonpass),test/_test, and the rest ofturbo.json's task names.pnpm-workspace.yaml'spackages: ["packages/*"]picks it up automatically — no workspace-file edit needed just for registration. -
Declare sibling dependencies as ordinary semver ranges, never
workspace:*—linkWorkspacePackages: truelinks the workspace copy whenever the range is satisfied, falling back to the registry otherwise (see the root README's Dependency ranges between packages). -
Add the package's bare name to
pnpm-workspace.yaml'sminimumReleaseAgeExcludelist. Without it,pnpm install --frozen-lockfile(the mode CI runs) can reject the package's own same-day release the moment a sibling bumps to depend on it. -
Give it an
eslint.config.tsthat callspackageLintConfigfrom the root'seslint.shared.ts, passingtsconfigRootDir: import.meta.dirnameand — if the package is Worker-isomorphic, as every foundation and format-codec package is (see the root README's Conventions) —isomorphic: true, plus atest:workerssuite proving it at runtime. Don't copy a sibling's config and don't restate the Node-builtin ban: the shared helper derives that ban fromnode:module's ownbuiltinModules, so a package opts in with one flag rather than a hand-maintained module list that silently rots as Node adds builtins. Anything genuinely specific to the package goes through the helper's own options (additionalRestrictedImportPatterns,additionalIgnores,barrelPolicy,nonNullAssertion, …), not a second config block: flat config replaces a same-key rule rather than merging it, so a secondno-restricted-importsof your own would silently switch the isomorphism ban back off while still reporting your rule as working. -
If the package publishes and exposes types, add
"_typecheck:attw": "attw --pack"to its scripts and include_typecheck:attwin its owntypecheckscript's turbo run list. Turbo silently skips a package missing the script — no error, no warning — so a package that forgets this step gets no export-resolution check at all, forever, until someone happens to notice. -
Configure the package's npm trusted publisher to point at this repository's own
.github/workflows/ci.yml, not wherever it published from before. For a package absorbed from a former standalone repo, this means updating whichever trusted-publisher configuration it already had on npmjs.com — OIDC publishing from this workflow fails outright (ENONPMTOKEN No npm token specified) until it's updated, and because packages release in one topological pass that aborts on the first failure, a misconfigured package sitting early in that order blocks every other package's release too, not just its own. Do this before the package's first release attempt here, not after.A genuinely new package (never published anywhere before) hits a different version of the same problem: npm has no settings page to configure a trusted publisher on until the package has been published at least once, so its very first release-job publish attempt fails outright with an OIDC token exchange 404, then
ENONPMTOKENon the fallback, since this workspace configures noNPM_TOKENto fall back to. Thenpm registrationCI job (.github/scripts/check-npm-registration.ts) catches this on the pull request that adds the package, rather than letting it surface as a red post-merge Release job: it fails whenever a package the pull request's own diff touches has never been published. Bootstrap the package with a one-time manualnpm publishfrom an authenticated maintainer account (or thesetup-npm-trusted-publishtool), then configure trusted publishing on its new npmjs.com settings page exactly as above, before merging. -
List the package in the root README's package table (and its Worker-isomorphic list in Conventions, if applicable), and run
pnpm installat the root so the lockfile picks it up.