Skip to content

Commit policy

With no input option, YAGA checks HEAD. Exactly one explicit source may be selected:

yaga commit check --message "feat(cli): add configurable checks"
yaga commit check --file .git/COMMIT_EDITMSG
printf 'fix: preserve stdin\n' | yaga commit check --stdin
yaga commit check --commit HEAD~1
yaga commit check --range origin/main..HEAD
yaga commit quality --commit HEAD --offline

Optional model advice is covered by the commit quality guide.

Git commit and range sources read complete messages without fetching or invoking a shell. Exact commit checks select one commit; range checks preserve oldest-first order. Both require complete, non-shallow history because Git's shallow boundary can hide stored parents. Commit, change, mode, path, tree, and size selection reject legacy graft overlays, and replacement refs are disabled for all six. An empty range, a missing ref, or a selection above the configured commit limit is an operational error.

All Git-backed checks share one bounded runtime. It resolves an absolute regular Git executable outside the enclosing worktree and its resolved metadata and object-store boundaries, starts no shell, inherits only a minimal environment, disables pagers and prompts, contains the spawned process tree, and enforces hard time and output limits including bounded termination cleanup. Every command uses Git's global --no-lazy-fetch option, so an unsupported client or a missing promised object fails closed instead of contacting a remote.

The built-in policy enforces only Conventional Commit structure, accepts any type and scope, and ignores commits that Git proves have multiple parents. Put stricter project policy in the nearest .yaga.toml or pyproject.toml. Discovery walks toward the repository root, preferring .yaga.toml in each directory; --config selects one file explicitly. Files are never merged, and unknown or misspelled keys fail loudly.

For pyproject.toml:

[tool.yaga]
config-version = 1

[tool.yaga.commit]
allowed-types = [
  "build", "chore", "ci", "docs", "feat", "fix",
  "perf", "refactor", "revert", "style", "test",
]
type-case = "lower"              # any, lower, or upper
scope-policy = "optional"        # optional, required, or forbidden
scope-policy-by-type = { feat = "required", fix = "required", revert = "forbidden" }
allowed-scopes = ["cli", "config", "git"]
scope-case = "lower"
header-max-length = 100
description-min-length = 3
description-max-length = 72
description-ending = "forbid"    # allow, require, or forbid . ! ?
breaking-markers = "paired"      # either or paired
required-footer-tokens = ["Signed-off-by"]
forbidden-footer-tokens = ["WIP"]
body-policy = "optional"
body-min-length = 0
body-min-words = 8
# Omit body-max-line-length unless this repository wants a wrapping limit.
# Set body-paragraph-splitting = "check" to catch sentence-like prose split by blank lines.
dependabot-pull-requests = "skip" # check or skip in event-aware PR checks
merge-commits = "reject"         # ignore, check, or reject
ignored-headers = ['Revert "*"'] # bounded, case-sensitive glob patterns
max-commits = 64

A standalone .yaga.toml uses config-version = 1 and [commit] instead of the two [tool.yaga...] tables. Omit allowed-types or allowed-scopes to allow any value. An explicit empty allowed-scopes list permits only unscoped messages; allowed-types must not be empty. yaga config show prints every effective value and the source file, with --format json for tools.

dependabot-pull-requests defaults to "check". Setting it to "skip" skips Conventional Commit policy evaluation only in yaga github pull-request check and the read-only commit-check Action, and only when the event's pull-request author is exactly the GitHub dependabot[bot] Bot account, following GitHub's event-author guidance for Dependabot automation. The adapter still validates the complete event and runner context, verifies the checked-out head, loads configuration, and enumerates the bounded exact commit range before returning a visible Dependabot pull request skip. It does not skip change, mode, path, size, tree, workflow, test, or review-gate checks.

Ordinary commit check and the commit provider inside repo check never infer Dependabot from a Git author name, email, message, or branch because those values are author-controlled. They keep checking normally. A saved event file can reproduce the adapter decision for diagnosis, but its author identity is only asserted local input; the fixed Action gets host-issued event provenance from GITHUB_EVENT_PATH.

scope-policy-by-type defaults to an empty mapping. Each entry replaces the global scope-policy only for that Conventional Commit type, so a project can require scopes for feat and fix, keep them optional elsewhere, and forbid them for revert. Type matching is case-insensitive and the same effective policy is used for complete commits and pull-request titles. scope-case and allowed-scopes still validate every scope that is present.

The mapping accepts at most 128 safe type tokens and the same closed optional, required, and forbidden values as the global policy. Configuration rejects case-insensitive duplicate keys and, when allowed-types is configured, override keys outside that list. An empty allowed-scopes list is invalid when any reachable type requires a scope; a non-empty list is invalid when every reachable type forbids scopes. header-max-length validation uses a conservative structural lower bound so shorter Unicode case-fold equivalents are never rejected during configuration loading; the commit check remains authoritative for every actual header.

breaking-markers defaults to "either". A header !, a recognized final BREAKING CHANGE: or BREAKING-CHANGE: footer, or both mark a breaking change in that mode. The "paired" policy requires both markers or neither; exactly one produces breaking.marker-pair. Marker pairing applies to complete commit messages, not pull-request titles, whose policy check intentionally covers only the Conventional Commit header.

required-footer-tokens and forbidden-footer-tokens default to empty lists. They apply to complete commit messages, including every commit selected by a range or a pull-request event, but never to the pull-request title's header-only check. Matching is exact and ASCII case-insensitive: configured Signed-off-by matches SIGNED-OFF-BY: A User, but not a token with an added prefix or suffix. Both Token: value and Token #value count, provided the value starts with a non-whitespace character. Repeated tokens are allowed; one occurrence satisfies a requirement, while any occurrence violates a prohibition. These are presence checks only: requiring Signed-off-by does not validate signer identity, DCO compliance, or a cryptographic signature.

Configured footer tokens contain only one through 128 ASCII letters, digits, or hyphens, and the two lists may contain at most 128 entries combined. Configuration rejects case-insensitive duplicates within either list, overlap between the lists, and both reserved breaking-marker token spellings, BREAKING CHANGE and BREAKING-CHANGE.

A footer block begins only when a valid footer token starts the first content paragraph or a new paragraph after a blank line. Once that boundary is found, the rest of the message is footer content: blank and non-token lines continue a multiline footer value, while a later valid token starts another footer even without an intervening blank line. Reporting remains bounded: footer.required reports only the first configured missing token at line 1 plus a count of any other missing tokens, and footer.forbidden reports only the earliest forbidden occurrence at its exact source line.

body-min-length and body-min-words are independent lower bounds on the parsed prose body. body-min-words accepts an integer from 0 through 100000 and defaults to 0; it counts Unicode-whitespace-delimited tokens that contain at least one Unicode alphanumeric character. Punctuation-only and emoji-only tokens do not count, while text without Unicode whitespace is one token. The minimum is checked only when a prose body exists, so use body-policy = "required" to require one. A recognized final footer block is not prose body and does not contribute words. When body-policy = "forbidden", both body minima must be zero.

body-paragraph-splitting accepts skip (the default) or check. The check mode catches a blank-line split between sentence-like one-line paragraphs while leaving standalone short paragraphs and list items alone. A one-line validation note or justification remains valid; this is not a minimum paragraph-height rule and does not enforce a wrapping width.

For a new repository, YAGA can create a recommended standalone policy and immediately report its effective values:

yaga config init --repo .

Use yaga config init --repo . --dry-run to validate and inspect the same starter policy without creating .yaga.toml; the report names the path that would be published and marks the preview explicitly.

The starter permits the common Conventional Commit types, requires a lowercase type, keeps the scope optional (and lowercase when present), bounds headers, rejects merge commits, and checks at most 64 commits per range. Initialization creates only .yaga.toml: it never edits pyproject.toml, overwrites a path, or shadows a configuration already discovered for the target directory. Use --format json when another tool needs the created path and effective policy.

Diagnostics have stable names such as syntax.header, type.allowed, scope.required, scope.forbidden, header.length, breaking.marker-pair, body.word-count, body.paragraph-format, footer.required, and footer.forbidden. Text and versioned JSON reports use these exit codes:

Exit Meaning
0 Every checked commit passed; ignored merge/header records may have been skipped.
1 At least one commit violated policy.
2 Invocation, input, configuration, or Git failed.

Standalone titles

For a standalone title, use yaga commit check --title "fix: correct the result". This applies header policy and optional spelling, without body or footer requirements. Titles must be one nonempty line of at most 1,024 UTF-8 bytes. Title input is mutually exclusive with every other message source.

Local commit-message hook

For an editor message containing Git comments, use yaga commit check --edit "$1" in a custom commit-msg hook. This reads the file without modifying it and delegates comment removal and whitespace cleanup to git stripspace --strip-comments, using Git's user and repository configuration, including core.commentChar and supported core.commentString settings. Comments cannot satisfy body requirements or introduce spelling findings. This is explicit strip-comments behavior, not an emulation of every git commit --cleanup mode; use literal --file when comments are intentional content. Git command-line -c overrides are not inherited. Automatic comment-character selection is not reconstructed from the editor session; use a fixed configured comment prefix. Missing files and invalid UTF-8 remain input errors. --edit cannot be combined with another source.

YAGA ships a pre-commit provider for the existing literal file-backed command. Pin the repository to an audited immutable commit that contains .pre-commit-hooks.yaml:

repos:
  - repo: https://github.com/dariuszpanas/yaga
    rev: cd02385e3216ac544e7783c7dd6929340e230e95
    hooks:
      - id: yaga-commit-check

Install the non-default hook type and its environment:

pre-commit install --hook-type commit-msg --install-hooks

The provider requires pre-commit 3.2 or newer and YAGA requires Python 3.12 or newer. If pre-commit's default interpreter is older, set language_version: python3.12 (or another supported interpreter) on the hook.

Repositories that want ordinary pre-commit install to include it can add default_install_hook_types: [pre-commit, commit-msg] to their configuration. Policy remains in .yaga.toml or pyproject.toml; do not duplicate it in hook arguments. The hook runs only after explicit installation, can be bypassed with --no-verify or SKIP=yaga-commit-check, and does not run for commits created directly by GitHub, APIs, or bots. File mode has no parent metadata: it cannot enforce merge-commits = "reject" and may apply ordinary syntax rules to a proposed merge message even when actual merge commits would be ignored. Retain the GitHub commit-policy check as the repository-side source of truth.

CI should use a non-shallow checkout containing the exact base and head, then call yaga commit check --range "$BASE_SHA..$HEAD_SHA". --quiet suppresses validation reports while operational errors still go to standard error; --format json provides a stable schema for another tool.

Optional Typos integration

Set typos = "check" to run the installed Typos CLI against each selected commit message. YAGA sends the message through standard input, requests Typos JSON Lines output, validates bounded finding fields, and converts each finding into a stable typos.word diagnostic. This works with --message, hooks, individual Git commits, ranges, and GitHub pull-request titles and commits. The default typos = "skip" keeps policy results independent of the tools installed on a developer machine. Set typos-config = "isolated" alongside typos = "check" to disable Typos configuration discovery consistently for local titles, complete messages, and hooks. The default typos-config = "repository" preserves discovery. Trusted Action checks always isolate spelling configuration, regardless of this setting. Local isolation still permits Typos installed in a project environment; trusted Actions require an executable outside the checkout. When Typos provides its bounded byte_offset, YAGA preserves it as a 1-based diagnostic column; older or alternate JSON output without that field falls back to column 1. Text and GitHub output include both the line and column in each policy diagnostic. GitHub annotations additionally escape the diagnostic for the runner command protocol.

[commit]
typos = "check"

The check is intentionally explicit: when enabled, a missing typos executable, malformed output, unexpected exit status, timeout, or excessive output is an operational error (exit 2) rather than an ignored warning. Install Typos separately, for example with cargo install typos-cli --locked, and keep project-specific words in Typos' _typos.toml. Use Typos' repository-wide action or pre-commit integration for source files; YAGA's adapter is narrowly scoped to commit messages. When --repo points at a different checkout, YAGA runs Typos from that repository so its local Typos configuration is used; direct message and Git-backed sources follow the same rule.

Spelling in trusted pull-request checks

With the commit Action's trusted-config mode, typos = "check" applies to both the PR title and every selected full commit message. Structural title checks remain header-only. Authentic Dependabot skips and existing skipped commit types also skip spelling checks. Findings fail with exit 1; a missing or broken tool fails with exit 2.

Install a pinned Typos executable outside the checkout before invoking the Action. Trusted mode passes --isolated, using the tool's built-in dictionary without repository, parent, or global configuration discovery. Custom dictionaries are currently unsupported in trusted mode: neither a PR's _typos.toml nor dirty files in the trusted checkout can weaken it. Ordinary local checks and the default PR-head mode retain repository Typos configuration. Keep the tool version consistent when comparing local and CI spelling results.

Proposed PR messages

pull-request-message = "title-and-body" opts into full commit policy for the PR title and description together. The default is "title-only". This setting affects only event-aware PR checks; see GitHub Actions for merge settings, event triggers, reporting, and limitations.

Workflow starters and type explanations

Start with explicit, editable settings for your workflow:

yaga config init --starter title-v1 --dry-run
yaga config init --starter complete-message-v1
yaga config show --type fix
yaga config show --type docs --format json

config init without --starter retains the original recommended-v1 starter byte for byte. title-v1 uses lowercase types and a 100-character header limit, allows ending punctuation, leaves bodies optional, and ignores Git-proven merge commits. It does not disable checks on commits selected by the caller. complete-message-v1 uses the same choices and additionally requires prose for feat and fix; other types remain optional. Neither starter imposes a word count or restricts the set of types. Their names are frozen: future recommendations need new names. Generated TOML is yours to edit, and initialization never replaces existing policy. --dry-run validates and reports the proposed policy without publishing a configuration file.

Both starters retain title-only PR checks. If the PR description is your proposed merge body, explicitly enable pull-request-message = "title-and-body" as described above. Full-message requirements apply to actual commits and enabled proposed messages, never standalone titles.

config show --type TYPE adds the effective body and scope requirements and names the override or global setting that supplied each. Matching is case-insensitive, just as in the checker. The JSON report adds effective_type only when requested. listed_type_allowed tells you whether the configured type list admits the token; it does not evaluate type casing or prove a complete message will pass. All independent casing, scope-list, length, footer, and other rules still apply.

The reported config_path identifies the single selected file, or built-in defaults. A project configuration replaces the global fallback as a whole; fields are not merged between files. For trusted Actions, inspect the committed default-branch policy used by that runner: a local config show does not reproduce a trusted Action's revision selection automatically.

To require a work-item reference and a controlled validation label in complete messages:

[tool.yaga.commit]
required-issue-prefixes = ["#", "PROJ-"]
required-footer-tokens = ["Validation"]
footer-values = {Validation = ["passed", "not-run"]}

Both new settings default to empty and apply only to complete messages, including opted-in proposed PR messages. Standalone titles and the independent PR title result remain header-only. A full message with neither #123 nor PROJ-123 reports reference.required.

References are case-sensitive whitespace-delimited tokens, optionally wrapped in the punctuation () [] {} <> , . ; : ! ? or ASCII quotation marks. A configured prefix must be followed by 1–20 ASCII digits, beginning with 1–9. (#123). matches; #0, #012, prefix#123, and https://example.com/#123 do not. References may appear in the header, prose, or final footers. Markdown links, URLs, and tracker-specific query syntax are not interpreted. Prefixes start with an ASCII letter or #, followed by ASCII letters, digits, _, or -; there are at most 128 unique prefixes of at most 64 characters. Matching uses a bounded prefix tree over the already bounded message rather than user-supplied regular expressions.

footer-values constrains every occurrence of each configured final footer token, matching tokens case-insensitively and values case-sensitively. It does not require the token to occur: combine it with required-footer-tokens when presence is mandatory. Values are the complete parsed footer value up to the next recognized token, with outer whitespace removed. A multiline value must match as a whole, so it cannot satisfy a configured single-line choice by adding an approved first line. Each offending token produces one source-located footer.value diagnostic. The configured footer syntax and paragraph boundary still determine which text is a footer; prose that resembles a trailer is not silently reclassified. With hash separators, Refs #123 has value 123.

The table allows up to 128 unique case-insensitive tokens and 256 total choices, with at most 128 choices per token. Each choice is a nonempty safe single-line string of at most 256 characters, without outer whitespace; duplicates are rejected. Breaking-change tokens and tokens forbidden by forbidden-footer-tokens cannot have value constraints.

These options verify stored syntax only. A reference does not prove that an issue exists or is resolved. A Validation: passed label does not prove that tests ran. An allowed sign-off value would not prove identity, DCO compliance, or a cryptographic signature.

Gradual enforcement

Keep a rule enabled while introducing it as a nonblocking warning:

[tool.yaga.commit]
body-policy = "required"
body-min-words = 8
warning-rules = ["body.required", "body.word-count"]

warning-rules defaults to empty. Only the listed diagnostic codes become warnings; other findings remain errors. A warning does not enable a rule: configure its underlying policy as usual. Remove a code from the list when you want its findings to block validation. Messages cannot waive or change severity themselves. The same policy applies locally, in the read-only Action, to opted-in proposed PR messages, and in the commit provider of repo check.

Supported codes are type.allowed, type.case, scope.required, scope.forbidden, scope.allowed, scope.case, header.length, description.length, description.case, description.ending, breaking.marker-pair, body.required, body.forbidden, body.length, body.word-count, body.line-length, body.paragraph-format, footer.line-length, footer.required-colon, footer.required, footer.forbidden, footer.value, reference.required, and typos.word. The list is closed, case-sensitive, and rejects duplicates. Structural diagnostics (syntax.header, syntax.separator), resource limits, and merge rejection cannot become warnings. Missing history, malformed configuration/input, unavailable tools, and other operational errors continue to exit 2. A configured warning for spelling never converts a missing or broken Typos tool into a successful check.

Warning-aware report contract

Commit and pull-request JSON reports use schema v1 consistently. Every diagnostic includes an error or warning severity, and every report includes warning_count, including zero for clean, empty, or skipped selections. Configuring warnings changes enforcement, not the report format. Codes, locations, and order remain unchanged. warning_count counts diagnostics across all targets, including failed targets. A warning-only target has status: "passed" and valid: true; passed, failed, and skipped continue counting targets. Policy errors still produce exit 1, and warning-only results exit 0. Consumers should use severity rather than treating every diagnostic as an error. Text identifies warning findings and their count. GitHub output emits escaped ::warning annotations for warnings and ::error for errors, retaining bounded annotation output. Repository reports embed the same commit report shape. Configuration inspection, quality-advisory reports, and operational-error envelopes retain their existing schema v1 contracts.

Ordinary messages and authoring help

Teams that want message policy without Conventional Commit prefixes can opt into plain format:

[tool.yaga.commit]
message-format = "plain"
header-max-length = 100
body-policy = "required"

Or start from an editable, frozen policy with yaga config init --starter plain-v1. The default remains "conventional"; format is never inferred from the message. Plain format requires a nonempty first line without outer whitespace or unsafe controls. The whole first line is the description. Header/description limits, casing and ending policy, body/footer rules, references, spelling, warnings, Git merge policy, and explicit ignored headers still apply. A blank line must separate the title from body or footers.

Plain parsing does not invent a type, scope, or header breaking marker, even when the text looks like fix(core)!: example. Final footer parsing is shared, including recognized breaking footers, but ! has no special header meaning. Plain configuration rejects allowed-types, type-case, scope-policy, allowed-scopes, scope-case, scope-policy-by-type, body-policy-by-type, and breaking-markers, even if explicitly set to their conventional defaults. Warning rules for type, scope, or marker pairing are also rejected. Remove those keys when converting an existing policy; config show --type is not applicable to plain format.

This option does not make messages suitable for tools that require Conventional Commit release classification. It changes YAGA's explicit validation grammar only. PR titles, opted-in proposed messages, and ordinary selected commits use the same configured format; title-only checks still exclude body and footer requirements.

Read-only message templates

yaga commit template --type fix --scope cli
yaga commit template --type fix --scope cli --comment-char ';'
# With message-format = "plain":
yaga commit template

commit template prints an incomplete draft using the selected policy. It never writes a file, changes Git configuration, launches an editor, or rewrites history. Conventional drafts require an explicit type and a scope when policy requires one; supplied components must satisfy blocking policy even if an ignored-header pattern would otherwise skip checking. Plain drafts reject type/scope options.

The description is deliberately blank. Required footer labels have blank values: YAGA does not invent a reason, work-item ID, sign-off identity, or a Validation: passed claim. Commented guidance shows effective prose requirements, relevant limits, allowed footer values, and warning rules. Complete the draft yourself and validate the final message; the draft is not a passing example.

Template comments default to #; ; is also supported explicitly. Match the Git editor's comment character or remove the guidance before committing. YAGA does not inspect or change that setting. commit check --file reads comments literally; --edit uses Git's configured cleanup behavior.