Rules
How every repository builds, checks, deploys and releases. There's one way to do each thing. drift checks every repository against these rules and lists where each has drifted, and align brings them back in line. Usage says how to run them.
The repositories are the ones in REPOS, in src/survey.ts. Where this page gives a version, the real value is in the reference, and drift reads it from there.
A rule drift checks is a list item that opens with its id as a span, like [`scripts-verify`]{#scripts-verify}. An id is lowercase kebab case, its part and then its name, since an id can't hold a dot. Its first sentence is the rule, and the rest says why. drift prints the id on each line a repository drifted on, and craftz rules <id> prints the rule. The check, and the fix where one can be automated, are in src/rules.ts, under the same id. Some checks don't have an id yet, and they move over part by part.
Axes
What a repository must have follows from four axes. Each is read from where it already lives, so none is declared twice, and each varies on its own.
| Axis | Values | Read from |
|---|---|---|
| publish | package, or none | package.json isn't private |
| deploy | static, app, or none | the Worker config, and whether it has code of its own |
| audience | public or internal | Cloudflare Access, for each site |
| repository | public or private | GitHub |
- Publish. A package is published to npm, so its audience there is public. A private npm package would be a fifth value, and none is needed yet.
- Deploy. A site deploys to a Cloudflare Worker. One whose config names code to run, cf's
entrypointor wrangler'smain, is an app. One without is static, whatever builds its files, like a prose site or a Vite page. - Audience. A site is internal when Cloudflare Access covers its domain, as Internal sites says. Audience applies only to a site.
- Repository. Whether the code is public is separate from whether the site is. A private repository can have a public site, and a public repository an internal one.
A package can have a site, like markz and its docs. A repository with neither is scripts run on a laptop. Where two axes meet badly, the pair is allowed, and what's lost is said where it applies. npm takes provenance only from a public repository, so a package in a private one publishes without it.
Toolchain: Vite+
One vite.config.ts, run through vp, covers dev, build, format, lint and test. A package builds with vp pack, which is part of Vite+, so there's no separate bundler. defineConfig comes from vite-plus.
Vite+ ships its own build of Vite. Each repository points every vite at it with an override in pnpm-workspace.yaml, and turns off pnpm's peer checks for vite, since that build is an npm alias. Bump vite-plus and the override together, in every repository at once. A version that differs between repositories is the drift that makes Vite+ painful.
Formatting runs at oxfmt's defaults, but for one setting.
toolchain-wrap:fmtsetsproseWrap: "never", so each Markdown paragraph is one line that the editor soft-wraps. Nobody hand-wraps, and a changed paragraph shows in a diff as one line with its changed words highlighted."always"would reflow the rest of a paragraph when one word is added.@prosecomments stay hand-wrapped at 100 characters, since no oxfmt setting wraps comment text.toolchain-lint: lint is type-aware and type-checks too, socheckcovers types without a separatetsc.toolchain-types: everytsconfig.jsonturns on the same strict options:strict,noUncheckedIndexedAccess,noImplicitOverride,noImplicitReturns,noFallthroughCasesInSwitch,verbatimModuleSyntax,isolatedModulesandforceConsistentCasingInFileNames. The reference's tsconfig.json is the example. The rest of a tsconfig, likeincludeandtypes, is the repository's own.
The lint block in vite.config.ts:
lint: {
plugins: ["typescript", "unicorn", "import"],
categories: { correctness: "error" },
options: { typeAware: true, typeCheck: true },
},
craftz() in the config's plugins sets the fmt and lint blocks, so a config with it holds neither. It fills in only what the config leaves unset, and fails a config that sets one of them differently, naming the key.
toolchain-craftz: every repository on Vite+ depends on@amitkaps/craftzand putscraftz()first in its config'splugins, so its format, lint and site build come from the version it installs. craftz's own config imports the plugin from source. The fix adds both, and takes out thefmtandlintblocks where they're the ones the plugin sets.
The editor
toolchain-editor: every repository has the reference's .vscode/settings.json, the one Vite+ writes, and recommends the extensions its.vscode/extensions.jsondoes. A repository can add settings of its own, like base's for Svelte, andalignkeeps them when it writes the reference's.
The settings make oxc the formatter, on save, with its fixes. The extensions recommended are oxc's and Vitest's, by name, rather than Vite+'s extension pack, which only wraps the two and could add more without a change to these rules. The oxc extension then runs the repository's own vp fmt and vp lint, so the editor formats with the oxfmt Vite+ pins and the fmt block in vite.config.ts, the same as check.
Nothing about oxc goes in VS Code's user settings. A path to oxfmt there, or a config path, overrides every repository's own, and the editor formats differently from check.
Browsers
Code shipped to browsers works in the browsers people use, with a floor for each engine rather than a date. Vite's default, Baseline Widely Available, waits 30 months for every browser and is frozen at each Vite major. But only one engine holds people back that long.
Blink, which is Chrome, Edge, Opera, Samsung Internet and Android's WebView: the oldest Chromium released in the last 12 months. Chrome and Firefox release every two weeks now, so a count of versions shrinks each time they speed up, and a window of time doesn't. Samsung Internet runs on a Chromium about ten months old, which is why the window is a year.
Gecko, which is Firefox: the oldest ESR Mozilla supports. It's the oldest Firefox that still gets fixes.
WebKit, which is Safari and every browser on iOS, in-app browsers too: the newest release of the major two before the current one. An iPhone that a new iOS drops stays on its last major and keeps getting point releases, so that's the oldest Safari in use. Chrome on iOS and an app's WebView run this engine too, so it covers more people than Safari's own share.
toolchain-browsers: a build that bundles code for browsers targets these browsers, inbuild.targetfor a site Vite bundles from source and inpack.targetfor a package that isn'tplatform: "node".craftz()setsbuild.targetfor a static site with anindex.html. A package that runs in Node too adds its Node to the list.
The list is worked out from MDN's browser-compat-data, by src/browsers.ts, as of the date of the data. The package is one of craftz's own dependencies, so a craftz release moves it, and the floors move with it, with drift listing what then needs the new list.
A target only rewrites syntax, like CSS nesting or a regular expression's flags. It adds no polyfills, so it doesn't stop code that calls an API too new for a floor. Something newer than a floor, like ::highlight() on Firefox's ESR, is fine where it fails cleanly and the page still works. The floor is for what has to work. A prose site's page scripts and styles aren't built by Vite, so the target doesn't reach them.
pnpm and Node
package.json is the one place for both versions.
"devEngines": {
"packageManager": { "name": "pnpm", "version": "12.13.0", "onFail": "download" },
"runtime": { "name": "node", "version": ">=26", "onFail": "error" }
},
"engines": { "node": ">=26" },
"packageManager": "pnpm@12.13.0"
versions-pnpm: pnpm is pinned exactly, withonFail: download. Whatever pnpm is installed globally, pnpm fetches and runs the pinned one, so a new pnpm release never breaks a checkout. CI readspackageManager, so keep the two equal.versions-node:devEnginesasks for the reference's Node withonFail: error, andenginesasks for the same. The wrong Node stops loudly. A package supports the Node it's built and tested on, and nothing older, so CI runs on one Node. Node 26 is the LTS release from October 2026. When the next LTS arrives, every repository moves to it at once, and for a package that's a breaking release.
Bump deliberately, everywhere at once. Being on the latest pnpm doesn't matter. Being on the same one does.
Other versions
The tools every repository shares are on one version, written the same way, so moving one never leaves two repositories to reconcile. The reference's package.json has the version, or the catalog in its pnpm-workspace.yaml does for a tool it doesn't install. npm isn't read. A new release is drift only once the reference moves to it, past pnpm's one-day wait.
versions-types-node:@types/nodeis the reference's, where a repository has it. It's the major of the Node thatenginesallows, so code can't use newer APIs.versions-vite-plus: every repository is on the reference'svite-plus, with the override at the same version and no directvite. Toolchain says why.versions-typescript:typescriptis the catalog's, where a repository has it.versions-prose:@amitkaps/proseis the reference's, where a repository has it.versions-markz:@amitkaps/markzis the reference's, where a repository has it.versions-craftz:@amitkaps/craftzis at craftz's own version, as a caret range, so a release of craftz is what moves every repository to its rules.versions-cf:cfis the reference's, where a repository deploys with it.versions-cf-vite-plugin:@cloudflare/vite-pluginis the reference's, where a repository deploys with cf, at the build that goes withcf.versions-beta: onlycfand@cloudflare/vite-pluginare on a pre-release, each pinned to an exact build.versions-wrangler:wrangleris the catalog's, while a repository is still on it.versions-publint:publintis the catalog's, where a package has it.
A beta's tags don't say which build goes with which, so it's moved by hand. align lists one in its pull request once npm's latest is a release, and then it leaves versions-beta. Development says how.
A repository's own libraries, like svelte or micromark, are its own to choose. align moves them within their major version, and lists a new major in its pull request for review.
pnpm-workspace.yaml is a settings file, not a workspace. Every repository has the same three settings: the cooldown, allowBuilds and the Vite+ override. allowBuilds lets esbuild and workerd run their install scripts, when wrangler or the Cloudflare plugin is installed.
versions-cooldown: pnpm's one-day wait on a new version is kept, with only@amitkaps/*excluded. A bad release can be caught before it's installed, and our own skip the wait, since each reaches npm only after the owner approves it.
Held
When a tool a repository depends on can't support a rule yet, the check is held. drift prints it with its reason, and doesn't count it as drift. Each hold says when to look again, like svelte-check accepting TypeScript 7. Holds are listed in src/survey.ts.
A hold is for what a tool can't do, never for a preference. The point of one set of rules is that a choice like Vite+ is made once and paid for once, not argued again in each repository. A hold whose check stops drifting fails drift until it's removed, so none outlives its reason.
Scripts
Every repository uses these names, and runs them with pnpm run … in anything automated.
| Script | Does |
|---|---|
dev |
the dev server, or vp pack --watch for a package |
build |
the production build |
preview |
a site's: serves what build wrote, with vp preview |
check |
format, lint and types: vp check, and a framework's own checker |
fix |
writes the format and lint fixes: vp check --fix |
test |
the tests, when there are any |
verify |
check, prose check, test and build, then the site if there is one |
ship |
uploads what verify built: craftz deploy |
prose |
reads the repository as a document: prose, so pnpm prose build works |
pr |
opens a pull request that merges itself once CI passes |
gone |
deletes the local branches that merged |
craftz |
runs craftz, so pnpm craftz rules works |
scripts-required:check,fix,verifyandproseare in every repository,devandbuildin every site and package, andshipin every site. A tool has nothing to serve or build, so itsverifyischeck,prose checkandtest.testis there when there are tests.scripts-check: on Vite+,checkrunsvp check. A framework can add its own steps around it, like SvelteKit'ssvelte-kit syncbefore andsvelte-checkafter.scripts-fix: on Vite+,fixis exactlyvp check --fix.scripts-one-checker: on Vite+, there are nolintorfmtscripts.checkandfixcover both, and a framework adds no second formatter or linter.scripts-verify:verifyrunspnpm prose check. It fails on what the text gets wrong, whichprose buildonly prints, so a mistake in a doc or a comment stops a merge the way a lint error does.scripts-prose:proseis exactlyprose, sopnpm prose buildworks. prose's own repository runs its command line from source.scripts-preview:previewis a site's, the same as the reference's. A package, whose build isdist/, has none.scripts-pr:priscraftz pr, and there's noscripts/pr.ts.pnpm pr "<title>"starts a branch when it's onmain, commits, runsverify, pushes, opens the pull request and turns on auto-merge. A package's pull request takes--label <label>, and stops without one, before anything is committed.--waitwaits for the merge, updating the branch when another merge leaves it behindmain, then switches tomainand pulls. craftz's own repository runs it from source.scripts-gone:goneiscraftz gone, and there's noscripts/gone.ts. It deletes each local branch whose branch on GitHub is gone, which is each one that merged, and works where no git alias is set, like an agent's machine. craftz's own repository runs it from source.scripts-craftz:craftzis exactlycraftz, sopnpm craftz rules,pnpm craftz checkand the rest work, aspnpm prosedoes for prose. craftz's own repository runs it from source.scripts-ship: a site'sshipiscraftz deploy, which reads the Worker config and runscf deploy --prebuiltfor a static site, orwrangler deployfor an app. The Worker's deploy command ispnpm run ship, so how a site deploys moves with craftz, not with each repository. craftz's own repository runs it from source.scripts-pnpm-names: no script is nameddeploy,publish,audit,ci,pipelineorpack. Those are pnpm's own commands, andpnpm <name>skips a script by one of those names.
Text the repository didn't write, like a test corpus or a fixture that's wrong on purpose, is marked linguist-vendored in .gitattributes, and prose check skips it. Any other script is the repository's own, like a package's size or fuzz.
verify is what CI runs and what Cloudflare runs before each deploy, so the two can't disagree. ship never builds.
A lifecycle script, one pnpm runs on its own like prepare on install or prepack before packing, only does what another step needs first. base's prepare generates the types SvelteKit and the Worker need, so a fresh clone type-checks. A package's prepack is pnpm run build, so a tarball never ships a stale dist/. Neither builds a site or deploys.
Cloudflare
A repository with a site deploys it to a Cloudflare Worker with static assets.
cloudflare-config: a site deploys withcloudflare.config.ts, cf's typed config. It holds the Worker's name, compatibility date, domain, addresses and asset handling.cloudflare-addresses: a site's config turns offworkers.devand preview URLs, so it answers only on its domain. Nothing uses the other addresses, and each would serve the site outside anything that covers the domain, like Access.cloudflare-date: a site'scompatibilityDateis the build date of the workerd that the reference's cf installs, and it sets no Node.js compatibility flag. That's the newest date the local runtime knows, so dev and tests run what production does, and from 2026-08-04 the date turns on Node.js compatibility itself.cloudflare-wrangler: a site cf can't deploy yet keepswrangler.toml, neverwrangler.jsonc. prose reads comments in TOML but not in JSONC.cloudflare-addressesandcloudflare-datereadcloudflare.config.ts, so they apply once a site moves to it.- Deploys: the Worker's Git integration (Workers Builds) builds and deploys
main. There's no deploy step in GitHub Actions, and no API token or secret. - The Worker's settings are the same on every Worker. Cloudflare's config has no field for them, so
alignwrites them withcf, anddriftchecks them whencfis logged in. Only connecting the Worker to its repository is left to Cloudflare's dashboard.
Build command pnpm run verify
Deploy command pnpm run ship
Root directory /
NODE_VERSION 26
Build cache on
Previews off
verify fails on a failing check, so a merge that breaks one doesn't deploy. Previews stay off because CI already checks every pull request. If a public repository turns them on, it sets the preview deploy command too: pnpm exec wrangler versions upload, or pnpm exec cf workers versions create --prebuilt.
A prose site
A site that's the folder prose build writes, like a package's, deploys with cf through Vite. cf's config has no field for an assets folder, so Vite hands the folder on.
export default defineConfig({
publicDir: ".prose",
plugins: [cloudflare({ assetsOnly: true })],
});
With craftz() in the plugins, the publicDir line goes, since the plugin sets it for a static site with no index.html.
cloudflare-prose-site: a prose site serves.proseas Vite'spublicDirthrough the Cloudflare plugin withassetsOnly,verifybuilds it afterprose build, and.gitignoreleaves out.cloudflare. With noindex.htmlat the root, the plugin builds an empty entry and deletes it, so only the folder's files are deployed.
In a site, the build after prose build is pnpm build, which is vp build. In a package, build is vp pack, so verify ends with vp build itself. Anything that adds to the site, like markz's Quality page, runs before it. ship is craftz deploy, which runs cf deploy --prebuilt to upload .cloudflare/output/.
An app
A site that needs code on the server starts from base: SvelteKit with adapter-cloudflare, which writes a Worker as well as the files. It's kept for when one is needed. cf can't deploy what the adapter writes yet, so base stays on wrangler.toml and wrangler deploy, held, and its check runs wrangler types for the Worker's types.
Internal sites
Any site can be public or internal, in a public repository or a private one. An internal site is one that Cloudflare Access covers, and it's deployed like any other.
- Access covers its domain. A Cloudflare Access application on the site's domain, with a policy that lets in only who it's for. Access is turned on once per account, in the Zero Trust dashboard.
- Its other addresses are off, as on every site, by
cloudflare-addresses. Access covers the domain, not the Worker, soworkers.devor a preview URL would serve the site without a login.
The config says so in its @prose comment. With cf logged in, drift looks up Access for every site's domain. An application with no policy has drifted. With no application, the site is public, and drift says so on its own line, without counting it, so a site meant to be internal doesn't go public unseen.
GitHub
Every main takes changes the same way. Work happens on a branch, in a pull request, and ci has to pass on a branch that's up to date with main. The pull request is squash-merged, so main reads as one commit per change, and nobody pushes to it, admins included.
github-rules:mainhas one ruleset, namedmain, from .github/ruleset.json. It requires a pull request with no approvals, since there's one maintainer, and thecicheck. It keeps history linear, and blocks force pushes and deleting the branch. Nobody bypasses it.github-actions: every workflow uses the actions at the versions the reference's do, and there's no Dependabot. A repository can add steps of its own, but the versions move in the reference first, like every other version.- Merge settings: from .github/settings.json. Squash is the only merge, a branch is deleted once it's merged, and auto-merge is on.
craftz settings writes both for one repository, and asks before removing classic branch protection. align writes both to every repository or to the ones named, and removes classic protection without asking. drift checks the rules, which GitHub shows to anyone. Without a login, GitHub answers 60 requests an hour, so drift uses gh's login when there is one, and a check the limit stopped shows as not checked, not as drift. With that login, it checks the merge settings too, which only the owner can read.
Agents
agents-sections: everyAGENTS.mdopens with the same two sections as the reference's, word for word, andCLAUDE.mdholds only@AGENTS.md, so Claude Code reads the same file.
"craftz" points at these rules, says to read them with craftz rules, and says how changes reach main. "Prose" says to run prose rules, which prints the rules in prose's usage as the installed version has them, so an agent reads them without a fetch. The rules live there, once, and aren't copied into each repository, since the copies had already started to differ. It also says to run prose style on the files a change touched. That's advice, so it isn't a script or a check. The rest of an AGENTS.md is the repository's own.
Docs
Explanations live in @prose comments, beside the code or setting they explain, and docs that span files live in docs/. Both are written in markz's Markdown, and pnpm prose reads them as one document. package.json has no comments, so its scripts are explained in these rules. A hand-written .html page ships its comments, so it has no prose.
Every repository has the same docs, so anyone, or any agent, finds the same things in the same places.
docs-files:docs/hasdesign.md,plan.md,development.mdandlessons.md, and a package's hasusage.md. Each is in thenavofdocs/README.md, andREADME.mdlinks todocs/.
| File | Holds |
|---|---|
README.md |
what it is, how it's used, and a link to docs/ |
docs/README.md |
the docs in reading order: nav in its metadata, and a line for each |
docs/design.md |
what it is, why it's built this way, and what it isn't |
docs/plan.md |
where it is, and its own next steps, later and open, not other repositories' |
docs/development.md |
only what's its own: its extra scripts, its site, beyond these rules |
docs/lessons.md |
what building it taught, each lesson what happened and what to do about it |
docs/usage.md |
a package's install and use, and a section for agents |
The README is the site's home page when prose builds it, so it reads as one. It says what it is, gives a short example, and lists the commands a user runs, like a package's command. The repository's own scripts, like dev and verify, are the shared ones, and are left to development.md, which links these rules for the shared toolchain, scripts and workflows rather than describing them.
A site has no usage.md, since the site is what people use, unless it has commands to run. Any other doc is the repository's own, like markz's grammar, and goes in the nav too. development.md links to these rules for what's shared rather than repeating it. docs-files's fix writes a first version of each one that's missing, and adds it to the nav, so new starts with all of them.
The docs are published at /docs/<file>.md, so a link to one works on GitHub and on the site alike.
- A package's site is
prose build, the repository read as a document, with nothing added to it. Its docs are in the bar, and its code is a page away. - A site shows its docs its own way, read from
docs/, so GitHub and the site have one set of files. base renders them with SvelteKit, as the starter it is. - A page the build fills in is a doc with a section for it. The doc holds the words and an empty
{@section}for each table, so prose builds it with its bar and styles. A script fills the sections in the page prose wrote, and a```=htmlblock holds its styles.
Releases
markz, prose, pagez and sitez are packages, published to npm. A package is any repository whose package.json isn't private, and every one releases the same way.
releases-files: The release files are the reference's package/, copied into the package unchanged: the release workflow and.github/release.yml, which groups the notes. They name no package, so the copies stay identical and the survey checks them word for word. CI has the reference'sLabelstep, word for word, and runs again when a label is added or taken off.releases-packing: A package packsdistwith publint.filesis["dist"], followed only by what the package reads as it runs, like craftz'sdocs/rules.md.prepackispnpm run build, andpublishConfig.accessispublic. publint runs insidevp pack, withpack: { publint: { strict: true } }andpublintas a dev dependency, sobuildfails on a package npm would serve badly, and there's no separate publint step.releases-types: Types come from Oxc. The pack block setsdts: { generator: "oxc" }, sovp packwrites the.d.tsfiles without the TypeScript compiler, and fails on an export whose type isn't written out. It's set there, not asisolatedDeclarationsintsconfig.json, which the editor would apply to tests and config too. A package has notypescriptdependency, like a site:vp checktype-checks with tsgolint, which Vite+ installs.releases-bundled: Our own packages are bundled, not depended on. prose and sitez have markz as a dev dependency, andvp packputs it insidedist/. So a package's users never install a second markz, and each package releases on its own, in any order. Picking up a new markz takes a release of the package that bundles it.
To release, run release with the package and its new version.
release sitez 0.5.0 --notes "What to change when you upgrade …"
It opens a pull request that bumps version, from a branch named release-X.Y.Z, titled vX.Y.Z and labelled internal, and turns on auto-merge. Its description, down to the first --- line, is the release's summary, like what to change in a breaking release. Without --notes, the editor opens for it.
Merging a new version is the release, and nobody tags by hand. When a merge changes package.json, the workflow looks for a vX.Y.Z tag for its version. If there isn't one, it runs pnpm run verify, packs the tarball and stages it on npm with trusted publishing. It then tags the merged commit and publishes a GitHub Release, with the summary above notes generated from the merged pull requests. If a run fails, rerun it, since a version already on npm is skipped.
You approve the staged version with 2FA, in the Staged Packages tab on npmjs.com or with npm stage approve <id>. That step stays by hand, so a merge alone can't put a version in front of users. A version with a pre-release part, like -rc.0, goes to npm's next tag and is marked a pre-release.
Release notes
There's no changelog file. Each pull request carries one label, which files it under a heading in the notes.
releases-labels: A package has the labels its release notes are grouped by, from .github/labels.json.aligncreates them.
| Label | Heading | For |
|---|---|---|
breaking |
Breaking | a change that needs users to update their code |
added |
Added | a new feature or warning |
fixed |
Fixed | a bug fix users would notice |
improved |
Faster and smaller | the same behaviour, faster or smaller |
docs |
Documentation | documentation readers use |
internal |
left out | tests, tooling, site and lessons |
A pull request's title is its line in the notes, so a user-facing one is written for the package's users. ci fails until a package's pull request has exactly one of these labels, and adding one runs it again, so a release's notes are right when it's tagged, not fixed after. A site or a tool has no release notes, so its pull requests need no label.
Setting up a new package
new copies in the reference's package/ files, through releases-files's fix, and align <repo> makes the labels. A package that isn't on npm yet doesn't release on a merge, since npm trusts the workflow only once the package exists. On npmjs.com, add a trusted publisher for the repository and the workflow release.yml, with direct publishing and dist-tags left unchecked, so staging is all it can do. npm may not take that before the package exists. Then publish the first version by hand, from a folder outside the repository, since npm refuses to run where devEngines names pnpm.
pnpm pack && cd /tmp && npm login && npm publish ~/code/<repo>/amitkaps-<repo>-<version>.tgz --access public
The next merge that changes package.json finds that version on npm with no tag, so it skips staging and tags it, with a GitHub Release.
CI
GitHub Actions runs one job, named ci, on pull requests and on main. It installs with pnpm install --frozen-lockfile and runs pnpm run verify, plus whatever a package adds, like publint or a size budget. In a package, it first checks the pull request has one release-notes label, and it runs again when a label changes. The main ruleset requires ci. Cloudflare's own check on each commit isn't required, since it runs after the merge.
Repositories
Every repository that follows these rules is in REPOS, in src/survey.ts, so drift checks it. A repository joins one of two ways, and both end the same: in REPOS, protected, and passing drift.
Bringing one in
- Add it to
REPOS, in a pull request to craftz.driftthen lists everything it's missing. - Fix what drift lists, in the repository's own pull requests. Where a tool can't meet a rule yet, add a hold rather than a workaround.
- Connect a site's Worker to the repository, in Cloudflare's dashboard.
- Run
align <repo>, for the branch rules, the merge settings, a package's labels and the Worker's build settings. A package still needs its trusted publisher on npm.
Keeping them in line
drift reads and align writes, each for every repository or the ones named.
- drift checks every repository's files, from GitHub's
main, or from a folder named on the command line, to see a fix before it's pushed. Withghlogged in it checks the merge settings too, and withcfthe Workers' build settings, and without a login it says what it couldn't check. - align brings each repository in line with the reference, from a fresh clone of each
main. Every shared tool, the override and the editor settings come from the reference, and each repository's own libraries move within their majors. A pull request opens whereverifypasses, with auto-merge on, and lists any new major left for review. GitHub's settings and the Worker's are written directly, since there's no file to review.
A change to the rules, like a new check, is a pull request to craftz, and the next align carries it.
Following it unlisted
A private repository can follow these rules without joining REPOS, since craftz is public and would name it. drift <folder> checks it from disk, everything but its branch rules and labels, which align <repo> still writes. Where it can't follow a rule, its own docs say which and why.
Starting one
new starts a repository that already follows these rules, in the folder next to the current one.
new <name> --package --description "What it is, in a sentence." --push
new <name> --package --site static --description "What it is, in a sentence." --push
new <name> --site static --internal --description "What it is, in a sentence." --push
new <name> --private --description "What it is, in a sentence." --push
The flags are the axes. --package publishes it and --site static deploys a static site, and with neither, it's scripts run on a laptop. --private makes the repository private on GitHub and leaves it out of REPOS. --internal says in a site's config that it's behind Access, and the Access application is the step left to do by hand.
It writes only what's the repository's own: its name and description, its source, its Vite config and strict types, and CI. Each is built from the reference's files rather than a template. Then it runs every rule's fix, the ones align runs. They write the rest: the versions and engines, the shared scripts, the editor settings, the cooldown, the shared AGENTS.md sections, a package's release files and packing, and a first version of each doc. So a new repository can't follow the rules any less than align makes an old one. Then it installs, formats with fix, runs verify and makes the first commit. --push creates the repository on GitHub, pushes that commit and writes its GitHub settings, as align does. That first push is the only one to main the rules allow, since the rules aren't on yet. It prints what's left: adding the repository to REPOS, and the Worker or npm's setup. A SvelteKit app starts from base instead, and is brought in like any other.
new's test generates a package, a static site, a package with a site, scripts in a private repository and an internal site. It checks each against the survey and runs its verify, so the generator can't fall behind the rules.