Config Reference
Every key vinaya.config.json accepts, rendered from the one authored registry that documents the schema — never hand-transcribed, and mechanically proven complete against it.
An adopter starting from nothing can write a working vinaya.config.json from this page alone.
rings
object (optional)Declarative booleans for the two opt-in accelerator rings. Ring 0 (git hooks) and the CI/branch-protection guarantee are never represented here — they are universal, not configurable — so this object controls only whether the optional rings are on.
{
"rings": {
"ring1_forgeWriteInterception": true,
"ring2_asyncAudits": true
}
}rings.ring1_forgeWriteInterception
booleanWhether pr/issue create|edit validate a body against briefSchema before any gh write.
{ "rings": { "ring1_forgeWriteInterception": true } }rings.ring2_asyncAudits
booleanWhether the async, forge-scheduled mechanisms (vinaya archive, vinaya audit’s dead-branch-push and direct-main-push detection) run.
{ "rings": { "ring2_asyncAudits": true } }checks
Record<string, CheckEntry> (optional)Custom-check registration, keyed by check name. Each entry produces the exact same CheckSpec shape the built-in registry does — no field either side can carry that the other cannot.
A key that exactly matches a core check id is an **override attempt** (replaces the core check, contract-validated, fail-closed if malformed); any other key must be namespaced <yourname>/<id> (exactly one /, both segments [a-z0-9][a-z0-9-]*, the vinaya prefix reserved). A bare key matching no core id is a config error.
Globs (include) are permitted for scoping; conditionals (if/unless/except) are never part of this grammar. Registered from a repo-local vinaya.config.json only — a global ~/.vinaya/config.json’s checks key is stripped at load time with a loud stderr warning, never resolved.
{
"checks": {
"myteam/vocab-check": {
"run": "./scripts/vinaya-checks/vocab-check.ts",
"scope": "diff",
"include": ["**/*.md"]
}
}
}checks.run
stringThe executable path (or bare command on PATH) the runner spawns directly — never through a shell.
{ "run": "./scripts/vinaya-checks/vocab-check.ts" }checks.scope
'diff' | 'full'diff checks may be skipped by the runner under --diff-only when no changed file matches include. full checks (coherence, dispatch-readiness) always run — they read live forge state, not the local diff.
{ "scope": "diff" }checks.include
string[] (optional)Glob patterns that scope a diff-scoped check to changed files. Scoping only — never a conditional.
{ "include": ["**/*.md", "apps/**/*.ts"] }checks.args
string[] (optional)Extra argv elements passed to run, appended after the runner’s own fixed arguments.
{ "args": ["--strict"] }checks.timeoutMs
number (optional)Advisory to the check; the RUNNER enforces the actual deadline (kills the whole process group), never the check itself.
{ "timeoutMs": 30000 }checks.requiresOpenPr
boolean (optional)Marks a check that can only meaningfully evaluate once a pull request exists — it reads the real PR body/number, not local git state. The generated pre-commit/pre-push hooks (vinaya check --all --local) skip a check declaring this entirely rather than running it against a PR that cannot exist yet; CI (which only ever runs after a PR is open) always runs it for real.
Use for a custom check with the same shape as the core closes-n/test-plan checks — anything that would otherwise deadlock the first commit on a fresh branch by requiring PR content before a PR can exist.
{ "requiresOpenPr": true }checks.env
Record<string, EnvEntry> (optional)The per-check environment allowlist. A check’s child process receives ONLY a fixed safe baseline (PATH, LANG, HOME, HTTPS_PROXY, HTTP_PROXY, NO_PROXY, TMPDIR) plus whatever this declaration explicitly forwards — never the full parent environment.
A check that reads process.env/Bun.env/Deno.env directly with no env declared is invisible to the child process; vinaya doctor carries the permanent diagnostic for that gap. Four declared forms, below.
{
"env": {
"JIRA_TOKEN": { "anyOf": ["JIRA_TOKEN", "JIRA_PAT"] }
}
}checks.env.passthrough
trueRequired passthrough — forwards the caller’s own value for this key verbatim. Absent from the caller’s environment synthesizes a CheckError before the check ever spawns.
{ "env": { "GITHUB_TOKEN": true } }checks.env.optional
{ optional: true }Passthrough if set, simply absent from the child otherwise — never fatal. Use for a var the check’s own code already tolerates missing (e.g. a CI-only secret that would otherwise hard-block git push on a developer laptop).
{ "env": { "PR_BODY": { "optional": true } } }checks.env.anyOf
{ anyOf: string[] }An either/or requirement with no deeper fallback — at least one named member must be set in the caller’s environment, or a CheckError is synthesized before spawn. Every set member passes through under its own name.
The record key must be one of its own anyOf members, and at least 2 unique members are required (a single-member anyOf is just true under a different name). Adopter-facing only — no core check uses this form, since every core check’s env read already has a deeper fallback (a gh auth token subprocess, a default literal) that an anyOf’s hard pre-spawn failure would be wrong for.
{ "env": { "JIRA_TOKEN": { "anyOf": ["JIRA_TOKEN", "JIRA_PAT"] } } }checks.env.literal
stringSets the key to this exact value, never interpolated — spawn takes an explicit env object, no shell expands $VAR.
{ "env": { "NODE_ENV": "test" } }Never put a secret in a literal. env values live in a COMMITTED file, reviewed like any other code change — a literal is for fixed, non-sensitive values only (e.g. "NODE_ENV": "test"), never a token or credential. vinaya doctor warns on a high-entropy literal (looks like a leaked secret) and on a literal "true"/"false" string (almost certainly meant as the boolean passthrough form instead) as a backstop, but review is the real defense — do not rely on the linter to catch every case.
briefSchema
object (optional)The config-defined brief schema the forge-write commands (pr create|edit, issue create|edit) validate a body against, locally, before any gh write. Declarative only — a required section is either a named battle-tested builtin or a generic heading/field/phrase matcher; no conditional grammar.
{
"briefSchema": {
"pr": { "sections": [{ "builtin": "tier" }, { "heading": "Rollback Plan" }] }
}
}briefSchema.pr
{ sections: BriefSection[] } (optional)Required sections for a PR body — checked by vinaya pr create|edit before any gh write.
{ "pr": { "sections": [{ "builtin": "closesN" }] } }briefSchema.issue
{ sections: BriefSection[] } (optional)Required sections for an Issue body — checked by vinaya issue create|edit before any gh write.
{ "issue": { "sections": [{ "builtin": "issueRationale" }] } }managed
object (optional)The ownership manifest vinaya init writes and vinaya eject reads — machine-owned, never adopter-authored. It records exactly what the installer created (files, marker-delimited blocks inside adopter-owned files, created-if-absent labels) so eject reverses precisely: deleting only files it created, stripping only blocks it wrote, reporting labels for manual removal.
If this manifest is absent or corrupt at eject time, eject refuses rather than guessing at ownership. Hand-editing this block is not a supported workflow.
{
"managed": {
"version": 1,
"files": [".github/workflows/vinaya-checks.yml"],
"blocks": [],
"labels": ["tier:1"]
}
}principals
string[] (optional, min 1)GitHub logins trusted as THIS repo’s own principals — the only authors whose PR comments count as a review-gate verdict, and the only actors an actor-verified vinaya/waiver:docs/vinaya/waiver:review label trusts. Overrides the package’s hardcoded default principal (this monorepo’s own maintainer) entirely — a full replacement, not additive.
Repo-local only, same rule as checks: a global ~/.vinaya/config.json’s principals key is stripped at load time with a loud stderr warning, never resolved — who is trusted to approve merges must come from the reviewed, committed per-repo file, never a machine-wide personal config.
Without this key, vinaya’s hardcoded default principal is the only trusted author — which makes review-gate structurally unpassable on any repo that principal doesn’t personally review. Set this to your own team’s GitHub logins to make the gate passable on your repo.
Read from your repository’s DEFAULT BRANCH via the GitHub API — the value itself never comes from the pull request’s own checkout or local git state, both of which a pull request can rewrite. A PR that edits this field therefore takes effect only once it merges, never for itself. (The API call is addressed using the repository identity the Actions runner provides; that part is not a separate lever, for the reason in the next note.)
⚠️ **This field is only a security control if main has branch protection with the Vinaya check marked as a required status check.** Vinaya’s checks run in a pull_request-triggered workflow, which GitHub executes from the pull request’s own copy of the workflow file — so a PR can always edit or delete the job that runs them. What a PR cannot do is satisfy a required status check that never reports. Without branch protection, principals is a useful team convention, not an enforced boundary. vinaya init prints the exact gh api command to enable it, and vinaya doctor reports when it is missing.
**Set this on your default branch before you need it.** Because the value is read from the default branch, a pull request that introduces principals for the first time is still evaluated against vinaya’s built-in default — so its own author cannot yet approve it, and cannot self-waive either (the waiver labels resolve through the same list). Land it with the vinaya init install commit, or any push to the default branch, before enabling branch protection. Once it is on the default branch, ordinary review applies: adding a principal thereafter needs an existing principal’s approval, which is the point.
{ "principals": ["alice", "bob"] }vinaya check --plan --json
The resolved check registry's JSON envelope — what reading the config back out, after override/additive resolution, looks like. This page is its documented home.
| Field | Type | Meaning |
|---|---|---|
| schema | 1 | The envelope version. Additive evolution only — a field is never removed or retyped under the same version number. |
| checks | Record<name, { state, source, env, envAnyOf?, scope }> | The fully resolved check registry, keyed by name — every core and config entry, after override/additive resolution. |
| checks.<name>.state | 'default' | 'overridden' | 'additive' | `default`: shipped with Vinaya, unmodified. `overridden`: a config entry currently claims this (core) id and satisfies its contract. `additive`: a wholly new, namespaced entry. |
| checks.<name>.source | 'core' | 'config' | Where the resolved spec came from — `registry.ts` (`core`) or `vinaya.config.json` (`config`). |
| checks.<name>.env | Record<string, 'passthrough' | 'optional' | 'literal' | 'anyOf'> | How each declared env var resolves — never the actual value. A security reviewer auditing the plan needs to see "reads the caller’s real token" vs. "sets a fixed string," never the token or string itself. |
| checks.<name>.envAnyOf | Record<string, string[]> (optional) | Present only for `anyOf`-labeled env keys — the full member list for that key. |
| checks.<name>.scope | 'diff' | 'full' | The resolved check’s scope, echoed from its `CheckSpec`. |
| roles | { available: false, reason: string } | An explicit degraded placeholder — role resolution/registration is not implemented yet. Not the shape roles will render once that lands; do not build against this as a stable contract. |
| errors | ResolverFailure[] | Every `FAIL_CLOSED` entry (a bare key with no namespace matching no core check) — rendered inline, never dropped. Non-empty `errors` always exits non-zero; `--plan` never swallows a failure to render a clean-looking table. |