Configuration Schema
The config file is JSON, conventionally named maintainability-agent.json, and currently uses "version": 1.
The machine-readable schema lives at:
maintainability-agent.schema.json
Minimal Config
{
"version": 1
}
Common Fields
paths.include_extensions: file extensions to scan.paths.exclude_patterns: directories orfnmatchglob patterns to ignore. Separators are normalized so a pattern written either way matches, but this is POSIX-only software and Windows is untested — see platform support. The normalization exists because patterns get copied between machines, not as a Windows claim.migrations/is excluded by default — migrations are append-only history, so a long, branchlessupgrade()is correct code rather than a finding.paths.exclude_additional: exclusions added to the defaults and to any earlier tier's. Settingpaths.exclude_patternsreplaces the default list —.git/,node_modules/,vendor/,*.min.jsand whatever a later release adds — so to skip one more directory, use this instead.- A committed docs site is not scored as source when its pages open with an
@generatedbanner, whichtools/render_docs.pywrites; discovery treats any such file as generated. thresholds: warning/failure limits for file size, function size, complexity, and duplication.thresholds.max_cognitive_complexity/warn_cognitive_complexity(default 25 / 15): nesting-weighted reading cost, charged per flow break plus the depth it sits at. Distinct frommax_complexity, which counts branches and is blind to nesting — five guard clauses and five levels of nesting score the same under it. Fitted against 21,300 declarations in the reference corpus, where 15 sits near the 94th percentile and 25 near the 97th. Omit both keys to disable the check.thresholds.max_class_lines/warn_class_lines: separate budget for classes (default 300 / 200). Classes are graded on length alone: they are containers, so the per-function line budget is the wrong yardstick, and a class's measured complexity is the sum of branches already charged to its own methods.idiom_groups: concern name → list of competing package names, e.g.{"logging": ["loguru", "structlog"]}. Setting this replaces the shipped list, which covers only well-known, slow-moving alternatives (HTTP clients, date handling, client state, schema validation, ORMs, web frameworks) and is incomplete by construction.hard_gates: rules that can fail CI.expected_files: files required by the repo.expected_commands: documented native commands such as test and lint.risk_patterns: regex checks for project-specific risk language or unsafe patterns.instruction_pack: context used when generating AI agent standards.
Configuration locations and precedence
Configuration merges from least to most specific: built-in defaults, then the
user file at $XDG_CONFIG_HOME/maintainability-agent/config.json (default
~/.config/maintainability-agent/config.json), then the repository's
maintainability-agent.json or explicit --config file. Later tiers win via
the same recursive merge used for repository configuration. Documented
environment overrides, where a setting has one, remain the highest-precedence
one-run value; there is no generic environment-to-JSON replacement.
Consent to run a repository's test suite lives only in the user file, under
test_execution.commands, keyed by the repository's resolved root: the command
you agreed to, or an empty list for a decline. A repository cannot supply it.
Loading either the user or repository tier defaults analyzers.run to true.
An explicit run: false survives until a later tier explicitly overrides it.
With neither file, load_config(None) retains the built-in run: false path.
Per-user first-run state lives at
$XDG_STATE_HOME/maintainability-agent/state.json (default
~/.local/state/maintainability-agent/state.json). Its seen object maps an
absolute repository root to an ISO date. Missing, corrupt, or unreadable user
configuration and state are treated as absent rather than aborting an audit.
Analyzer policy (analyzers)
Controls whether the external pool runs and which analysis tools may run in it. Full background, including the tool inventory and what each class means, is in the analyzer pool.
"analyzers": {
"run": true,
"depth": "moderate",
"license_policy": "permissive",
"prompt_when_interactive": true,
"allow_tools": [],
"deny_tools": [],
"deny_license_classes": [],
"deny_concerns": ["security"],
"timeout_seconds": 120
}
run: whether ordinary audits run the configured external analyzer pool. A loaded configuration file defaults this totrue; set it tofalseto use only the built-in fallback.load_config(None)keepsrun: false, so unconfigured programmatic callers retain the zero-install behavior. CLI flags override this value. This switch does not enable tool acquisition:acquire_toolsremains separately opt-in and defaults tofalse.depth:baseline|moderate|heavy|all. Cumulative —heavyincludes everything inbaselineandmoderate. Larger pools take longer and produce better-supported scores.license_policy:permissive|copyleft-weak|copyleft-any|commercial-free-tier|unverified. Also cumulative. Defaults topermissive, the setting fewest organizations have to argue about.acquire_tools: whether a missing Node tool may be fetched (npx --yes) during a run. Defaultfalse— acquisition is a network action, and the P1 separation of analysis from acquisition only means something when acquisition is chosen rather than defaulted. Off, a missing tool is reportednot-installedwith its install command in the environment work order.prompt_when_interactive: whether an interactive first run may ask the setup questions when no config exists (release-plan 6.1, shipped). Chat, MCP, and a CLI TTY ask the same questions (first run); this key only gates whether an interactive surface asks at all. The ask fires only when there is an operator (MCP elicitation / host question UI, or a CLI TTY) and no discovered or--configfile. It never fires in CI — a non-TTY run neither asks nor writes. Setfalseto keep even an interactive run silent. Answers otherwise come from this file.--depth/--license-policyontools/resolve_pool.pypreview a pool; they are not a second setup.allow_tools: tool slugs admitted even when the policy tiers would exclude them — for a commercial analyzer you hold a license for, say.deny_tools: tool slugs never run, whatever else permits them.deny_license_classes: whole classes never run, e.g.["strong-copyleft"].deny_concerns: concern tags never run.securityis denied by default; that work belongs tosecure-code-agentand duplicating it here would produce two tools disagreeing about the same repository.timeout_seconds: per-tool wall clock. A tool that exceeds it is recorded as unavailable-timeout, never as a clean result.class_dirs: extra directories of already-compiled Java classes for SpotBugs, on top of the defaultstarget/classesandbuild/classes. The agent never creates these directories and never runs the build (ADR 012).
Precedence is fixed and not configurable: every deny wins, including over allow_tools. An organization's prohibition must not be overridable by a per-repository opt-in. Within what remains, allow_tools admits, then the depth and license tiers decide.
Slugs come from src/maintainability_audit/_assets/analyzer-catalog.json. To see exactly what a configuration selects, and why everything else was left out:
python tools/resolve_pool.py # resolve the current config
python tools/resolve_pool.py --explain pylint # why is one tool in or out?
python tools/resolve_pool.py --depth all # what-if, without editing the file
A slug that is not in the catalog is a config error, not a silent no-op.
Changed-code policy (policy.changed_code)
Holds the code a change touches to a stricter bar than code it leaves alone (Decision 15) — how a team adopts a quality gate on a codebase it cannot fix all at once.
"policy": {
"changed_code": {
"max_function_lines": 40,
"max_complexity": 10,
"max_cognitive_complexity": 15
}
}
- Applies on a run against a change:
--changed-only main...HEAD. A function is touched when the change added or altered a line inside it; a pure deletion touches nothing, and classes are not held to it. - Each limit may only tighten the matching
thresholdsvalue. A looser one is refused when the configuration loads, by name. - It decides the gate, never the grade: breaches fail
--fail-on-gateand the chat door'sgate_passed, and the report lists them under Changed-code policy — but the score is computed exactly as without it.
Security pillar (security)
The security pillar is measured by secure-code-agent, which this tool runs
for you. A full run can take several minutes on a large repository.
"security": {
"timeout_seconds": 900
}
timeout_seconds: how longsecure-code-agentmay run before the pillar is reported unmeasured, with the reason. Default 900; held to 1–3600, likeanalyzers.timeout_seconds, and a value that is not a whole number falls back to the default.
Economic context
ADR 004 v1. Optional. The audit CLI reads these keys
(tests/test_economic_context.py).
Optional. Absent, the report is a normal maintainability report: no $
block, work order stays in risk×effort order. Present, it never changes
the 0–5 score or verified_grade.
"economic_context": {
"version": 1,
"currency": "USD",
"planning_horizon_months": 12,
"loaded_engineering_cost_per_hour": { "low": 90, "base": 140, "high": 210 }
}
loaded_engineering_cost_per_hour: required for any $ scenario. Low / base / high loaded blended rate. One number is not enough; the range is the honesty.currency: display label only (defaultUSD). No network conversion.planning_horizon_months: default12when omitted.- Optional later keys (not required for the $ block):
reliability_tier(internal|customer|regulated),typical_review_minutes_per_change,representative_incident_cost.
When the labor range is missing and stdin is a TTY, a first interactive
run may ask and write this block into maintainability-agent.json.
Non-TTY never asks. analyzers.prompt_when_interactive: false suppresses
the ask. Environment overrides for one run, not persisted unless the ask
writes the file: MAINTAINABILITY_LABOR_LOW, MAINTAINABILITY_LABOR_BASE,
MAINTAINABILITY_LABOR_HIGH, MAINTAINABILITY_CURRENCY,
MAINTAINABILITY_HORIZON_MONTHS.
This section is the shipped v1 contract.
Semantic policy
ADR 003 option C. Optional and absent by default. The first increment is TypeScript only and contributes report findings and prompt-only candidates, never score weights.
"semantic_policy": {
"version": 1,
"domain_types": [
{
"name": "order-status-public-boundary",
"paths": ["src/orders/api.ts"],
"boundary": "public",
"symbol": "updateStatus",
"required_type": "OrderStatus"
}
],
"operations": [
{
"name": "session-operation-review",
"paths": ["src/session/operations.ts"],
"capability_type": "SessionCapability",
"operation_contract": "Op<TResult>"
}
]
}
version: currently1.domain_types: exact TypeScript paths and public symbols for which the repository declares a required domain type. Each entry names itself so a finding can cite the policy that produced it.operations: exact TypeScript paths and operation concepts eligible for the repeated dispatch/capability/description candidate. Configuration does not promote that candidate to a fact.
Broad naming rules are outside v1. In particular, policy cannot say every
*_id must be a value object. Missing type analysis is reported as unknown
coverage rather than zero semantic violations.
Validation
Any JSON Schema validator that supports draft 2020-12 can validate the config.