Docs

CLI

Harness commands

The vinaya commands you run, grouped by job: install and maintain, gates, the Log, Studio and rulings, and trying it out. Commands the Operator and the agents call are not listed.

Reference
vinaya
npx

Install and maintain

quickstart

$ npx @attalabs/vinaya quickstart

Guided wizard: init, doc-owners, project, commit, demo break, doctor, push — one command

Calls init's own diff-and-confirm flow unchanged (pausing on Enter before the diff prints, so its own step header isn't scrolled off by a long diff), then Y/n-prompts through the workarounds a guest used to run by hand: binding .vinaya/doc-owners pairs (bad input offers a retry instead of silently skipping, and a pointer that doesn't exist on disk is refused outright — both loop across as many pairs as the guest wants, not just one), registering tracked projects (init product, same retry/loop shape), committing the install, running demo break as proof the gates actually work (default yes — the one step this wizard makes hardest to skip), running doctor, and pushing. Each declined prompt skips only that step; the install commit itself is never prompt-gated — it just no-ops when there is genuinely nothing to commit.

Never reimplements or edits init/init product/demo break/doctor — it only calls their existing, unmodified entry points in sequence.

init

$ npx @attalabs/vinaya init

Install Vinaya's git hooks, CI workflow, and starter config (diff-and-confirm, non-destructive)

It detects your repo, prints the complete diff of every intended change, and waits for your confirmation before installing anything. --dry-run prints that same diff and installs nothing. Nothing ever runs automatically on package install.

It installs one CI workflow that runs vinaya check --all --diff-only, alongside your existing workflows — refusing to overwrite rather than touching foreign content already at that path. Git hook stubs invoke the vinaya binary directly; if a hook already exists, it appends a delimited managed block, shown verbatim in the diff first, rather than overwriting it.

vinaya.config.json is seeded with a starter ruleset extracted from Vinaya's own battle-tested gates, not invented defaults. Issue and PR templates carrying the brief schema are added alongside your own; tier and needs:*-input labels are created only if they don't already exist — your existing labels are never modified.

Two empty folders — vinaya/checks/ and vinaya/roles/ — are scaffolded alongside the config, each held open by a placeholder file (git does not track empty directories). vinaya new noop-check and vinaya new role write their real output into these folders later; both placeholders are recorded in the ownership manifest, so eject removes them exactly like everything else init created.

The generated workflows reach the vinaya binary through npx @attalabs/vinaya, with no build step — unless your repo vendors the CLI itself (a workspace member declaring the name @attalabs/vinaya), in which case npx would resolve to that unbuilt local member instead of the registry. init detects that at generation time and writes workflows that build and run your own copy by path instead, so your CI exercises the code in the pull request. upgrade and doctor make the same determination, so regenerating is stable.

The recommended branch-protection command is printed for you to run yourself — it is never applied, and your PATH is never touched. eject removes exactly the managed block it owns, or a whole file only if init created it.

--dry-runPrint the full diff without installing anything
--yesSkip the confirmation prompt

upgrade

$ vinaya upgrade

Regenerate hooks, workflow, and config to the current contract version (diff-and-confirm)

When claude is a selected agent vendor, also retrofits the Claude Code Stop hook (.claude/hooks/track-transcript.sh, .claude/settings.json) the same way init installs it on a fresh repo — for a repo that ran init before this hook existed, closing the gap that left token rows reading —/—/— with no operator ever told to re-run init. Never overwrites a foreign .claude/settings.json (refuse-if-foreign, same as init); appends to a foreign Stop-hook script rather than replacing it (managed-block, same discipline as the git hooks).

--dry-runPrint the full diff without regenerating anything
--yesSkip the confirmation prompt

doctor

$ vinaya doctor

Diagnose hook, workflow, and config health — report only, never mutates

Carries the same env-declaration diagnostic vinaya check warns with — permanently, at info severity, not just ahead of the spawn-default flip — plus a warn-severity lint over suspicious env literal forms (a stray "true"/"false" string, or a high-entropy literal that reads like a leaked secret committed to config).

Also carries the two permanent checks-classification diagnostics: a config key that REPLACES a core check (warn), and a bare un-namespaced key that is REJECTED (error, naming the rename requirement). These are the reason a config vinaya check now refuses outright is still diagnosable — the refusal runs nothing, so doctor is the surface that explains why.

--jsonEnveloped JSON output (schema: 1)

eject

$ vinaya eject

Remove every Vinaya-installed artifact, restoring the repo to stock

--dry-runPrint the full removal diff without removing anything
--yesSkip the confirmation prompt

Gates

check

$ vinaya check

Run one check, or every registered check

Each spawned check's child process sees only a fixed baseline (PATH, LANG, HOME, HTTPS_PROXY, HTTP_PROXY, NO_PROXY, TMPDIR) plus whatever its CheckSpec['env'] declaration explicitly forwards — never the full parent environment. A required (true) or unsatisfied anyOf declaration missing from the caller's environment synthesizes a CheckError before the check ever spawns. Declare env (a core check's own registration, or vinaya.config.json's checks.<name>.env for a custom one) for any check that reads process.env/Bun.env/Deno.env directly — vinaya doctor carries the permanent diagnostic for one that doesn't.

The resolved registry IS what runs. A checks key that exactly matches a core check id REPLACES that core check (the core one does not run); any other key must be namespaced <yourname>/<id>. Anything the resolver cannot classify — a malformed entry, a bare un-namespaced key matching no core id, a duplicate id — makes vinaya check refuse the ENTIRE run: exit 1, nothing executes, never a partial ruleset and never a core-only fallback. vinaya doctor carries the permanent diagnostic for each rejected entry, so a refused config is still diagnosable.

--plan composes with --json. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A FAIL_CLOSED entry always renders inline rather than being dropped, and exits non-zero. --plan and execution read the same resolution, so what the plan prints is what runs.

--local exists because a requiresOpenPr check (the core closes-n/test-plan, or a custom check declaring the same field) can only evaluate for real once a pull request exists — the generated pre-commit/pre-push hooks pass it so the first commit on a fresh task branch is never asked to satisfy a PR-body field before a PR can possibly exist. CI's vinaya-checks.yml omits it, so these checks always run for real once a PR is open.

--plan's roles half resolves vinaya.config.json's roles block against bundled doctrine — same override/additive shape as checks, with its own RENDERS AS column (the registry key and the role's own role_id differ for an additive entry) and a GATING column (core vs. inert — an additive role carries no core ACTIONS wiring). roles.available is false only when no bundled doctrine can be found next to this CLI install.

--allRun every registered check instead of one named check
--jsonEnveloped JSON output (schema: 1)
--diff-onlyScope diff-declared checks to changed files
--localSkip every requiresOpenPr check (closes-n, test-plan) — set by the generated hooks, never by CI
--parallel[=n]Concurrency cap (default: cpu-derived)
--planPrint the resolved check registry and the resolved `roles` registry (default/overridden/additive) without running anything

new check

$ vinaya new check

Scaffold a custom check into ./scripts/vinaya-checks/

Takes the REGISTRATION KEY, not a bare name: vinaya new check <yourname>/<id> writes ./scripts/vinaya-checks/<id>.ts and prints the namespaced checks entry to paste. It refuses a bare, un-namespaced name — vinaya check refuses its entire run over a key it cannot resolve, so scaffolding one would brick every check invocation in the repo — and refuses a core check id, since registering one REPLACES that core gate and a scaffolded stub is never what an adopter means by that.

new noop-check

$ vinaya new noop-check

Scaffold an explicit no-op into vinaya/checks/ that silences a core check

Takes a CORE check id — the opposite of what new check accepts, which refuses one. vinaya new noop-check <core-check-id> writes vinaya/checks/<id>.ts, an explicit, contract-satisfying no-op (always exits 0, emits no findings, carries a comment marking the silencing as intentional) and prints the checks entry that REPLACES the named core check with it. This is the only sanctioned way to silence a core check.

waiver

$ vinaya waiver

Apply the actor-verified 'vinaya/waiver:docs' or 'vinaya/waiver:review' label after prompting for a reason

Applies the label via gh pr edit --add-label, under the invoking human's own authenticated gh identity — this command never fabricates an actor. A waiver is never an agent-emittable string: not a PR body field, not a commit trailer, not a comment — the label plus its own labeling-timeline actor is the only mechanism ring 1 honors.

--print-only prints the exact gh pr edit/gh pr comment commands and runs nothing — for a human who wants to run them itself, or a CI/non-interactive context where an agent session should never be the one applying its own waiver.

--reasonThe waiver rationale, posted as a PR comment (prompted for if omitted)
--print-onlyPrint the exact `gh` commands instead of running them

Log

log selftest

$ vinaya log selftest

Prove log delivery works end to end: send one marked test event and read it back — PASS (exit 0) or FAIL with the one reason (exit 1)

Resolves the destination exactly as an UNATTENDED review-loop run does in this repository — the same trust-anchor read and credential lookup, forced unattended so running it by hand proves the launch path's own resolution, not a human's. It sends one test event to the configured logs.url server, then reads it back from the server by the event's own id through the server's cursor read.

PASS (exit 0) only when the event round-trips. Otherwise FAIL (exit 1) with the ONE reason that stopped it: no server is configured, the default branch's config could not be read to confirm the url (and why), this host holds no ingest or read credential, the server refused with its status, or the event could not be read back. It never prints a credential value.

The read-back needs a READ credential distinct from the write-only ingest one the server refuses on a read route: set logs.readHeaders in vinaya.config.json to the server's read token (referenced by variable name, resolved from the environment exactly as logs.headers is). The test event is marked operation: 'log.selftest' so a reader can exclude it from real telemetry.

log send

$ vinaya log send

Deliver this repository's locally-held log events to the configured `logs.url` server, once

For a run whose events ended up on local disk instead of the configured server — an unattended run that fell back to the local folder because the trust-anchor read could not confirm its logs.url (a developer machine slower than the read deadline, or genuinely offline), or a retry queue holding a backlog or a .draining.ndjson a dead drain left behind. It moves the local folder's events into the retry queue and drains every queue for this repository through the SAME webhook delivery a live event uses.

It resolves the destination exactly the way a normal event does — the sink's own decision over vinaya.config.json's logs setting, trust-anchor-gated for an unattended caller — never a URL passed on the command line. A repository whose destination is a folder (or none) has nothing to send to a server and the command says so.

Safe to run more than once: the server stores events by event_id and ignores one it already holds, so a re-sent chunk creates no duplicate, and a second run finds the folder emptied and the queue drained.

--jsonEnveloped JSON output (schema: 1)

sync

$ vinaya sync

Fill, resume and rebuild the local log cache from this repository's configured destination (read-only)

Resolves the destination exactly the way a normal event does — the sink's own decision over vinaya.config.json's logs setting, trust-anchor-gated for an unattended caller — then reads a folder destination through the folder source or a server destination through the server source (with the configured logs.readHeaders, substituted from the environment), and runs the bounded sync engine against the durable cache this repository holds under its runtime directory. Prints pages read, rows stored, duplicates, edits, deletions, gaps, quarantined lines and, for a server destination, its lost-event diagnostic. Reads the destination and writes only its own cache directory — never the folder, the server, or the forge.

Exits 0 when the run completed or stopped cleanly on its page bound (and says more is available), 1 when it failed part way (the progress made is kept — a second run resumes from the stored cursor), and 2 for a usage or configuration problem: no destination is configured, or a server destination is configured with no usable read credential. A missing credential names the environment variable to set and never prints its value.

--rebuild deletes the cache file ALONE — never anything else under the cache directory — and syncs again from the beginning of what the destination still retains; an event already rotated out of a folder's single backup slot, or no longer retained by the destination, cannot be recovered and shows up as a gap, never silently.

--rebuildDelete the cache file and sync from the beginning of what the destination still retains
--jsonEnveloped JSON output (schema: 1)

Studio and rulings

studio

$ vinaya studio

Launch Vinaya Studio — runs the Studio dev app when its source (apps/vinaya-studio/web) is in a checkout above the current directory; a published install launches its bundled standalone server instead

Resolution happens in this order: a workspace checkout carrying apps/vinaya-studio/web (Studio's source, which lives in the attalabs monorepo — not this repository) runs the dev app directly; a published install's studio-standalone/ bundle (fetched from attalabs' release artifact at publish time, see scripts/bundle-studio.ts) runs that bundled server; anything else — a publish that shipped without the bundle — gets an explicit refusal and exit 1 rather than a silent no-op.

Every published @attalabs/vinaya build ships the studio-standalone/ bundle: prepack fetches attalabs’ latest CI-built standalone Studio artifact and assembles it into the tarball before publish.

--port <n>Bind this exact port. Without the flag the default is unchanged — 3008, falling back to 3108 when it is taken. With it there is no fallback: a taken port is refused, so you always know which server answered. Accepts `--port 3208` and `--port=3208`. Applies to a published install; in a workspace checkout Studio's own dev script owns the port and the flag is refused.

pr rule

$ vinaya pr rule

Post a Principal ruling on a PR, marked and versioned — never mistaken for a review verdict

Refuses before posting when the file's first line reads as an escalation (ESCALATE:), or when the file carries verdict grammar anywhere extractCodeReviewVerdict/extractSecurityReviewVerdict would treat as a candidate — a VERDICT: line past the extractor's own first-five-line window still counts, since the whole-body candidate test is what disqualifies the file, not merely a clean read.

Marker numbers (<!-- aeg:principal:ruling:<pr>-<k> -->) are counted on the forge at post time from gh pr view --json comments, never derived from a local file — two rulings racing to the same number are a known, undocumented-lock case.

--filePath to the ruling file to post as a PR comment
--jsonEnveloped JSON output (schema: 1)

Try it and help

demo break

$ vinaya demo break

Run a guided refusal-then-fix demo on an isolated, discardable branch

Creates a collision-safe vinaya/demo-break-<id> branch off the current one, stages a deliberately incomplete draft brief, and attempts a real git commit — the repo's actually-installed pre-commit hook refuses it with its real output, not a scripted string. Applies the minimal fix, commits again, then switches back and deletes the demo branch.

Safe to run twice: refuses on a dirty working tree, refuses from a detached HEAD, and recovers automatically from a prior crashed run before starting a fresh one — never leaves the original branch touched or a stray demo branch behind.

--keepSkip cleanup and leave the demo branch checked out to inspect

help

$ vinaya help

Show this help text

version

$ vinaya version

Print the CLI version

--jsonEnveloped JSON output (schema: 1)

What's next