Skip to main content

.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. setup creates it, fix keeps it current, and its writtenBy/writtenAt stamps 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​

FieldTypeRequiredDescription
$schemastringnoURL of this schema; stamped on every write so editors validate the file.
versionintegeryesLockfile 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.
recordobjectyesThe 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.
rulesobjectnoThe 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​

FieldTypeRequiredDescription
configobjectyesThe resolved setup configuration this repo was scaffolded or audited with.
assetsobjectnoPreset 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.
writtenBystringyesPackage name and version that last wrote the record subtree.
writtenAtstring (date-time)yesISO 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):

FieldTypeRequiredDescription
projectNamestringyes
languagejs | swift | perl | pythonno
projectTypelibrary | web-app | node-api | nextjs-app | react-appyes
typescriptobjectyes
lintingobjectyes
formattingobjectyes
testingobjectyes
gitHooksbooleanyes
commitLintbooleanyes
semanticReleasebooleanyes
changesetsbooleanno
releasePleasebooleanno
oxlintbooleanno
securityAutomationbooleanyes
bundlertsup | esbuild | rollup | rolldown | vite | noneyes
treeshakeCheckbooleanno
publintbooleanno
badgesbooleanno
aiSetupbooleanno
turborepobooleanno
nxbooleanno
tailwindbooleanno
docsSitebooleanno
brandbooleanno
bunbooleanno
docsobjectno

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.

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/" }
]
}
}
FieldTypeRequiredDescription
namestringyesThe server name as it would appear in .mcp.json.
importancenice-to-have | important | criticalyesHow much of the repo's workflow assumes the server.
whystringyesOne 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" adds label (default needs-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 declared with its reason, and the summary carries a declared: N count. 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 doctor does not run — a typo, or a check that was renamed or removed — is reported as drift, 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​

FieldTypeRequiredDescription
agentUserstringnoLogin 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/repo string 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​

VersionChange
1Initial format.
2Added config.language (multi-language seam). Older files migrate to js on read.
3Added assets — pristine hashes of copied presets, so doctor can tell a local fork from a stale copy.
4Split 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.