Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration Reference

pretender.toml is placed at the root of your repository. All sections and keys are optional — omitted keys fall back to the defaults shown here.


For charly-vibes suite repos (Rust CLI tools like pretender, wai, dont, espectacular, vampiro, testaruda, crua, livin), we recommend pinning roles and overriding the default thresholds to match a typical Rust project layout:

RolePathsNotes
testtests/**, **/*_test.*, spec/**Default — already matches
unit-testtests/unit/**Explicit sub-role for fast unit tests
integration-testtests/integration/**Explicit sub-role for slower integration
scriptscripts/**, examples/**Default — already matches
generated**/*_generated.*Default — pb.go, protobuf, etc.

Why pin roles explicitly

Without a pretender.toml, pretender uses library defaults: all files get the app role, tests are detected via heuristics only, and every role’s paths use convention over configuration. This works, but has two downsides:

  1. No sub-role awareness — tests/unit/ and tests/integration/ are not differentiated, so duration thresholds (duration_max_ms) never activate.
  2. No version-controlled baseline — new team members or CI environments must guess which role layout your project uses.

Pinning roles in pretender.toml resolves both issues and makes the config part of the reviewed, committed project state.

Baseline threshold values

These values are copied from templates/pretender.toml.example and represent the recommended starting point for Rust CLI repos:

[thresholds]
cyclomatic_max        = 10
cognitive_max         = 15
function_lines_max    = 40
file_lines_max        = 400
nesting_max           = 3
params_max            = 4
abc_max               = 30
duplication_pct_max   = 5

[thresholds.test]
cyclomatic_max      = 3
cognitive_max       = 5
function_lines_max  = 80
nesting_max         = 2
params_max          = 2
duplication_pct_max = 30
min_assertions      = 1

[thresholds.library]
exported_params_max       = 3
exported_cyclomatic_max   = 8
exported_lines_max        = 30
require_docstring         = true

[thresholds.unit-test]
duration_max_ms = 100

[thresholds.integration-test]
duration_max_ms = 2000

that may not match the project’s quality targets.

Doctor check for effective hooks path

pretender doctor also includes a Hook location check: it resolves core.hooksPath across all config scopes (local, global, system) using genesis::git_hooks::effective_hooks_dir and warns when git will invoke hooks from a directory other than where the pretender hook was installed — the case where a global core.hooksPath shim (e.g. lefthook) silently makes the installed gate inert. An empty-string core.hooksPath (git disables hooks entirely) is reported as an error.

Deploying across the suite

Each suite repo should commit its own pretender.toml (start from the template at templates/pretender.toml.example) and adjust thresholds as needed. The canonical template lives in the pretender repo and serves as the upstream source of truth for all suite repos.


[pretender]

KeyTypeDefaultDescription
modestring"gate"Check behaviour: "gate" (any violation fails — the fail-closed default, softened only by an adopted gate baseline), "tiered" (failures scale with severity; advisory), "guidance" (hints only; advisory)
advisory_untilISO date string— (required for tiered/guidance)Advisory lease expiry (pretender-1te). Advisory modes are dated downgrades from the gate default: an expired, missing, or unparseable lease makes check FAIL, forcing a renewed, dated, reviewable decision. Renewal = push the date forward in this file (a visible diff). Gate mode never consults the lease.
languagesarray of strings["auto"]Languages to analyse. "auto" detects from file extensions. Explicit values: "python", "rust", "go", "javascript", "typescript", "java", "ruby", "c", "cpp"
excludearray of glob strings["vendor/**", "node_modules/**", "**/*_generated.*"]Path globs to skip during analysis
respect_gitignorebooleantrueWhen walking directories, skip anything ignored by git (repo + nested .gitignore, global core.excludesFile, .git/info/exclude) so target/, node_modules/, and vendored code aren’t scanned without per-repo exclude lists. Outside a git repo the walk is unfiltered. Explicitly passed file paths and --staged are never filtered. Pass --scan-ignored to check for a one-off exhaustive walk (pretender-094).

[thresholds]

App-level metric limits applied to files assigned the app role (the default for files not matched by any other role).

KeyTypeDefaultDescription
cyclomatic_maxinteger10Maximum cyclomatic complexity per function
cognitive_maxinteger15Maximum cognitive complexity per function
function_lines_maxinteger40Maximum lines per function
file_lines_maxinteger400Maximum lines per file
nesting_maxinteger3Maximum control-flow nesting depth
params_maxinteger4Maximum formal parameters per function
abc_maxinteger30Maximum ABC score (√(A²+B²+C²)) per function
duplication_pct_maxinteger5Maximum structural duplication percentage (0–100)

Planned (not yet enforced): Maintainability Index (mi_min), coverage (coverage_line_min / coverage_branch_min), and mutation score (mutation_min) thresholds are not currently configurable. They were removed from the schema in v0.5.0 because they were never enforced — passing a config that sets these keys silently ignored them. Mutation score is already gateable via pretender mutation --score-min N; coverage and MI enforcement are future work.

[thresholds.unit-test]

Overrides for files assigned the unit-test sub-role. Inherits all [thresholds.test] values and overlays the duration threshold.

KeyTypeDefaultDescription
duration_max_msinteger0Maximum allowed test duration in milliseconds. 0 disables the check.

[thresholds.integration-test]

Overrides for files assigned the integration-test sub-role. Inherits all [thresholds.test] values and overlays the duration threshold.

KeyTypeDefaultDescription
duration_max_msinteger0Maximum allowed test duration in milliseconds. 0 disables the check.

[thresholds.test]

Overrides for files assigned the test role.

KeyTypeDefaultDescription
cyclomatic_maxinteger3Cyclomatic limit for test functions
cognitive_maxinteger5Cognitive limit for test functions
function_lines_maxinteger80Line limit for test functions
nesting_maxinteger2Nesting limit for test functions
params_maxinteger2Parameter limit for test functions
duplication_pct_maxinteger30Duplication tolerance in test files
min_assertionsinteger or null1Minimum assertion calls per test function; null disables

[thresholds.library]

Overrides for files assigned the library role.

KeyTypeDefaultDescription
exported_params_maxinteger3Parameter limit on exported/public functions
exported_cyclomatic_maxinteger8Cyclomatic limit on exported/public functions
exported_lines_maxinteger30Line limit on exported/public functions
require_docstringbooleantrueRequire a docstring on every exported/public function

[thresholds.script]

Overrides for files assigned the script role.

KeyTypeDefaultDescription
function_lines_maxinteger100Line limit per function
file_lines_maxinteger300Line limit per file

[bands]

Colour bands used when mode = "tiered" to map raw metric values to severity levels. Each band specifies thresholds for green (acceptable), yellow (warning), and red (violation). Values must satisfy green <= yellow <= red.

Each band (cyclomatic, cognitive) is optional. When the [bands] section is entirely absent from pretender.toml, both bands are active with the defaults shown below. However, once you add a [bands] section, any band key you omit is disabled (set to None) — it does not fall back to its default. Always specify both bands together when overriding either one.

# Inline form
[bands]
cyclomatic = { green = 10, yellow = 15, red = 20 }
cognitive  = { green = 15, yellow = 25, red = 40 }

# Block form (equivalent)
[bands.cyclomatic]
green  = 10
yellow = 15
red    = 20

[bands.cognitive]
green  = 15
yellow = 25
red    = 40

[bands.cyclomatic]

Default (when [bands] is absent): { green = 10, yellow = 15, red = 20 }

KeyTypeDescription
greenintegerCyclomatic complexity at or below this is green
yellowintegerAt or below this is yellow
redintegerAbove this is red

[bands.cognitive]

Default (when [bands] is absent): { green = 15, yellow = 25, red = 40 }

KeyTypeDescription
greenintegerCognitive complexity at or below this is green
yellowintegerAt or below this is yellow
redintegerAbove this is red

[scope]

Controls which files are analysed during pretender check.

KeyTypeDefaultDescription
diff_onlybooleantrueWritten by pretender init to record your project preference. Setting this to true does not automatically enable diff filtering — you must pass --diff-only on the CLI (or CI step) to activate it. When active, only files changed relative to diff_base are checked
diff_basestring"origin/main"Git ref used as the comparison base for --diff-only

[execute]

Optional shell commands pretender can run to collect coverage and mutation data.

KeyTypeDefaultDescription
enabledbooleanfalseRun external commands (test, coverage, mutation) during pretender check
coverage_cmdstring or nullnullPlanned: Shell command that produces a coverage report. Not yet executed by pretender check
mutation_cmdstring or nullnullPlanned: Shell command that runs mutation testing. Mutation score is already gateable via pretender mutation --score-min N
test_cmdstring or nullnullShell command that runs the test suite and produces a JUnit XML report (used with --execute)
test_report_pathstring or nullnullPath to the JUnit XML report produced by test_cmd (relative to repo root)
test_timeout_sinteger600Timeout in seconds for test_cmd; on timeout pretender emits a test-cmd-timeout error
test_time_unitstring"seconds"Unit of the JUnit time attribute: "seconds" (XSD-conformant) or "milliseconds" (non-conformant runners)

[plugins]

Controls which built-in language and metric plugins are active.

KeyTypeDefaultDescription
languagesarray of strings["python","javascript","typescript","go","rust"]Language plugins to load
metricsarray of strings["eslint","ruff","clippy"]Built-in metric tool plugins to load

External metric plugins are always loaded in addition to this list from the first directory found in this order:

  1. $PRETENDER_METRICS_DIR (if set)
  2. $XDG_CONFIG_HOME/pretender/metrics/ (if XDG_CONFIG_HOME is set)
  3. ~/.config/pretender/metrics/ (fallback)

See Writing plugins.


[output]

KeyTypeDefaultDescription
formatsarray of strings["human","sarif"]Output formats to emit. Valid values: "human", "json", "sarif", "junit", "markdown". At least one value is required
sarif_pathstring"pretender.sarif"File path for SARIF output when "sarif" is in formats

[roles.*]

Role matchers assign a role to files based on path globs. The resolved role determines which [thresholds.*] block applies.

Each role section has a single key:

KeyTypeDescription
pathsarray of glob stringsFiles matching any glob are assigned this role
classname_rootstringFor [roles.test] only: directory to search when resolving JUnit classname to a source file (default "tests")

Available roles and their defaults:

RoleDefault paths
test["tests/**", "**/*_test.*", "spec/**"]
unit-test["tests/unit/**"]
integration-test["tests/integration/**"]
library["pkg/**", "lib/**"]
script["scripts/**", "examples/**"]
generated["**/*.pb.go", "**/*_generated.*"]
vendor["vendor/**", "node_modules/**"]

Example override:

[roles.test]
paths = ["tests/**", "**/*.spec.*", "**/__tests__/**"]

Role detection order

pretender assigns the first role that matches, checked in this priority order:

  1. Pragma — a comment on any line of the file: # pretender: role=<name> or // pretender: role=<name>
  2. Configured glob — the first [roles.*] section whose glob matches the file path
  3. File-name heuristic — built-in patterns (e.g. _test. suffix → test, _generated. → generated)
  4. Default — app

Test-duration check

pretender can analyse per-test timing data from a JUnit XML report and flag tests that exceed role-appropriate duration budgets. This is the first dynamic check in pretender — it consumes a runtime artifact rather than source text.

Activation

The check activates when you pass --test-report <path> to pretender check. You can also configure [execute] test_cmd and use --execute to have pretender run the test suite for you:

# Analyse an existing report
pretender check --test-report target/junit-report.xml

# Run tests first, then analyse
pretender check --execute

Thresholds

Set duration limits per test sub-role:

[thresholds.unit-test]
duration_max_ms = 100       # unit tests should be fast

[thresholds.integration-test]
duration_max_ms = 2000      # integration tests can be slower

A finding is emitted when observed_ms > duration_max_ms (strict comparison, so a test exactly at the threshold does not fail). A threshold of 0 (the default) disables the check.

Sub-role detection

pretender detects unit-test and integration-test sub-roles using the same resolution order as other roles:

  1. Pragma — # pretender: role=unit-test or // pretender: role=unit
  2. Configured glob — [roles.unit-test] with custom paths
  3. Heuristic — tests/unit/** and test/unit/** → unit-test; tests/integration/** and test/integration/** → integration-test
  4. Default — test (no duration threshold)

Sub-roles inherit all [thresholds.test] values and overlay only duration_max_ms.

How timing data is read

pretender parses standard JUnit XML (<testsuite> / <testcase time="...">). The time attribute is interpreted as seconds by default (XSD-conformant); set [execute] test_time_unit = "milliseconds" for non-conformant runners. Rounding uses round-half-up to the nearest millisecond.

JUnit emission incantations per runner

RunnerCommand
pytestpytest --junitxml=report.xml
cargo testcargo2junit > report.xml (requires cargo2junit crate)
Mavenmvn surefire-report:report (produces target/surefire-reports/*.xml)
GradleBuilt-in: build/reports/tests/test/*.xml
Jestjest --junitOutput=report.xml (requires jest-junit reporter)

CLI flags

FlagDescription
--test-report <path>Path to a JUnit XML report for duration analysis
--executeRun [execute] test_cmd before analysing durations

Output

Findings appear in all output formats:

  • Human: unit-test test_addition tests/unit/test_math.py: 100ms > 50ms
  • JSON: test_findings top-level array with test_name, file, role, observed_ms, threshold_ms
  • SARIF: pretender/test-duration rule id, Warning level

In gate mode, any duration finding causes a non-zero exit code.

pretender maintains a local event log that surfaces recurring problem areas across runs — useful for calibrating thresholds and identifying structural debt before it compounds.

pretender tracks every violation it reports in .pretender/history/events.jsonl at the root of your repository. Each line is a JSON object (a ViolationEvent) recording the file fingerprint, rule key, role, area, run ID, and Unix timestamp.

Events older than 90 days are pruned automatically on each run.

From the event log pretender computes two summaries shown at the end of pretender check:

  • Hotspots — the 10 files with the highest total violation count across at least two distinct days. A file that repeatedly triggers is a structural problem, not a fluke.
  • Patterns — the 10 (rule, role, area) combinations that recur most often across the most files. Use these to calibrate thresholds or identify conventions your team hasn’t codified yet.

Resolution tracking — every check run (including clean runs) also persists the current set of finding IDs to .pretender/history/last-findings.json. IDs are stable across line-shift edits (path::unit_name::rule_key). The next run reports a delta:

→ resolution: 3 fixed, 5 still open, 2 new (38%)

The rate is fixed / (fixed + still_open) — null on the first tracked run. The same delta is available to machine consumers under data.history.resolution in JSON output and in history/summaries.json, so CI dashboards can track the fixed-vs-flagged ratio over time.

The .pretender/ directory should be committed so the feedback loop survives across machines and CI runs. Note: events.jsonl records file paths and fingerprints — review the file before committing if your repository contains sensitive filenames.