.repo-tooling.json
.repo-tooling.json — two documents sharing one file: `record` is written by @rtorcato/repo-tooling (`setup` and `fix`) and stamped with provenance; `rules` is written by humans, reviewed in PRs, and never stamped. Both are read by `doctor`.
The file is two documents sharing one name, split by who writes them:
record— the tool-written half.setupcreates it,fixkeeps it current, and itswrittenBy/writtenAtstamps are provenance claims about exactly this subtree.rules— the human-written half: the repo's stated intent (mcp,exceptions,dependabot), edited by hand and reviewed in PRs. The tool carries it forward verbatim on every write and never stamps it.
doctor reads both. Commit the file — it is the repo's memory of its own tooling choices
and its standing rules.
Editor validation
Every written file carries a $schema pointer, so editors that understand JSON Schema
(VS Code out of the box) validate, autocomplete, and show these descriptions inline:
{
"$schema": "https://docs.torcato.dev/repo-tooling/schemas/lockfile.json",
"version": 4,
...
}
The schema itself is published at https://docs.torcato.dev/repo-tooling/schemas/lockfile.json. It is
generated from the Lockfile TypeScript interface (pnpm schema:generate), and CI fails when the
published copy drifts from the type — this page renders from the same file, so what you read here
is what your editor enforces.
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
$schema | string | no | URL of this schema; stamped on every write so editors validate the file. |
version | integer | yes | Lockfile format version (current: 4). v2 added config.language, v3 added assets, v4 split the file into record/rules subtrees; older files are migrated on read. |
record | object | yes | The tool-written record of what setup/fix last did. Only the tool writes here — the writtenBy/writtenAt stamps are provenance claims about exactly this subtree. |
rules | object | no | The human-written ruleset: the repo's stated intent, edited by hand and reviewed in PRs. The tool carries it forward verbatim on every write and never stamps it. |
Unknown fields are rejected (additionalProperties: false): the CLI rewrites the file from
scratch on every save, so any key it doesn't know is a key it would silently drop — except
rules, which is carried forward verbatim.
record — what the tool wrote
| Field | Type | Required | Description |
|---|---|---|---|
config | object | yes | The resolved setup configuration this repo was scaffolded or audited with. |
assets | object | no | Preset name → sha256 of the asset's pristine content at copy time. Lets doctor tell a deliberate local fork (file differs from this hash) from a copy the package has since moved past (file still matches, shipped asset doesn't). A preset with no entry is untracked, never drifted. |
writtenBy | string | yes | Package name and version that last wrote the record subtree. |
writtenAt | string (date-time) | yes | ISO 8601 timestamp of the last record write. |
record.config — the ProjectConfig
The full setup configuration, identical to what setup --config accepts
(its standalone schema is published at
/schemas/project-config.json,
printable with setup --config-schema):
| Field | Type | Required | Description |
|---|---|---|---|
projectName | string | yes | |
language | js | swift | perl | python | no | |
projectType | library | web-app | node-api | nextjs-app | react-app | yes | |
typescript | object | yes | |
linting | object | yes | |
formatting | object | yes | |
testing | object | yes | |
gitHooks | boolean | yes | |
commitLint | boolean | yes | |
semanticRelease | boolean | yes | |
changesets | boolean | no | |
releasePlease | boolean | no | |
oxlint | boolean | no | |
securityAutomation | boolean | yes | |
bundler | tsup | esbuild | rollup | rolldown | vite | none | yes | |
treeshakeCheck | boolean | no | |
publint | boolean | no | |
badges | boolean | no | |
aiSetup | boolean | no | |
turborepo | boolean | no | |
nx | boolean | no | |
tailwind | boolean | no | |
docsSite | boolean | no | |
brand | boolean | no | |
bun | boolean | no | |
docs | object | no |
The object-typed fields above (typescript, linting, formatting, testing) are small
enum-valued records — see the schema for their exact shapes. docs (url, deploy) records
where the docs site lives and how it deploys; see
Docs site.
rules — what the humans wrote
Everything under rules is edited by hand and reviewed in PRs. It is deliberately outside
the writtenBy/writtenAt stamps: the tool never writes a rule here, so it never claims
authorship of one — a hand edit to rules leaves the record's provenance true.
setup does write the empty containers, so a new repo has the shape in front of it rather
than a blank page — and doctor --rules-from has something local to compare:
"rules": { "mcp": { "recommended": [] }, "exceptions": {} }
Every value is empty on purpose. A default here would be this tool asserting a rule on a repo
whose humans have not stated one. An existing rules is carried forward untouched.
rules.mcp — recommended MCP servers
Advisory metadata about the MCP servers the repo's workflow assumes. Never an install directive: an entry may say what and why, and may not say how.
"rules": {
"mcp": {
"recommended": [
{ "name": "some-server", "importance": "important", "why": "edits the design files under design/" }
]
}
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The server name as it would appear in .mcp.json. |
importance | nice-to-have | important | critical | yes | How much of the repo's workflow assumes the server. |
why | string | yes | One line on what the server is for — the thing .mcp.json structurally cannot say. |
importance is one of nice-to-have, important, critical — it signals how much of the
workflow assumes the server, and nothing more. why is the one line that .mcp.json
structurally cannot carry.
doctor reports, informationally, which recommended names the repo-scoped .mcp.json does not
declare. It never installs or enables a server, and there is deliberately no command, args
or env here: MCP servers execute code, so the real config belongs in .mcp.json, which
carries Claude Code's own first-use consent prompt. The lockfile says what and why,
.mcp.json says how, and you say whether. User-scoped MCP config is machine-private and
is not probed at all.
rules.dependabot — what the workflow does with a safe PR
Sets what the generated dependabot-automerge.yml does with a patch or minor PR it would
otherwise auto-merge. Absent means automerge, so a repo that never sets it sees no change.
"rules": { "dependabot": { "onPr": "label", "label": "needs-review" } }
onPr: "automerge"(default) merges on green.onPr: "label"addslabel(defaultneeds-review) and never merges, so a reviewer or a review bot can pick the PR up by label.
Majors and consumer-facing bumps go to a human in both modes. doctor reports a workflow that
does not match the configured mode and fix dependabot regenerates it from the config.
rules.exceptions — declared deviations
A map from a doctor check name (the check string in doctor --json output) to the reason
this repo deliberately deviates. The reason is mandatory and non-empty — the schema rejects
an entry without one, so every deviation is argued in the PR that declares it.
"rules": {
"exceptions": {
"TypeScript": "this repo is the package; the tsconfig lives at src/cli/tsconfig.json"
}
}
Three rules keep it from becoming a mute button:
- Shown, never hidden. The check still appears in every report, as
declaredwith its reason, and the summary carries adeclared: Ncount. Only the exit code changes: a declared exception no longer fails the run. - A stale exception is itself a finding. An entry naming a check
doctordoes not run — a typo, or a check that was renamed or removed — is reported asdrift, so it can't silently do nothing (or silently stop suppressing). - Per-repo by construction. The file lives in the repo it excuses, so an exception that is legitimate here cannot leak into a repo where the same finding is real.
A bulk fix / fix --yes skips declared checks; a targeted fix <target> still applies,
since naming the fixer is an explicit override.
Deprecated fields
rules.brand and rules.docs (the docs-site and brand scaffolds) moved to
@rtorcato/shared-docs (#718). rules.aiLoop and
rules.requiredSkills moved to .repo-ai.json, owned by the optional
@rtorcato/repo-ai (see
Using with repo-ai). They still validate, so existing files keep
working, and are removed in the next major. Move them with npx @rtorcato/repo-ai fix config.
rules.aiLoop
| Field | Type | Required | Description |
|---|---|---|---|
agentUser | string | no | Login that in-flight work is assigned to, so `assignee` says whose turn it is. Must be an assignable collaborator; the skills verify that at runtime. |
rules.requiredSkills
An array of skill names, e.g. ["ai-loop", "ai-issue"].
Comparing against another repo
There is no guideline package, and there never will be: the rules are unique to each repo,
and a rule that travels as a dependency stops being the repo's own. Sharing a guideline means
pointing at a reference repo — one whose .repo-tooling.json you consider exemplary.
# Report how this repo's config and rules differ from another repo's
npx @rtorcato/repo-tooling doctor --rules-from owner/repo
The reference is read over gh (repos/owner/repo/contents/.repo-tooling.json), so it works
for any repo your gh login can see, public or private.
Three properties make this safe to point at a repo you do not control:
- Informational, always. Differences are printed in their own section, never as check
results — they are not
drift, they do not appear in the summary counts, and they cannot change the exit code. Two repos legitimately differ; the point is seeing where. - Read and report, never apply. There is no fixer, and nothing from the reference is ever written into your repo.
- The reference is untrusted input. The
owner/repostring is shape-checked before it reaches an API path, the response is size-capped, and the JSON is validated against the published schema before a single field is read. A missing, malformed, oversized or schema-invalid reference is reported plainly and the comparison is skipped — never as a finding about your repo.
Only the comparable half is diffed: record.config and everything under rules. The assets
hashes and the writtenBy/writtenAt stamps are excluded — they differ between any two repos
by construction, so including them would bury every difference that means something.
With --json, the comparison rides alongside the results under rulesReference, and only when
the flag is given.
Starting a new repo from another one
The same reference, the other direction:
npx @rtorcato/repo-tooling setup --from owner/repo -d ./my-new-lib
The wizard is seeded, not skipped: every question is still asked, with that repo's answers as the defaults instead of the built-in ones. Nothing is written that you did not see.
Only record.config crosses over. projectName is never seeded (a new repo is not the
reference repo), the record-side stamps are not copied (the new repo writes its own writtenBy
and starts with no assets), and rules stays behind — an exceptions entry excuses a
deviation in the repo that argued for it, so copying one would mute a check somewhere it is
still a real finding.
--from seeds the wizard, so it is ignored (with a warning) alongside --preset or --config,
which skip the wizard entirely.
Version history
| Version | Change |
|---|---|
| 1 | Initial format. |
| 2 | Added config.language (multi-language seam). Older files migrate to js on read. |
| 3 | Added assets — pristine hashes of copied presets, so doctor can tell a local fork from a stale copy. |
| 4 | Split the file into record (tool-written, stamped) and rules (human-written, unstamped) subtrees. Nothing renamed or dropped — the flat v3 fields moved into them. |
Older files are migrated in memory on read and rewritten at the current version on the next save.
The pre-rename .js-tooling.json is still read as a fallback and replaced on the next write.