Skip to content

Configuration Reference

File: .cadence/config.json

This page documents every field in the CADENCE configuration file. For conceptual explanations of profiles, gates, tiers, and providers, see docs/concepts.md. For provider setup (env vars, fallback behavior), see docs/providers.md.

cadence init writes an initial config.json from the chosen preset and then overlays several detected values: profile (from --gate-profile or git-history suggestion), verification.testGlobs (from repo layout and detected project language), and verification.coverageMode (from detected project language). It also writes verification.coverageScheme: "phase-qualified" unconditionally — not detected, always this value regardless of language or preset (Phase 239) — see cadence init behavior.

Unsure what your config actually does? Don’t read this whole page — run cadence config explain. It renders your active configuration in plain language: which gates fire for each tier, which provider backs each gate, and any config-semantic foot-guns (e.g. a real provider set with no API key).

To change the behavior-shaping keys interactively, use cadence config edit; to see what your current config does, cadence config explain.



Field Type Default Description
$schema string (optional) JSON Schema URL for editor validation. Omit if not using a schema validator.
schemaVersion 1 (literal) Must be 1. Required. Signals the config format version to the parser.
profile "strict" | "standard" | "auto" "auto" Gate-involvement profile. Controls how many gates fire per phase. See Profiles × tiers.
loopEnforcement "strict" | "soft" | "reminder" "soft" How hard CADENCE pushes back on out-of-loop actions. strict refuses; soft warns and continues; reminder logs only.
acDiscipline "strict" | "tier-scaled" | "optional" "tier-scaled" AC (acceptance-criteria) discipline level. strict requires every task to have ACs; tier-scaled scales requirements by tier; optional does not enforce.
workstreamBackend "simple" | "multi-branch" | "custom:<id>" "simple" Storage backend for workstream state. simple uses a single .cadence/ dir; multi-branch manages one dir per git branch; custom:<id> delegates to a registered plugin.
ruleProvider "trigger-taxonomy" | "carl" | "custom:<id>" "trigger-taxonomy" Source of trigger-rule definitions. trigger-taxonomy is the built-in ruleset; carl is an alternate built-in; custom:<id> loads a plugin.
commitCadence "task" | "draft" | "manual" "draft" When CADENCE recommends committing. task = after each task; draft = after each settled phase; manual = never auto-prompted.

Controls subagent dispatch thresholds.

Field Type Constraints Default Description
subagentPolicy.contextBudgetThreshold number 0.30.95 0.7 Fraction of the context window consumed before CADENCE considers spawning a subagent (0.3–0.95).
subagentPolicy.largeTaskTokens integer (positive) 8000 Token count above which a task is classified as “large” and may be routed to a subagent.
subagentPolicy.mechanicalBatchMin integer (positive) 3 Minimum number of mechanical tasks required before CADENCE batches them into a single subagent dispatch.

Maps task-complexity classes to model identifiers. Values are passed directly to the provider; use any model string your provider accepts.

Field Type Default
modelPerClass.mechanical string "claude-haiku-4-5-20251001"
modelPerClass.standard string "claude-sonnet-4-6"
modelPerClass.complex string "claude-opus-4-7"
modelPerClass.drafting string "claude-opus-4-7"

Field Type Default Description
templates.dir string ".cadence/templates" Directory CADENCE searches for template overrides. Relative to repo root.
templates.overrides string[] [] List of template names that are replaced by files in templates.dir.

Boolean switches that enable or disable each Claude Code hook. Hooks are registered in settings.json; these flags tell CADENCE whether to install/activate them.

Field Type Default Description
hooks.sessionStart boolean true Run the session-start hook (prints loop state at the top of each Claude session).
hooks.stopReminder boolean true Run the stop-reminder hook (prints a commit/loop reminder when Claude stops).
hooks.preToolUseBuildGate boolean false Run the pre-tool-use build gate hook (blocks destructive tool calls when a phase is mid-flight and no task is active).
hooks.userPromptSubmit boolean true Run the user-prompt-submit hook (injects loop state into each human turn).

Extension pack management.

Field Type Default Description
packs.enabled string[] [] Pack IDs to activate.
packs.disabled string[] [] Pack IDs to suppress even if auto-discovered.

All telemetry is local unless remoteOptIn is true.

Field Type Default Description
telemetry.tokenUtilization boolean true Track token counts per phase for the local usage report.
telemetry.skillInvocations boolean true Track which skills fire per session for the local usage report.
telemetry.remoteOptIn boolean false Send anonymized telemetry to the CADENCE telemetry endpoint. Off by default.

Boundary definitions for each tier. The coherence-check gate validates DRAFT frontmatter against these values. See Profiles × tiers.

Field Type Default Description
tier.quickFix.maxTasks integer 1 Maximum tasks allowed in a quick-fix phase.
tier.quickFix.maxFiles integer 1 Maximum touched files allowed in a quick-fix phase.
tier.standard.maxTasks integer 5 Maximum tasks allowed in a standard phase.
tier.standard.maxFiles integer 8 Maximum touched files allowed in a standard phase.
tier.complex.maxTasks integer 999 Upper cap on tasks for a complex phase (effectively unlimited).
tier.complex.minTasks integer 6 Minimum tasks required in a complex phase.

Field Type Default Description
verification.testGlobs string[] ["packages/**/*.test.ts", "packages/**/*.test.tsx"] Glob patterns the test-coverage scanner walks when checking AC coverage. Supports ** and *. Set by cadence init based on repo layout and detected project language (Phase 166) — see cadence init behavior.
verification.testCommand string (optional) derived by cadence init — see cadence init behavior Shell command the build-test-must-pass gate runs at cadence settle run. When set, settle runs it and refuses on a non-zero exit unless --allow-failing-build / --force. When absent, the gate is evaluated but cannot enforce — it still passes, but (as of Phase 139) writes a loud, non-blocking stderr notice instead of passing silently.
verification.coverageMode "mention" | "assertion" "assertion" for a fresh cadence init when the detected project language is js/ts, "mention" for every other detected or unknown language (Phase 166); the schema-level fallback for configs that predate this field stays "mention" How the test-coverage gate counts an AC-N token. mention counts any occurrence of the token anywhere in a matched test file, including comments. assertion counts it only when it sits inside an asserting test block for that file’s language; a comment-only or assertion-less mention is reported as a weak link and the gate refuses with a distinct hint (closing the “mentioned-but-not-tested” false positive). assertion mode has real span-parsing support for five built-in languages — js/ts, python, go, rust, php — dispatched per file by extension; see the supported-language matrix. Edit it with cadence config edit coverageMode.
verification.coverageScheme "bare" | "phase-qualified" "bare" — both the schema-level default and what every config that predates this field (or has no config file at all) resolves to; "phase-qualified" for a fresh cadence init, written explicitly and unconditionally regardless of detected language or preset (Phase 239) — see cadence init behavior Whether the test-coverage gate’s AC-N token match must also carry the active slice’s own id prefix. bare is the historical unqualified token — any AC-N occurrence counts as evidence, including a different phase’s identically-numbered AC, since AC ids restart at AC-1 every phase. phase-qualified requires the literal <slice-id>/AC-N form (e.g. 239-01/AC-3 — CONTEXT.md’s slice id, not the phase directory name); a bare or foreign-phase token is dropped and does not count as evidence, and cadence verify phase’s replay switches from file-scoped to config-scoped-by-token accordingly — see verify phase. Edit it with cadence config edit coverageScheme.
verification.coverageProfiles CoverageProfileConfig[] [] Operator-defined custom assertion-mode profiles that extend assertion mode to a language with no built-in profile. Validated at config-load time — a bad regex, a missing required field, or an extension collision with a built-in profile refuses loudly, naming the offending field/collision and suggesting a fix. Add-only: a custom profile can never override a built-in’s extensions. See Custom coverage profiles.

Supported-language matrix (assertion mode)

Section titled “Supported-language matrix (assertion mode)”

assertion mode’s span-finder is one shared string/comment-aware engine (phase 167) parameterized by a per-language profile: an opener pattern (what counts as a test), an assertion pattern (what counts as asserting inside it), a comment/string table (so text inside comments/strings is never mistaken for code), and one of four block-boundary strategies. Each test file is dispatched to the profile registered for its extension; a file whose extension no profile claims — or a block shape no profile positively recognizes — contributes zero spans (false-negative-over-false-positive is the invariant: an unrecognized shape never fabricates a span). The table below is generated from, and kept in sync with, the live profile registry (packages/core/src/verify/coverage-profiles/registry.ts’s listProfiles()) by a doc-content test (packages/core/tests/docs/coverage-language-matrix.test.ts) that fails on drift in either direction — a profile added without a matching row here, or a row here with no matching profile, both fail the build.

Language Profile id Extensions Block strategy Recognized shape(s)
JS/TS js-ts .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts call-expression it(...) / test(...) calls, optionally chained with .only/.skip/.todo/.concurrent/.failing, asserting via expect(...), a bare assert, or .should. .skip/.todo/.failing mark the test skipped — an intact assertion inside one does not count as qualifying coverage (Phase 169).
Python python .py indentation-delimited [async ]def test_<name>(...): — module-level or a class method (indentation is resolved relative to the def line itself, not the enclosing class) — asserting via a standalone assert statement. A def not prefixed test_ is never an opener, matching pytest’s own collection rule.
Go go .go brace-delimited func Test<Name>(t *testing.T) where <Name> is empty or starts with an uppercase letter, digit, or underscore (mirrors go vet’s own naming rule) and the parameter’s declared type is literally *testing.T, asserting via t.Error/t.Errorf/t.Fatal/t.Fatalf or testify’s assert.<Method>(...)/require.<Method>(...). Table-driven tests and t.Run(...) subtests fold into the outer function’s span — they are never mistaken for a second, independent test opener.
Rust rust .rs brace-delimited #[test]-attributed functions — optionally stacked with #[should_panic], and/or nested inside #[cfg(test)] mod tests { ... } — asserting via assert!, assert_eq!, or assert_ne!. A bare #[should_panic] with no #[test] on the same function is never treated as a test, matching real Rust semantics.
PHP php .php brace-delimited Both shapes in one profile: Pest it('description', function () { ... }) / test('description', function () { ... }) closures asserting via expect(...) (a description string is required — a bare closure with no description never matches), and PHPUnit public function test<Name>(...): ... { ... } methods asserting via $this->assert*(...). A non-asserting test-prefixed PHPUnit method still yields a span (with no assertion found), matching PHPUnit’s own reflection-based discovery, which collects it regardless.

Each built-in profile’s own module (packages/core/src/verify/coverage-profiles/{js-ts,python,go,rust,php}.ts) documents its fixed assertion set, string/comment edge cases (e.g. Go’s raw strings, Rust’s arbitrary-hash-count raw strings and non-nesting block comments, PHP’s heredoc/nowdoc), and — for go/rust/php — the specific spoofing vectors their reviews found and closed (a comment- or string-embedded fake opener token, a nested-parens false terminator, an unmasked heredoc). Read the profile’s docstring for the full detail behind any given row above.

To see exactly which test files, profile, and spans a specific AC resolved against — and why a span did or didn’t satisfy the configured coverageMode — run cadence verify coverage --explain AC-N (--json for machine consumption); it is read-only and never mutates loop state.

verification.coverageProfiles extends assertion mode to a language with no built-in profile — an unsupported language is never a dead end. Each entry is a JSON-serializable mirror of the engine’s internal LanguageProfile shape (CoverageProfileConfig, packages/types/src/config.ts); openerPattern/assertionPattern are regex source strings, compiled (with the sticky y flag added to openerPattern) at config-load time — an invalid regex is refused loudly, naming the profile id and the field.

Field Type Required Description
id string yes Unique profile id, e.g. "ruby-rspec". Named in load-time refusal messages.
extensions string[] (min 1) yes Lowercase file extensions this profile claims, e.g. [".rb"]. Refused at load time if any extension is already owned by a built-in profile — add-only, never an override.
openerPattern string (regex source) yes What counts as a test opener. Compiled with the sticky y flag.
assertionPattern string (regex source) yes What counts as an assertion inside an opener’s resolved block.
strategy "call-expression" | "brace-delimited" | "indentation-delimited" | "do-end-keyword" yes Which block-boundary primitive resolves this profile’s opener. do-end-keyword bounds the block from a block-opening keyword (e.g. do) to a matching end-family keyword — the shape ruby/elixir-style languages need, and the one no built-in profile ships (it’s exercised end-to-end by a custom-profile test fixture instead).
syntax { comments: { line?: string[], block?: [string, string][] }, strings: { open: string, close?: string, escape?: string | null }[] } yes Comment/string delimiter table used to mask non-code text before opener/assertion matching. Required — even an empty table must be declared explicitly, rather than silently getting no masking.
keyword { blockOpenKeywords: string[], endKeyword: string } only when strategy is "do-end-keyword" Refused at load time, naming the profile id, when missing for that strategy.
openerMatchesStrings boolean no (default false) Default false masks string content from opener matching, same as every other masked region (the false-positive-averse default). Set true only when the opener’s own syntax legitimately spans a quoted string as structure, not incidental content — e.g. a do-end-keyword opener like it 'title' do, where the quotes are the framework’s own title-delimiter syntax.

A collision with a built-in extension (.ts, .py, .go, .rs, .php, …) is refused loudly at load time, naming the colliding extension and the built-in profile that already owns it, with a suggested fix (choose a different extension, or drop the entry) — custom profiles are add-only by design (phase 167 operator decision, 2026-07-11).

Example — a Ruby-style do-end-keyword profile for RSpec’s it '...' do ... end blocks (adapted from the fixture packages/core/tests/verify/coverage-profiles-custom.test.ts exercises end-to-end):

.cadence/config.json
{
"verification": {
"coverageProfiles": [
{
"id": "ruby-rspec",
"extensions": [".rb"],
"openerPattern": "\\bit\\s+'[^']*'\\s+do\\b",
"assertionPattern": "\\bexpect\\s*\\(",
"strategy": "do-end-keyword",
"keyword": { "blockOpenKeywords": ["do"], "endKeyword": "end" },
"openerMatchesStrings": true,
"syntax": {
"comments": { "line": ["#"] },
"strings": [{ "open": "'" }, { "open": "\"" }]
}
}
]
}
}

openerMatchesStrings: true is needed here because the opener’s own syntax (it 'title' do) legitimately spans the quoted title — without it, the title’s quotes would be masked to blank space along with every other string and the opener would never match at all.


Drives the skill-audit check, which enforces that declared required skills were actually invoked during a phase. Declaring required skills (here or via a DRAFT’s requiredSkills) is the opt-in — the check is inert when the effective required set is empty.

Field Type Default Description
skillAudit.required string[] [] Skill IDs that must be invoked before a phase settles. At cadence settle run, this set is unioned with the DRAFT’s requiredSkills; if any are missing from telemetry, settle refuses unless --allow-skill-audit-miss is passed. Enforcement is skipped (with a skill-audit-miss warning) when telemetry.skillInvocations is false.

Bounds the convergent review loops (spec-review and plan-review).

Field Type Constraints Default Description
convergence.maxAttempts integer (positive) 3 Maximum review attempts before a convergent gate (spec-review, plan-review) gives up and requires a human decision. After maxAttempts non-passing attempts, the gate refuses unless the relevant --allow-*-failure flag is set.

Seven gates delegate to an AI verifier. Each block has the same shape:

{
"provider": "mock" | "anthropic" | "local", // default: "mock"
"model": "<string>" // optional; overrides provider default
}
Field Gate it controls When the gate fires
specReview spec-review cadence spec approve (always runs at this step); convergent loop bounded by convergence.maxAttempts; non-passing/unconverged refuses approve unless --allow-spec-review-failure
uiSpecReview ui-spec-review cadence spec approve (runs only when a sibling <id>-UI-SPEC.md is present); convergent loop bounded by convergence.maxAttempts; non-passing/unconverged refuses approve unless --allow-ui-spec-review-failure
verifier deep-verify cadence settle run (when gate is in the active set)
perTaskVerifier per-task-verify cadence build task <id> --status=DONE (when gate is in the active set: strict × standard or strict × complex)
codeReview code-review cadence settle run (when gate is in the active set); HIGH findings refuse settle unless --allow-code-review-failure / --force
planReview plan-review cadence draft approve (strict × complex only); pass=false refuses approve unless --allow-plan-review-failure
securityAudit security-audit cadence settle run after code-review (strict × complex only); CRITICAL findings refuse settle unless --allow-security-audit-failure / --force

All seven blocks default to { "provider": "mock" }. Provider options:

Provider Description Requires
mock Deterministic offline placeholder, no network call. Not real verification. For verifier/perTaskVerifier (deep-verify/per-task-verify): only checks each AC links to a test; always passes when linked. For the five review-family gates (codeReview, securityAudit, planReview, specReview, uiSpecReview): still runs its deterministic checks (e.g. flagging an added console.log( as a code-review finding) and can still refuse on a real finding — but a genuinely clean mock pass is recorded as abstained, not as a persisted pass, so provenance can never claim a mock placeholder “reviewed” something. Nothing
anthropic Calls the Anthropic API. ANTHROPIC_API_KEY in environment or a .env file at the repo root
local OpenAI-compatible /v1/chat/completions endpoint (e.g. Ollama). CADENCE_LOCAL_BASE_URL + CADENCE_LOCAL_MODEL; falls back to mock with a warning if unset

See docs/concepts.md — Providers for conceptual detail and docs/providers.md for setup instructions.

Turning on real verification. Don’t hand-edit these blocks for a first run — use cadence activate. It flips verifier.provider from the default mock to a real provider (just the deep-verify seam by default, or --all for every block), validates your key with a live check, and prints the exact next step. The key is discovered from the environment or a .env file at the repo root and is never written here — only the provider name. cadence doctor’s verification-readiness check reports whether real verification is actually wired.

The verifier block (the deep-verify gate) takes extra fields beyond provider / model:

Field Type Default Description
verifier.diffCapBytes integer (positive) 262144 (256KB) Byte budget for the unified git diff sent to the deep-verify verifier. deep-verify feeds the verifier the actual phase diff (git diff HEAD over the touched files) so it judges the implementation, not just test-linkage. A diff larger than this cap is truncated with an explicit [diff truncated: N of M bytes] marker, and the SUMMARY’s deepVerifyMeta records truncated: true plus the original byte count.
verifier.timeoutMs integer (positive, optional) anthropic only. Per-request timeout (ms) passed to the Anthropic client. Omitted → the SDK default holds. A transient blip in a settle gate then retries (see maxRetries) before failing loud. (Phase 72)
verifier.maxRetries integer (non-negative, optional) anthropic only. Retry budget for transient (429/5xx/network) errors, passed to the Anthropic client. Omitted → the SDK default holds; an explicit 0 disables retries. (Phase 72)
verifier.localHeaders Record<string, string> (optional) local only. Extra HTTP headers merged over the base content-type on every request to the OpenAI-compatible endpoint — e.g. a custom gateway header. Merged under the Authorization bearer derived from CADENCE_LOCAL_API_KEY (see below), which custom headers can override. Values are secrets and are never logged. (Phase 72)

These extra fields apply only to the top-level verifier slice (the deep-verify gate). The other five gate provider blocks share the selection + model shape but do not read the hardening fields.

Selecting the provider at the command line. cadence settle run --verifier <mock|anthropic|local> overrides verifier.provider for one run (precedence flag > config > default mock). Token usage from a real provider is recorded on the SUMMARY’s deepVerifyMeta (inputTokens / outputTokens); dollar cost is not derived. (Phase 73)

Variable Provider Purpose
ANTHROPIC_API_KEY anthropic API key; absent → warn + mock fallback.
CADENCE_LOCAL_BASE_URL local Endpoint base URL; absent → warn + mock fallback.
CADENCE_LOCAL_MODEL local Fallback model when verifier.model is unset.
CADENCE_LOCAL_API_KEY local Optional. When set, sent as an Authorization: Bearer <key> header so token-gated OpenAI-compatible proxies work. Never logged. (Phase 72)

Controls how CADENCE surfaces anomaly events. Only fires when the anomaly-notify gate is in the effective gate set.

Field Type Default Description
notify.transport "stderr" | "file" | "none" | "webhook" "stderr" Delivery transport. stderr writes one line per event; file appends NDJSON to notify.file; none drops events silently; webhook POSTs { "events": [...] } JSON to the configured URL.
notify.file string (optional) .cadence/anomalies.log Destination path for the file transport. Ignored for other transports.
notify.webhook object (optional) Required when transport === "webhook"; ignored otherwise. The URL is sensitive (may carry an auth token) and is never logged on failure.
notify.webhook.url string (URL) Full URL to POST anomaly batches to. Required when transport is webhook.
notify.webhook.headers Record<string, string> (optional) Additional HTTP headers (e.g. Authorization).
notify.webhook.timeoutMs integer (positive, optional) Request timeout in milliseconds.

Operational diagnostic logging for CADENCE itself (the structured logger). This is distinct from telemetry/skillAudit, which track user behavior — logging is operator-facing diagnostics for why CADENCE did something (which gate refused, which hook fired, what a verifier call did). It is default-off and writes only to stderr, so it never collides with --json output or the cadence mcp serve stdio protocol channel.

Field Type Default Description
logging.level "silent" | "error" | "warn" | "info" | "debug" | "trace" "silent" Minimum severity emitted. silent emits nothing. Diagnostics are most useful at debug.
logging.format "pretty" | "json" (optional) Output format. When omitted, the logger picks pretty on a TTY (stderr) and json otherwise.

Instrumented seams (records carry a seam field): gate (settle gate decisions — skipped/passed/refused), hook (host lifecycle event dispatch), and verify (AI verifier provider calls — request/response/error, with token usage when present). Verifier auth headers and API keys are never logged.

Both fields can be overridden at runtime without editing config.jsonenv wins over config:

Variable Overrides Values
CADENCE_LOG_LEVEL logging.level silent | error | warn | info | debug | trace
CADENCE_LOG_FORMAT logging.format pretty | json
Terminal window
# Turn on debug diagnostics for a single command, JSON to stderr:
CADENCE_LOG_LEVEL=debug CADENCE_LOG_FORMAT=json cadence settle run --auto

An invalid env value is ignored (falls through to config, then the default) rather than failing the command.

The two interactive gates (approve, interactive-verdict) auto-bypass in a non-TTY by default (v1.29) so agents and CI don’t hit a stdin is not a TTY error. These env vars tune that behavior (no config.json field — env only):

Variable Effect
CADENCE_REQUIRE_TTY=1 Restore the strict pre-1.29 refusal in a non-TTY (wins over the others).
CADENCE_NONINTERACTIVE=1 Force bypass even when a TTY is present (pty-allocated agents).
CADENCE_PROMPTER_SCRIPT=<answers> Newline-separated scripted answers; when set, the prompt is always honored (never bypassed).

See docs/concepts.md — Non-TTY auto-bypass for the full precedence and semantics.


Worktree-safety collision guard (v1.18). CADENCE’s loop state lives in the working tree, and each git worktree holds its own private .cadence/. Two worktrees branched from the same commit can both conclude “phase N is next”; with different slugs (30-auth in one, 30-cache in the other) git silently merges both in — two phase 30s, no conflict marker. The guard observes ground truth (git worktree list + the upstream integration ref) and refuses to scaffold a phase number already claimed by a sibling worktree or upstream, naming the conflict and the next free number, so the collision fails loud before wasted work. See the worktree-concurrency note.

Field Type Default Description
phaseGuard.enabled boolean true Master switch. false disables the guard entirely (scaffolding behaves exactly as pre-v1.18).
phaseGuard.integrationRef string "main" The upstream ref checked for already-merged phases — origin/<integrationRef>. Retarget if your project integrates on a branch other than main.

The guard fires at scaffold time (cadence spec new / cadence draft new, before any file is created) and as a settle backstop (cadence settle run, catching a scaffold-race). It is best-effort: a non-git checkout, a missing origin, an offline run, or a configured integrationRef absent on the remote each degrade silently to “no extra data” — the only hard failure is an actual detected collision. Bypass a specific run with --allow-phase-collision (which never bypasses the existing local same-directory refusal).

Terminal window
# A sibling worktree already holds phase 30 — this refuses, suggesting 31:
cadence draft new 30-cache 01
# Proceed anyway (you know what you're doing):
cadence draft new 30-cache 01 --allow-phase-collision

Handoff retention (v1.20). cadence handoff writes dated SESSION-*.md docs into .cadence/handoff/; left unmanaged they accumulate indefinitely. An opt-in, count-based retention policy prunes the stale ones at handoff-write time (not settle — settle fires per-phase and would race the lastHandoff pointer mid-session). Selection is deterministic and offline (lexicographic-descending by filename — the ISO date prefix sorts chronologically — no git introspection).

Field Type Default Description
handoff.retain integer >= 1 (optional) unset Keep the N most-recent SESSION-*.md docs and hard-delete the rest on each handoff write. Unset disables pruning entirely (current behavior). The just-written doc (the new lastHandoff) is always the newest, so it is never deleted.

Pruning is best-effort and never silently destructive: it only runs when retain is set, always keeps the active handoff, reports what it removed (handoff: pruned N stale doc(s): …), and a failure (unreadable config, unlink error) leaves the handoff intact rather than failing it. Because the behavior is opt-in and the dated archive that cadence resume relies on is preserved (the newest N), nothing the resume flow needs is destroyed.

cadence doctor includes a handoff-retention check: ok when the count is within the retain budget (or retention is set and the next write will self-heal the excess), and a warning only when retention is unset and at least 10 docs have piled up — suggesting a handoff.retain value to cap the growth. cadence doctor --fix auto-remediates that warning: it sets handoff.retain to the default (10) and immediately prunes the archive down to budget (see docs/reference/commands.md’s cadence doctor section for the full --fix classification table).

// .cadence/config.json — keep the 10 most-recent session handoffs
{ "handoff": { "retain": 10 } }

Cross-worktree handoff discovery (v1.38, phases 142–143). cadence handoff writes a dated SESSION-*.md doc per worktree’s own .cadence/handoff/; a bare cadence resume now looks beyond the local worktree for other resumable sessions before falling back to the old local-only behavior.

Field Type Default Description
resume.crossWorktree boolean true Master switch. false disables cross-worktree discovery entirely — cadence resume behaves exactly as it did before phases 142/143 (local worktree only).
resume.autoList boolean false When 2+ resumable candidates exist across worktrees, true opens the interactive picker automatically instead of resuming local and printing a stderr nudge (cadence resume --list).
resume.remoteCheck boolean true Origin-freshness probe (phase 163): resume runs a best-effort git fetch and warns when origin has commits this clone lacks, so a stale handoff superseded by another machine’s push isn’t silently replayed. false (or --offline) skips the fetch entirely — the fetch only touches remote-tracking refs, never the working tree.

This block is schema-only: like phaseGuard/handoff/logging, it is deliberately absent from config edit’s wizard catalog and config explain’s field rows, which are curated to a smaller, guided field set. Hand-edit .cadence/config.json to change these values. See the worktree-concurrency note and cadence resume for the full candidate-discovery and picker behavior.

// .cadence/config.json — never look outside this worktree
{ "resume": { "crossWorktree": false } }

Recommendation retention (v1.24). Terminal recommendations (shipped/rejected/ converted) drop out of the active cadence recommend surface but, left unmanaged, accumulate in .cadence/intelligence/recommendations.json forever. Soft-archival moves a finished rec aside into the ledger’s archived array — recoverable via cadence recommendation unarchive, never deleted — keeping the active ledger lean while preserving provenance.

Field Type Default Description
recommendations.autoArchive boolean true Automatically soft-archive a rec immediately when it reaches a terminal state (shipped/rejected) via recommendation promote. Also gates the settle-time transition of a converted rec to settle-pending (see below) — off leaves such a rec at converted through settle. Set false to leave terminal recs in the active ledger. Manual recommendation archive/unarchive work regardless.
recommendations.autoRoute boolean true Automatically route identified code-review findings into the recommendation ledger at settle time (Phase 242), as Recommendation entries with source: 'review'. Keyed on Finding.id, so a re-settle of an unchanged phase never mints a duplicate entry for a finding already routed. Set false to leave settle’s ledger writes limited to the existing auto-archive hook.

Unlike handoff.retain (a hard delete, so opt-in/off), auto-archival is recoverable and so defaults on. Archiving itself is only ever immediate-on-terminal-status; a converted rec whose phase settles is not archived — instead it moves to the non-terminal settle-pending status instead (see recommendation promote), staying in the active ledger until it’s later promoted to shipped (which does archive it). Both the settle-pending transition and the terminal-status archival are best-effort — a failure never blocks or fails the settle. The archive is viewable with cadence recommendation list --archived.

// .cadence/config.json — keep terminal recs in the active ledger (disable auto-archive)
{ "recommendations": { "autoArchive": false } }

autoRoute gates a separate settle-time writer: turning identified code-review findings (ones that carry a stable Finding.id, per phase 236’s identity work) into ledger Recommendation/Evidence entries, so real findings stop being stranded in the SUMMARY. A finding with no stable id (e.g. a security-audit finding, which has no identity wired in) is skipped, never force-routed. Like autoArchive, this is best-effort — a routing failure (e.g. a ledger write error) never blocks or fails settle, and always prints a stderr notice rather than failing silently.

// .cadence/config.json — never auto-route findings into the ledger
{ "recommendations": { "autoRoute": false } }

Post-settle retro artifact (Phase 174, rec-20260712-001). On every successful settle, cadence synthesizes a friction digest — gate bypasses, tasks whose terminal status wasn’t a clean DONE, and any code-review / security-audit / boundary-scan findings — purely from the SUMMARY it already assembled, and writes it as <draftId>-RETRO.json/.md alongside SUMMARY.json/.md. When the digest is non-empty and the run is interactive, it also offers to file a GitHub issue for it via gh (auto-labeled needs-triage on a best-effort basis after creation).

Field Type Default Description
retro.enabled boolean true Master switch. false disables both the artifact write and the GitHub issue offer entirely.
retro.offerGithubIssue boolean true Sub-toggle. false keeps writing the retro artifact but never prompts to file a GitHub issue, even when the digest has friction.

The GitHub issue offer only appears when the digest is non-empty, the run is interactive (a real TTY, or the CADENCE_PROMPTER_SCRIPT test seam), and gh can resolve the current repo (gh repo view). It never appears in CI or any other non-TTY run.

{ "retro": { "enabled": true, "offerGithubIssue": false } }

Sealed-gate enforcement (Phase 141). A gate id listed in gates.sealed becomes non-bypassable at cadence settle run: both the global --force flag and that gate’s own per-gate --allow-* flag are ignored, and the refusal message names gates.sealed instead of the usual bypass hint. This closes the gap where an operator (or an agent under pressure) could wave away the exact gates a production-tier project cares about most.

Field Type Default Description
gates.sealed string[] [] Gate ids that cannot be bypassed at settle time. An empty array (the default) restores normal bypass behavior for every gate.
gates.evidenceFloor "ai-verified" | "executed" | "assertion" | "mention" | "unverified" "mention" (schema-level, back-compat) Minimum evidence class every PASS-verdicted AC must meet at settle, on the Phase 140 evidence ladder ranked strongest to weakest: ai-verified > executed > assertion > mention > unverified. See evidenceFloor below.

Only three gate ids currently check gates.sealed and are meaningful here:

Gate id Bypass flag(s) it seals shut
test-coverage --allow-missing-coverage, --force
build-test-must-pass --allow-failing-build, --force
boundary-scan --allow-boundary-scan-failure, --force

Naming any other gate id in gates.sealed currently has no effect — only test-coverage, build-test-must-pass, and boundary-scan consult isGateSealed; the other gates’ bypass flags are unaffected regardless of what’s listed here.

Sealing binds the auto settle path (cadence settle run --auto, the one agents and CI use) — cadence settle run --interactive (non-auto) still lets test-coverage pass without computing coverage, since that mode delegates per-AC verification to the interactive-verdict human walker instead.

// .cadence/config.json — seal the two gates the production preset seals by default
{ "gates": { "sealed": ["test-coverage", "build-test-must-pass"] } }

See docs/concepts.md — Gate bypass reference summary for the full bypass-flag table, covering all three sealed-eligible gates, that these config entries interact with.

Minimum-evidence floor (Phase 214). Closes the enforcement gap left by the visibility-only Phase 140 evidence ladder: before this gate, a weak-evidence AC (e.g. a comment-only AC-N mention) was reported in SUMMARY.json/.md but never blocked settle. gates.evidenceFloor makes that ladder load-bearing — the evidence-floor gate (see docs/concepts.md — the gate universe) refuses settle when any PASS-verdicted AC’s deriveAcEvidence result ranks below the configured floor, naming every offending AC id with its actual level vs. the required floor.

The five-level ladder, ranked strongest to weakest:

Level Meaning
ai-verified A real (non-mock) deep-verify provider returned pass: true for the AC. A mock-provider pass never counts here — mock is a placeholder, not real verification (same v1.25 mock-honesty precedent deriveAcEvidence already applies).
executed The AC has a qualifying (assertion-mode) linked test and build-test-must-pass actually ran the suite this settle.
assertion The AC has a qualifying linked test (an AC-N token inside an asserting it()/test() block, per verification.coverageMode: "assertion"), but the suite wasn’t confirmed to have run this settle.
mention The AC is only referenced by an AC-N token somewhere in a matched test file — including a comment, or a coverageMode: "mention" match with no assertion-block requirement.
unverified No linked test evidence at all.
Field Type Default Description
gates.evidenceFloor "ai-verified" | "executed" | "assertion" | "mention" | "unverified" "mention" at the schema level The minimum ladder level every PASS-verdicted AC must meet. "mention" is the weakest rung and is deliberately the schema-level default — it is a back-compat no-op: no config predating this field newly refuses. cadence init (all three presets) writes a stricter value, described next.

Per-preset defaults (set by cadence init, not the schema default above):

Preset gates.evidenceFloor
solo "assertion"
team "executed"
production "executed"

ai-verified is reachable only via an explicit config override — no preset defaults to it, since requiring a real (non-mock) deep-verify provider just to pass the floor was deliberately rejected as a preset default (independent review, ev-20260724-010 on rec-20260724-001); a production-tier project that wants that stronger guarantee opts in explicitly.

mention is reachable the same two ways as any looser-than-default value: an explicit gates.evidenceFloor: "mention" override, or a named, per-AC, reason-required bypass (below) — never as a preset default.

Bypass. --evidence-floor-bypass <AC-id:reason> (repeatable) exempts exactly the named AC from the floor for that settle run — never a blanket, phase-wide bypass. A non-empty reason is required; the bypass (AC id + reason) is recorded in SUMMARY.gateBypasses, so an exemption is always auditable after the fact.

// .cadence/config.json — require a real deep-verify pass for every AC (explicit opt-in)
{ "gates": { "evidenceFloor": "ai-verified" } }

Note: ai-verified evidence is structurally unreachable while the active deep-verify provider is mock (the schema/cadence init default) — a mock pass never counts as ai-verified. Configuring this floor without also running a real provider (cadence activate, or --deep with verifier.provider set to something other than mock) makes every settle refuse forever. The refusal names this specific structural reason rather than the generic below-floor message, so the cause is diagnosable from the CLI output alone.

Terminal window
# Settle past the floor for exactly AC-3, with a required reason:
cadence settle run --auto --evidence-floor-bypass "AC-3:legacy AC, backfill test tracked in issue #300"

See docs/concepts.md — the gate universe for the gate’s place among the other 14 gates and its full bypass-flag entry.


Boundary enforcement mode (Phase 155). A DRAFT declares files: per task — the allow-list of files that task may touch. boundaryEnforcement controls what happens when an edit falls outside that declared set.

Field Type Default Description
boundaryEnforcement "warn" | "block" "warn" warn — an out-of-boundary edit is only notified via the anomaly-notify gate (unchanged historical behavior). block — the pre-tool-edit hook refuses the edit outright, wherever the host surfaces the touched files before the write lands.

block mode fails open (never blocks) in two cases: when there is no active draft/phase, and when the active draft’s tasks declare zero files in total — an enforcement mode never blocks 100% of edits as a side effect of a DRAFT that omits files:.

Overridable per-phase via DRAFT frontmatter, mirroring profile:

---
phase: 42-example
id: 42-01
tier: standard
boundaryEnforcement: warn
status: PENDING
---

A DRAFT-level override wins over the project default in .cadence/config.json.

// .cadence/config.json — refuse out-of-boundary edits project-wide
{ "boundaryEnforcement": "block" }

Scope note: block mode is enforced at three independent points. It fires on PreToolUse (edit-time), which includes subagent-originated edits (Claude Code’s PreToolUse hook fires for subagent tool calls, not just main-thread ones). A settle-time diff scan (phase 156, boundary-scan gate) is a second, independent line of defense for anything that slips past edit-time detection. Since phase 280 (the dispatch contract), cadence build task <id> --status=DONE also enforces it at record time: a stray working-tree file outside every task’s declared files: refuses the recording (exit 1, no state mutation) unless --allow-boundary-breach is passed — see docs/reference/commands.md’s build task entry.

Dispatch-scoped auto-escalation: independent of this config field’s own value, once any task in the active phase has been recorded with --execution dispatch, both the record-time check and the settle-time boundary-scan gate escalate to block mode for the rest of the phase — even when this field is left at its default warn. This never de-escalates for the phase’s remaining tasks. An operator who has never touched boundaryEnforcement can still see a hard record-time refusal once a dispatched task is recorded; that refusal is the escalation, not a misconfiguration.


Redundant-work enforcement mode (subagent task-redundancy monitoring). A DRAFT task’s files: declares what it owns; once that task’s PROGRESS.json status reaches DONE/DONE_WITH_CONCERNS, redundantWorkEnforcement controls what happens when an edit touches one of those files again.

Field Type Default Description
redundantWorkEnforcement "off" | "warn" | "block" "warn" off — the check never runs. warn — a redundant-work edit is only notified via the anomaly-notify gate. block — the pre-tool-edit hook refuses the edit, and a SubagentStop safety net can hard-block a subagent’s turn from ending if it slipped through edit-time detection.

Unlike boundaryEnforcement ("warn" | "block" only), this field has a third "off" value: re-touching finished work is a more subjective signal than an out-of-boundary edit (fixing a bug just introduced by that task is a legitimate reason to touch it again), so a full opt-out is offered.

Overridable per-phase via DRAFT frontmatter, mirroring boundaryEnforcement:

---
phase: 42-example
id: 42-01
tier: standard
redundantWorkEnforcement: block
status: PENDING
---
// .cadence/config.json — refuse redundant edits project-wide
{ "redundantWorkEnforcement": "block" }

Scope note: this check runs at edit time (handlePreToolEdit, fires for both main-thread and subagent edits) and again as a SubagentStop safety net scoped to files touched during that specific subagent’s run (tracked via PostToolUse, not a git diff — a subagent’s edits are normally uncommitted working-tree changes, so there’s no commit boundary to diff against). It does not track cross-worktree task status, and it does not know which task a subagent was assigned — only whether a touched file’s owning task is already finished.


Reading your config — cadence config explain

Section titled “Reading your config — cadence config explain”

This page is a field reference. To see what your config actually does — without reading all of it — run:

Terminal window
cadence config explain # curated, plain-language summary
cadence config explain verifier # deep-dive a single block
cadence config explain --all # every key, grouped
cadence config explain --json # structured output for scripting

The default view has five parts:

  1. Profile & enforcementprofile, loopEnforcement, acDiscipline, each with a one-line meaning.
  2. Gates that fire, by tier — the concrete gate set for quick-fix / standard / complex under your profile (the ← current marker points at the active phase’s tier when a phase is mid-loop). This is the profile × tier matrix resolved for your settings.
  3. Verifier & gate providers — the seven provider blocks (specReview, uiSpecReview, verifier, perTaskVerifier, codeReview, planReview, securityAudit) collapsed into one table, flagging which run mock (offline — no real AI verification).
  4. Warnings — config-semantic foot-guns, shown only when they apply:
    • a provider set to anthropic/local with its API key (ANTHROPIC_API_KEY / CADENCE_LOCAL_API_KEY) unset — it will silently fall back to mock;
    • a hooks.* flag enabled while the host adapter is not installed (no managed entry in .claude/settings.json) — the hook does nothing until cadence-host-claude-code install;
    • the auto × complex soft cap — complex phases under the auto profile refuse to approve/settle without --allow-auto-complex.
  5. Footer — pointers to <field>, --all, and cadence doctor.

config explain is read-only and describes; it does not judge structural health. It is complementary to:

  • cadence config doctor — flags conflicting config pairs (e.g. strict loopEnforcement with manual commit cadence).
  • cadence doctor — the full structural health check (Node version, init state, git hooks, host hooks, worktree collisions, handoff retention).

The three share detection logic where they overlap (e.g. the host-hooks-installed check), so their answers never drift. When config explain raises a warning, it points you at cadence doctor for the complete picture.


cadence init --profile <preset> seeds config.json from one of three presets. The table shows only the fields that differ from defaultConfig; all other fields take the defaultConfig value.

Field solo team (default) production
loopEnforcement "reminder" "soft" "strict"
acDiscipline "optional" "tier-scaled" "strict"
commitCadence "manual" "draft" "draft"
hooks.preToolUseBuildGate false false true
gates.sealed [] [] ["test-coverage", "build-test-must-pass"]
gates.evidenceFloor "assertion" "executed" "executed"

The production preset seals test-coverage and build-test-must-pass by default (see gates) — solo/team leave gates.sealed empty, so every gate’s normal bypass flags still work.

All three presets also set gates.evidenceFloor stricter than the schema-level "mention" back-compat default (see gates.evidenceFloor) — solo to "assertion", team/production to "executed". "ai-verified" is never a preset default; it is reachable only via an explicit config override.

All other fields are identical to defaultConfig across all three presets. After scaffolding, cadence init overlays the detected profile, verification.testGlobs, and verification.coverageMode regardless of preset, and unconditionally writes verification.coverageScheme: "phase-qualified" (Phase 239) regardless of preset or detected language (see below).


cadence init writes two fields whose values depend on the project rather than the preset:

Sourced from --gate-profile <p> if provided. Otherwise:

  • If stdin is a TTY, the user is prompted (suggested value shown in brackets).
  • In non-interactive mode (no TTY, no flag), the suggestion is used directly.

The suggestion is derived from git history: a repo with 20 or more commits gets "standard"; fewer commits or any git error gets "auto".

cadence init sniffs the target repo’s root for language marker files, best-effort and never throwing (a permission or fs-read failure just falls back to unknown). Checked in this priority order, first match wins:

Marker file(s) present at init cwd Detected language
package.json js (covers JS and TS)
pyproject.toml, setup.py, or requirements.txt python
go.mod go
Cargo.toml rust
composer.json php
none of the above unknown

package.json is checked first, so a mixed-language repo with a root package.json alongside e.g. a nested pyproject.toml deterministically resolves to js. This detected language feeds both verification.testGlobs and verification.coverageMode below.

Detected from the repo layout and the detected project language at init time:

Language / layout condition Written value
js + packages/ directory exists at init cwd (monorepo) ["packages/**/*.test.ts", "packages/**/*.test.tsx"]
js + no packages/ directory (single-package) ["**/*.test.ts", "**/*.test.tsx"]
python ["**/test_*.py", "**/*_test.py"]
go ["**/*_test.go"]
rust ["tests/**/*.rs", "**/*_test.rs", "src/**/*.rs"]
php ["**/*Test.php", "tests/**/*.php"]
unknown same layout-based fallback as js (monorepo vs. single-package glob above)

The scanner prunes node_modules/, dist/, .git/, and .turbo/, so the broad single-package glob is safe. Rust’s src/**/*.rs entry was added in Phase 167 (AC-10): idiomatic Rust unit tests commonly live inline in a #[cfg(test)] mod tests { ... } block within the same file as the code under test, rather than only under tests/ or in a *_test.rs-suffixed file — safe to widen because the attribute-aware rust coverage profile only ever yields spans for genuine #[test] functions, never from ordinary source.

Derived from the target repo’s package.json#scripts.test, prefixed with the detected package manager:

Condition Written testCommand
scripts.test exists + pnpm-lock.yaml present pnpm test
scripts.test exists + yarn.lock present yarn test
scripts.test exists + bun.lockb present bun test
scripts.test exists + package-lock.json present, or no lockfile matched npm test
No scripts.test (or no package.json at all) not written — field stays absent

cadence init --dry-run previews the same derived value (or its absence) without writing.

verification.coverageMode (Phase 139, language-aware as of Phase 166)

Section titled “verification.coverageMode (Phase 139, language-aware as of Phase 166)”

A fresh cadence init originally wrote "assertion" unconditionally for every preset (Phase 139) — a comment-only AC-N mention no longer counts as tested. As of Phase 166, that default is language-aware, using the same project language detection that drives verification.testGlobs:

Detected language Written coverageMode
js "assertion"
python, go, rust, php, or unknown "mention" — init also prints a stderr notice naming the detected language and explaining why assertion mode wasn’t used

This table records what a fresh cadence init writes, not what assertion mode can actually parse — those are no longer the same thing. When Phase 166 shipped this table, assertion mode’s span-finder understood JS/TS test files only, so defaulting a non-JS/TS project to assertion would have produced a gate that could never pass no matter how well-tested the code was. As of Phase 167, the span-finder is a shared multi-language engine with real built-in support for python, go, rust, and php too — see the supported-language matrix. Phase 167 deliberately left this init default table itself unchanged (it shipped the span-parsing, not a revisit of what a fresh init auto-writes): a fresh init still writes "mention" for every non-js detected language, and switching one of python/go/rust/php to "assertion" is now a manual, informed choice via cadence config edit coverageMode — real span support exists for them today, so the switch genuinely works rather than being permanently unsatisfiable. unknown still gets "mention" since there is no profile to dispatch to at all until an operator adds one via verification.coverageProfiles.

This only affects what a new init writes; existing .cadence/config.json files are never rewritten.

Unlike coverageMode, this is not language-dependent: a fresh cadence init writes "phase-qualified" unconditionally, for every preset and every detected project language.

Config source Resolved/written coverageScheme
A config.json that predates this field, or no config file at all "bare" — resolved through loadConfig’s two-layer default described below, not merely the Zod field-level default
A fresh cadence init "phase-qualified", written explicitly into the verification overlay, unconditionally

The two-layer default exists because loadConfig (packages/core/src/config/loader.ts) deep-merges the user’s config.json over defaultConfig’s verification object, key by key, before Zod ever parses the result — so coverageScheme is copied in from defaultConfig for any pre-existing config that doesn’t specify it, and the key is therefore always present by the time Zod sees it. That makes the field-level .default("bare") a dead branch on this path: it can only fire when CadenceConfigZ parses an object directly, bypassing loadConfig’s merge entirely (e.g. a hand-built verification object in a test), never for a real .cadence/config.json read through the CLI. The load-bearing back-compat mechanism is therefore defaultConfig itself holding "bare" — if it held "phase-qualified" instead, that value would flow into every pre-existing config through the exact same merge and silently flip every consumer on upgrade. cadence init’s verification overlay is the only place that writes "phase-qualified" — it is the sole opt-in point for the whole scheme, since it is the only writer that can distinguish a fresh project from an upgraded one.

This only affects what a new init writes; existing .cadence/config.json files are never rewritten.


See also: docs/concepts.md — profiles, gates, tiers, providers | docs/providers.md — provider setup | docs/reference/commands.md — CLI command reference