# Configuration Reference `.agentrc.json` is the single configuration contract for the Mandrel framework. It is parsed at the start of every script via [`config-resolver.js`](../scripts/lib/config-resolver.js), validated against AJV schemas at runtime, and consumed through grouped accessors (`getCommands()`, `getQuality()`, `getPaths()`, etc.). This document is the reader-facing reference for the post-Epic-#1720 grouped shape. The authoritative contract is the JSON Schema mirror at [`.agents/schemas/agentrc.schema.json`](../schemas/agentrc.schema.json), which is itself a mirror of the AJV schemas under [`.agents/scripts/lib/`](../scripts/lib/). A drift test (`tests/config-schema-mirror-drift.test.js`) keeps the static mirror aligned with the runtime validators. > **Editor support.** `.agentrc.json`, `.agents/starter-agentrc.json`, and > `.agents/docs/agentrc-reference.json` all declare > `"$schema": "./.agents/schemas/agentrc.schema.json"`, so any editor with > JSON Schema support gets autocomplete and inline validation. ## Top-level shape ```jsonc { "$schema": "./.agents/schemas/agentrc.schema.json", "project": { /* paths, commands, baseBranch, docsContextFiles */ }, "github": { /* owner, repo, branchProtection, mergeMethods, notifications */ }, "planning": { /* riskHeuristics, codebaseSnapshot, context */ }, "delivery": { /* execution, quality, worktreeIsolation, deliverRunner, ... */ } } ``` The schema declares `additionalProperties: false` at the root — unknown top-level keys are validation errors. | Top-level key | Required | Purpose | | ------------- | -------- | ---------------------------------------------------------------------------------- | | `project` | **Yes** | Project-local paths, base branch, validation commands, and context-hydration files. | | `github` | No | Ticketing provider config: owner/repo, branch protection, merge methods, notifications. | | `planning` | No | `/plan` tuning: ticket budget, risk heuristics, codebase snapshot, context cap. | | `delivery` | No | `/deliver` and `/deliver` tuning: quality gates, worktree isolation, runners, lifecycle. | | `$schema` | No | JSON Schema pointer for editor tooling. | --- > Generated by `node .agents/scripts/generate-config-docs.js` from > [`.agents/schemas/agentrc.schema.json`](../schemas/agentrc.schema.json). > Edit the schema (and its AJV mirror under `.agents/scripts/lib/`), > then re-run the generator — do not hand-edit this region. ### `project` (required) | Key | Required | Type | Default | Description | | --- | --- | --- | --- | --- | | `baseBranch` | No | `string` | — | — | | `paths` | Yes | `object` | — | Nested configuration block. | | `paths.agentRoot` | Yes | `string` | — | — | | `paths.docsRoot` | Yes | `string` | — | — | | `paths.tempRoot` | Yes | `string` | — | — | | `docsContextFiles` | No | `array` | — | — | | `commands` | No | `object` | — | Nested configuration block. | | `commands.lintBaseline` | No | `string` | — | — | | `commands.test` | No | `string` | — | — | | `commands.typecheck` | No | `string` \| `null` | — | — | | `commands.formatCheck` | No | `string` | — | — | | `commands.formatWrite` | No | `string` | — | — | ### `github` (optional) | Key | Required | Type | Default | Description | | --- | --- | --- | --- | --- | | `owner` | Yes | `string` | — | — | | `repo` | Yes | `string` | — | — | | `projectNumber` | No | `integer` \| `null` | — | — | | `projectOwner` | No | `string` \| `null` | — | — | | `operatorHandle` | Yes | `string` | — | — | | `defaultTimeoutMs` | No | `integer` | — | Default `timeoutMs` (in milliseconds) applied to every `gh` subprocess spawned by the GitHub provider facade. Caps any single `gh api` / `gh issue ...` / `gh pr ...` invocation so a stalled TCP socket or long-poll cannot hang an orchestration indefinitely. A `GhExecTimeoutError` from a hit ceiling is classified `transient` and retried by `withTransientRetry`. Recommended floor 1000ms; recommended default 60000 (60s). Omit to use the in-code default of 60000. Story #2860. | | `branchProtection` | No | `object` | — | Nested configuration block. | | `branchProtection.enforce` | No | `boolean` | — | — | | `branchProtection.requiredChecks[]` | No | `array` | — | Each item (`branchProtectionCheck`) has: name, cmd. | | `mergeMethods` | No | `object` | — | Nested configuration block. | | `mergeMethods.allow_squash_merge` | No | `boolean` | — | — | | `mergeMethods.allow_rebase_merge` | No | `boolean` | — | — | | `mergeMethods.allow_merge_commit` | No | `boolean` | — | — | | `mergeMethods.allow_auto_merge` | No | `boolean` | — | — | | `mergeMethods.delete_branch_on_merge` | No | `boolean` | — | — | | `notifications` | No | `object` | — | Nested configuration block. | | `notifications.mentionOperator` | No | `boolean` | — | — | | `notifications.commentEvents` | No | `array` | — | — | | `notifications.webhookEvents` | No | `array` | — | — | ### `planning` (optional) | Key | Required | Type | Default | Description | | --- | --- | --- | --- | --- | | `riskHeuristics` | No | `string[]` or `{ append?, prepend? }` | — | — | | `context` | No | `object` | — | Nested configuration block. | | `context.maxBytes` | No | `integer` | — | — | | `context.summaryMode` | No | `"auto"` \| `"always"` \| `"never"` | — | — | | `codebaseSnapshot` | No | `object` | — | Nested configuration block. | | `codebaseSnapshot.tier` | No | `"skinny"` \| `"medium"` | — | — | | `codebaseSnapshot.include` | No | `array` | — | — | | `codebaseSnapshot.exclude` | No | `array` | — | — | | `codebaseSnapshot.recentCommitWindow` | No | `integer` | — | — | | `taskSizing` | No | `object` | — | Story-sizing thresholds consumed by ticket-validator-sizing.js. Operator overrides shallow-merge with DEFAULT_TASK_SIZING defaults. Story #3760 collapsed the per-profile matrix and the parallel testSurface axis into a flat set of knobs; the sizingProfile enum was replaced by an optional body-level `wide` declaration that lifts the hardFiles rejection. Story #3874 cut over to one uniform relaxed profile sized for capability slices a frontier model delivers and self-verifies in one pass. | | `taskSizing.softFiles` | No | `integer` | — | File-count soft-warn threshold above which a typical-Story width finding fires (default 15). | | `taskSizing.hardFiles` | No | `integer` | — | File-count hard ceiling: a Story exceeding it is rejected unless it declares `wide` with a reason (default 30). | | `taskSizing.maxAcceptance` | No | `integer` | — | Hard ceiling on acceptance[] item count (default 14). | | `taskSizing.softAcceptanceCount` | No | `integer` | — | Soft-warn threshold on acceptance[] item count (default 10). | | `failOnSharedEditors` | No | `boolean` | — | — | | `requireExplicitCrossStoryDeps` | No | `boolean` | — | — | | `failOnRegistryConflicts` | No | `boolean` | — | — | | `failOnLargeFanOut` | No | `boolean` | — | — | | `largeFanOutThreshold` | No | `integer` | — | — | | `crossCuttingRegistries` | No | `string[]` or `{ append?, prepend? }` | — | — | | `navigation` | No | `object` | — | Navigability-reachability config consumed by the epic-plan-healthcheck --paranoid reachability check. Opt-in: absent or empty routeGlobs degrades to a silent no-op. | | `navigation.routeGlobs` | No | `array` | — | Glob patterns (pages/**, app/**/route.ts) marking paths that add a user-facing route. | | `navigation.navRegistry` | No | `array` | — | Tokens identifying the nav-registry SSOT a route-adding Story is expected to reference. | ### `delivery` (optional) | Key | Required | Type | Default | Description | | --- | --- | --- | --- | --- | | `execution` | No | `object` | — | Nested configuration block. | | `execution.timeoutMs` | No | `integer` | — | — | | `maxTokenBudget` | No | `integer` | — | — | | `lease` | No | `object` | — | Story #3480 (Epic #3457). Assignee-as-lease primitive. ttlMs is the staleness window — a ticket claim whose owner has not emitted a story.heartbeat within this many milliseconds is reclaimable by another operator. Defaults to 900000 (15 min) in lib/config/limits.js. | | `lease.ttlMs` | No | `integer` | — | — | | `docsFreshness` | No | `object` | — | Nested configuration block. | | `docsFreshness.paths` | No | `array` | — | — | | `deliverRunner` | No | `object` | — | Nested configuration block. | | `deliverRunner.concurrencyCap` | No | `integer` | — | Maximum Stories dispatched in parallel within one wave. Default 3. Conservative by design — keeps host-quota consumption predictable on small waves and avoids GitHub API saturation. Operators running wide-wave Epics on hosts with adequate parallel-agent quota should raise this value to reduce wall-clock time proportionally. See epic-deliver.md § Phase 2b for the dispatch model and the throughput tradeoff discussion. | | `deliverRunner.progressReportIntervalSec` | No | `integer` | — | — | | `deliverRunner.verifyConcurrencyCap` | No | `integer` | — | Bounded-concurrency cap for the per-wave verifyWaveResults loop (Epic #3019 Tech Spec §1.4). Separate from the wave-execution `concurrencyCap` so operators can tune ticket-verification parallelism independently of Story dispatch parallelism. Default 4. | | `worktreeIsolation` | No | `object` | — | Nested configuration block. | | `worktreeIsolation.enabled` | No | `boolean` | — | — | | `worktreeIsolation.root` | No | `string` | — | — | | `worktreeIsolation.nodeModulesStrategy` | No | `"per-worktree"` \| `"clone"` \| `"symlink"` \| `"pnpm-store"` | — | — | | `worktreeIsolation.primeFromPath` | No | `string` \| `null` | — | — | | `worktreeIsolation.allowSymlinkOnWindows` | No | `boolean` | — | — | | `worktreeIsolation.reapOnSuccess` | No | `boolean` | — | — | | `worktreeIsolation.reapOnCancel` | No | `boolean` | — | — | | `worktreeIsolation.bootstrapFiles` | No | `array` | `[".env",".mcp.json"]` | — | | `signals` | No | `object` | — | Nested configuration block. | | `signals.hotspot` | No | `object` | — | Nested configuration block. | | `signals.hotspot.p95Multiplier` | No | `number` | — | — | | `signals.rework` | No | `object` | — | Nested configuration block. | | `signals.rework.editsPerFile` | No | `integer` | — | — | | `signals.retry` | No | `object` | — | Nested configuration block. | | `signals.retry.repeatCount` | No | `integer` | — | — | | `quality` | No | `object` | — | Nested configuration block. | | `quality.gateScoping` | No | `object` | — | Nested configuration block. | | `quality.gateScoping.scope` | No | `"diff"` \| `"full"` | — | — | | `quality.gateScoping.diffRef` | No | `string` | — | — | | `quality.gates` | No | `object` | — | Nested configuration block. | | `quality.gates.lint` | No | `object` | — | Nested configuration block. | | `quality.gates.lint.enabled` | No | `boolean` | — | — | | `quality.gates.lint.baselinePath` | No | `string` | — | — | | `quality.gates.lint.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.lint.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.lint.tolerance.value` | Yes | `number` | — | — | | `quality.gates.lint.floors` | No | `object` | — | — | | `quality.gates.lint.components` | No | `object` | — | — | | `quality.gates.coverage` | No | `object` | — | Nested configuration block. | | `quality.gates.coverage.enabled` | No | `boolean` | — | — | | `quality.gates.coverage.baselinePath` | No | `string` | — | — | | `quality.gates.coverage.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.coverage.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.coverage.tolerance.value` | Yes | `number` | — | — | | `quality.gates.coverage.floors` | No | `object` | — | — | | `quality.gates.coverage.components` | No | `object` | — | — | | `quality.gates.coverage.coveragePath` | No | `string` | — | — | | `quality.gates.coverage.timeoutMs` | No | `integer` | — | — | | `quality.gates.crap` | No | `object` | — | Nested configuration block. | | `quality.gates.crap.enabled` | No | `boolean` | — | — | | `quality.gates.crap.baselinePath` | No | `string` | — | — | | `quality.gates.crap.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.crap.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.crap.tolerance.value` | Yes | `number` | — | — | | `quality.gates.crap.floors` | No | `object` | — | — | | `quality.gates.crap.components` | No | `object` | — | — | | `quality.gates.crap.targetDirs` | No | `string[]` or `{ append?, prepend? }` | — | Directories whose JS sources the CRAP gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal. | | `quality.gates.crap.newMethodCeiling` | No | `integer` | — | — | | `quality.gates.crap.requireCoverage` | No | `boolean` | — | — | | `quality.gates.crap.friction` | No | `object` | — | Nested configuration block. | | `quality.gates.crap.friction.markerKey` | No | `string` | — | — | | `quality.gates.crap.refreshTag` | No | `string` | — | — | | `quality.gates.crap.refreshTimeoutMs` | No | `integer` | — | Bounded timeout (ms) for `npm run crap:update` spawned by the baseline-attribution refresh path. Mirrors `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124 so the close orchestrator can flip the Story to `agent::blocked`. Default 60000 (Story #2165). | | `quality.gates.crap.ignoreGlobs` | No | `array` | — | Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from CRAP discovery before scoring. Orthogonal to `components` (grouping) — a file excluded here never appears in any component bucket. Absent or empty preserves the existing IGNORED_DIRS-only behaviour (Story #3217). | | `quality.gates.maintainability` | No | `object` | — | Nested configuration block. | | `quality.gates.maintainability.enabled` | No | `boolean` | — | — | | `quality.gates.maintainability.baselinePath` | No | `string` | — | — | | `quality.gates.maintainability.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.maintainability.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.maintainability.tolerance.value` | Yes | `number` | — | — | | `quality.gates.maintainability.floors` | No | `object` | — | — | | `quality.gates.maintainability.components` | No | `object` | — | — | | `quality.gates.maintainability.targetDirs` | No | `string[]` or `{ append?, prepend? }` | — | Directories whose JS sources the maintainability gate scores. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/` plus `tests/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal. | | `quality.gates.maintainability.refreshTimeoutMs` | No | `integer` | — | Bounded timeout (ms) for `npm run maintainability:update` spawned by the baseline-attribution refresh path. Mirrors `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124 so the close orchestrator can flip the Story to `agent::blocked`. Default 60000 (Story #2165). | | `quality.gates.maintainability.ignoreGlobs` | No | `array` | — | Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from MI discovery before scoring. Orthogonal to `components` (grouping) — a file excluded here never appears in any component bucket. Absent or empty preserves the existing IGNORED_DIRS-only behaviour (Story #3217). | | `quality.gates.mutation` | No | `object` | — | Nested configuration block. | | `quality.gates.mutation.enabled` | No | `boolean` | — | — | | `quality.gates.mutation.baselinePath` | No | `string` | — | — | | `quality.gates.mutation.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.mutation.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.mutation.tolerance.value` | Yes | `number` | — | — | | `quality.gates.mutation.floors` | No | `object` | — | — | | `quality.gates.mutation.components` | No | `object` | — | — | | `quality.gates.mutation.strykerConfigPath` | No | `string` \| `null` | — | — | | `quality.gates.lighthouse` | No | `object` | — | Nested configuration block. | | `quality.gates.lighthouse.enabled` | No | `boolean` | — | — | | `quality.gates.lighthouse.baselinePath` | No | `string` | — | — | | `quality.gates.lighthouse.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.lighthouse.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.lighthouse.tolerance.value` | Yes | `number` | — | — | | `quality.gates.lighthouse.floors` | No | `object` | — | — | | `quality.gates.lighthouse.components` | No | `object` | — | — | | `quality.gates.lighthouse.baseUrl` | No | `string` \| `null` | — | — | | `quality.gates.lighthouse.routes[]` | No | `array` | — | Each item (`lighthouseRoute`) has: path, formFactor. | | `quality.gates.bundle-size` | No | `object` | — | Nested configuration block. | | `quality.gates.bundle-size.enabled` | No | `boolean` | — | — | | `quality.gates.bundle-size.baselinePath` | No | `string` | — | — | | `quality.gates.bundle-size.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.bundle-size.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.bundle-size.tolerance.value` | Yes | `number` | — | — | | `quality.gates.bundle-size.floors` | No | `object` | — | — | | `quality.gates.bundle-size.components` | No | `object` | — | — | | `quality.gates.bundle-size.bundles[]` | No | `array` | — | Each item (`bundleDeclaration`) has: name, path, limit. | | `quality.gates.duplication` | No | `object` | — | Nested configuration block. | | `quality.gates.duplication.enabled` | No | `boolean` | — | — | | `quality.gates.duplication.baselinePath` | No | `string` | — | — | | `quality.gates.duplication.tolerance` | No | `object` | — | Nested configuration block. | | `quality.gates.duplication.tolerance.kind` | Yes | `"absolute"` \| `"percent"` | — | — | | `quality.gates.duplication.tolerance.value` | Yes | `number` | — | — | | `quality.gates.duplication.floors` | No | `object` | — | — | | `quality.gates.duplication.components` | No | `object` | — | — | | `quality.gates.duplication.targetDirs` | No | `string[]` or `{ append?, prepend? }` | — | Directories whose JS sources the duplication (DRY) gate scans for copy-paste clones. Mandrel ships a `src/`-centric default; projects whose executable code lives elsewhere (e.g. this repo's `.agents/scripts/`) override here. The framework default is intentionally not auto-discovered, so an override is the explicit, auditable signal (Story #3664). | | `quality.gates.duplication.refreshTimeoutMs` | No | `integer` | — | Bounded timeout (ms) for `npm run duplication:update` spawned by the baseline-attribution refresh path. Mirrors `crap.refreshTimeoutMs` / `coverage.timeoutMs`: a SIGKILL fired at the budget boundary maps to exit 124. Default 60000 (Story #3664). | | `quality.gates.duplication.ignoreGlobs` | No | `array` | — | Minimatch glob patterns matched against the canonicalised repo-relative path of each discovered file. Files matching any pattern are excluded from duplication discovery before scanning. Orthogonal to `components` (grouping). Absent or empty preserves the existing behaviour (Story #3664). | | `quality.formatAutofix` | No | `object` | — | Bounded-timeout knob for the close-time `npx biome format --write` spawn (Story #2165). A SIGKILL fired at the budget boundary maps to exit 124 so the close orchestrator can flip the Story to `agent::blocked` with a friction comment. | | `quality.formatAutofix.timeoutMs` | No | `integer` | — | — | | `quality.codingGuardrails` | No | `object` | — | Nested configuration block. | | `quality.codingGuardrails.cyclomaticFlag` | No | `integer` | — | — | | `quality.codingGuardrails.cyclomaticMustFix` | No | `integer` | — | — | | `quality.codingGuardrails.miDropMustRefactor` | No | `number` | — | — | | `quality.codingGuardrails.requireSiblingTest` | No | `boolean` | — | — | | `quality.autoRefresh` | No | `object` | — | Nested configuration block. | | `quality.autoRefresh.enabled` | No | `boolean` | — | — | | `quality.autoRefresh.miDropCap` | No | `number` | — | — | | `quality.autoRefresh.crapJumpCap` | No | `number` | — | — | | `quality.autoRefresh.scope` | No | `"diff"` \| `"full"` | — | — | | `quality.baselineEpsilon` | No | `object` | — | Per-kind epsilon for s-stability-epsilon (Story #1964). Sub-epsilon row deltas resolve to prior bytes so env variance does not rewrite the on-disk baseline. | | `quality.baselineEpsilon.maintainability` | No | `number` | — | — | | `quality.baselineEpsilon.crap` | No | `number` | — | — | | `quality.baselineEpsilon.coverage` | No | `number` | — | — | | `quality.baselineEpsilon.mutation` | No | `number` | — | — | | `quality.baselineEpsilon.lint` | No | `number` | — | — | | `quality.baselineEpsilon.lighthouse` | No | `number` | — | — | | `quality.baselineEpsilon.bundle-size` | No | `number` | — | — | | `quality.baselineEpsilon.duplication` | No | `number` | — | — | | `quality.navigability` | No | `object` | — | Navigability lens + post-wave integration gate config (Epic #4131, F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and the deliver-epic.md Phase 6.5 gate (journey suite). Opt-in: absent or empty routeGlobs degrades to a silent no-op. | | `quality.navigability.routeGlobs` | No | `array` | — | Glob patterns (pages/**, app/**/route.ts) marking paths that add a user-facing route — the route-tree SSOT the navigability lens enumerates and the route-added routing predicate matches against. | | `quality.navigability.navRegistry` | No | `array` | — | Tokens identifying the nav-registry SSOT the navigability lens checks every route resolves a nav door against. | | `quality.navigability.journeySuite` | No | `string` | — | Path or command for the per-persona journey suite the deliver-epic.md Phase 6.5 post-wave integration gate runs. | | `lifecycle` | No | `object` | — | Knobs consumed by the lifecycle event bus (Epic #2172). `timeouts` is a per-event budget map (eventName → seconds) used by `TimeoutWatchdog`; missing entries fall back to in-listener defaults. `heartbeatWarnSeconds` is the no-progress threshold consumed by `HeartbeatMonitor`. Story #2227 lays down the keys; consumers land in later stories. | | `lifecycle.timeouts` | No | `object` | — | — | | `lifecycle.heartbeatWarnSeconds` | No | `integer` | — | — | | `mergeWatch` | No | `object` | — | Knobs consumed by the MergeWatcher lifecycle listener (Story #2896, Epic #2880). `intervalSeconds` is the poll cadence between `gh pr view --json mergeCommit,mergedAt` probes after epic.merge.armed; `maxBudgetSeconds` is the total wall-clock budget before the watcher surfaces `agent::blocked` with reason `budget-exceeded`. | | `mergeWatch.intervalSeconds` | No | `integer` | `30` | Seconds between MergeWatcher polls. Default 30. | | `mergeWatch.maxBudgetSeconds` | No | `integer` | `3600` | Total wall-clock budget (seconds) for the MergeWatcher poll loop. Default 3600 (60 minutes). | | `epicAudit` | No | `object` | — | Nested configuration block. | | `epicAudit.maxFixAttempts` | No | `integer` | — | Maximum auto-fix retry attempts per finding in /deliver Phase 4 (epic-audit). 0 disables auto-fix. Default 3. | | `epicAudit.maxFixScopeFiles` | No | `integer` | — | Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5. | | `codeReview` | No | `object` | — | Nested configuration block. | | `codeReview.provider` | No | `"native"` \| `"codex"` \| `"security-review"` | `"native"` | Legacy single-adapter selection. ReviewProvider that produces the Finding[] consumed by runCodeReview(). Story #2833 registered `native` (in-process maintainability/lint); Story #2830 added `codex` (invokes `/codex:review` plugin); Story #2871 added `security-review` (shells out to `claude --print /security-review`). When `providers` (chain shape) is set this field is ignored with a warning. Selecting an adapter whose probe fails hard-fails at factory construction unless declared `optional: true` in the chain. | | `codeReview.providers[]` | No | `array` | — | Multi-provider chain (Story #2871). When set and non-empty, takes precedence over the legacy `provider` field. The orchestrator iterates inline entries in declaration order and merges their Finding[] before posting one structured comment; manual-prompt entries (e.g. ultrareview) contribute a trailing 'Manual review suggestions' section. Each item has: name, scopes, optional, manualPrompt, when. | | `codeReview.providerConfig` | No | `object` | — | Optional escape hatch for adapter-specific configuration. No documented keys in Epic #2815; reserved so future adapters can be configured without another schema migration. | | `codeReview.maxFixAttempts` | No | `integer` | — | Maximum auto-fix retry attempts per finding in /deliver Phase 5 (code-review). 0 disables auto-fix. Default 3. | | `codeReview.maxFixScopeFiles` | No | `integer` | — | Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5. | | `retro` | No | `object` | — | Story #3042 (Epic #3019). Operator-tunable retro behaviour. Currently exposes `perfThresholds`, the gates the retro perf-signals classifier uses to decide which signals to surface in the `## Performance Signals` / `## Recommended Follow-Ons` retro sections. | | `retro.perfThresholds` | No | `object` | — | Gates for `classifyPerfSignals` (lib/orchestration/retro-perf-heuristics.js). Defaults are 0.6 / 0.4 / 2. | | `retro.perfThresholds.utilisation` | No | `number` | — | Per-wave utilisation threshold. Waves whose `utilisation` is strictly below this value emit a `low-utilisation` signal. Default 0.6. | | `retro.perfThresholds.bootstrapShare` | No | `number` | — | Maximum acceptable share of cumulative Story execution time spent in the story-init phase. When exceeded the retro emits a `high-bootstrap-share` signal. Default 0.4. | | `retro.perfThresholds.capBindingRunLength` | No | `integer` | — | Minimum run length of consecutive cap-binding waves before the retro emits a `cap-binding-run` signal. Default 2. | | `refactorStage` | No | `object` | — | Opt-in, config-gated post-green refactor checkpoint wired into story-deliver (Story #3430, Epic #3418). Strictly additive and default-OFF: when disabled, story-deliver behaves exactly as before. Advisory only — never changes existing close-validation gate semantics. | | `refactorStage.enabled` | No | `boolean` | `false` | When true, story-deliver runs an advisory post-green refactor stage (refactorer persona + core/refactoring-discipline skill) after the suite is green. Default false — when unset the stage is skipped and close-validation gate semantics are unchanged. | | `acceptanceEval` | No | `object` | — | Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the working diff against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds, then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the loop is a hard cutover (always on). | | `acceptanceEval.maxRounds` | No | `integer` | — | Maximum number of redraft rounds before escalation. Default 2; clamped into [1, hard ceiling] by lib/config/acceptance-eval.js so the cap can never be disabled (maxRounds: 0 clamps up to 1). | | `ci` | No | `object` | — | Nested configuration block. | | `ci.skipForStoryPushes` | No | `boolean` | — | Story #2899 (Epic #2880, F13). When true (default), pre-push tooling appends a '[skip ci]' trailer to Story-branch commit subjects so intermediate pushes do not stampede the CI fleet. The Epic-branch merge commit produced by story-close.js never carries the marker, regardless of this flag. | | `preflight` | No | `object` | — | Story #2899 (Epic #2880, F13). Thresholds consumed by `.agents/scripts/epic-deliver-preflight.js`. When any value is exceeded the preflight envelope flags a breach and /deliver Phase 1 surfaces it via agent::blocked. | | `preflight.maxStories` | No | `integer` | — | — | | `preflight.maxWaves` | No | `integer` | — | — | | `preflight.maxInstallCostSeconds` | No | `integer` | — | — | | `preflight.maxGithubApiRequests` | No | `integer` | — | — | | `preflight.maxClaudeQuotaTokens` | No | `integer` | — | — | | `failOnConcurrencyHazards` | No | `boolean` | — | — | | `feedbackLoop` | No | `object` | — | Nested configuration block. | | `feedbackLoop.codeReviewAutoFile` | No | `boolean` | `true` | When true (default), the Epic finalize listener auto-files non-blocking code-review findings as follow-up issues routed by source classification. Set to false to suppress auto-filing; findings remain accessible in the structured comments on the Epic. | | `feedbackLoop.auditResultsAutoFile` | No | `boolean` | `true` | When true (default), the Epic finalize listener auto-files non-blocking audit-results findings as follow-up issues routed by source classification. Set to false to suppress auto-filing; findings remain accessible in the structured comments on the Epic. | ## `project` (required) Project-local execution behaviour. Only `paths` is required; everything else falls back to documented defaults. ### `project.paths` (required) Filesystem roots the framework reads from. All three keys are required — the resolver no longer applies code-level fallbacks; a missing value is a validation error with a clear `instancePath`. | Field | Required | Purpose | | ----------- | -------- | ------------------------------------------------ | | `agentRoot` | Yes | Path to the materialized framework payload (e.g. `.agents`). | | `docsRoot` | Yes | Path to project documentation (e.g. `docs`). | | `tempRoot` | Yes | Path for ephemeral artefacts (e.g. `temp`). | `auditOutputDir` is derived (not configurable) — it resolves to `${tempRoot}/audits` and is the canonical destination for every `audit-*` workflow's result reports **and** the audit-suite's prompt artifacts. Override `tempRoot` to relocate audit output. ### `project.baseBranch` | Field | Required | Default | Purpose | | ------------ | -------- | ------- | ------------------------------------------------------------------------- | | `baseBranch` | No | (none) | Default branch name (e.g. `main`). Read by close, push, and rebase paths. | ### `project.commands` Executable strings the framework spawns for validation, testing, and baseline ratchets. Strings must be non-empty and pass the shell-injection guard (`safeString` — disallows `;`, `&`, `|`, backtick, `$`, `<`, `>`). `typecheck` is nullable to indicate "not applicable for this repo"; `null` is the canonical disabled value. | Field | Required | Default | Type | Purpose | | -------------- | -------- | ----------------------------- | --------------- | ------------------------------------------------------ | | `lintBaseline` | No | (none) | `string` | Structured-output linter for the lint ratchet. | | `test` | No | (none) | `string` | Project test runner. | | `typecheck` | No | `null` | `string \| null` | Strict type-checking. `null` = disabled. | | `formatCheck` | No | `npx biome format .` | `string` | Read-only format check used by close-validation. | | `formatWrite` | No | `npx biome format --write .` | `string` | Auto-format invocation used by `runFormatAutofix`. | Read with `getCommands(config)` — see [`config-resolver.js`](../scripts/lib/config-resolver.js). ### `project.docsContextFiles` | Field | Required | Default | Purpose | | ------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------- | | `docsContextFiles` | No | `[]` | Files the context-hydration engine includes when assembling agent prompts. Resolved against `paths.docsRoot`. | > **Entries are plain filenames, not globs.** Each entry is resolved as a > single file under `paths.docsRoot`; the loader does **not** expand glob > patterns. This matters for the decisions log: when a project adopts the > index + `decisions/` ADR-directory layout (see > [`documentation-and-adrs`](../skills/core/documentation-and-adrs/SKILL.md)), > the **index** `decisions.md` stays the mandatory-read and the per-ADR bodies > under `decisions/` are link-followed on demand — **index-only by default**. > Auto-loading every ADR body into each task's context would reintroduce the > bloat the directory split exists to remove. A project that genuinely wants > the full ADR set in mandatory context must opt in by listing the individual > ADR files explicitly (one filename per entry); there is no built-in > `decisions/*.md` glob. --- ## `github` Ticketing provider configuration. Required when any GitHub-aware workflow runs (which is the common case). ### `github` — top-level | Field | Required | Purpose | | ---------------- | -------- | ------------------------------------------------------------------ | | `owner` | Yes | GitHub repository owner (user or org). | | `repo` | Yes | GitHub repository name. | | `projectNumber` | No | GitHub Projects V2 number for custom field writes. | | `projectOwner` | No | Project board owner (defaults to `owner`). | | `operatorHandle` | No | `@`-prefixed handle used in operator @mentions. | ### `github.branchProtection` Drives the `node .agents/scripts/bootstrap.js` flow that creates or merges branch protection on the base branch. | Field | Required | Default | Purpose | | ---------------- | -------- | ------- | ----------------------------------------------------------------------- | | `enforce` | No | `true` | When `true`, `node .agents/scripts/bootstrap.js` calls `applyBranchProtection(...)`. | | `requiredChecks` | No | `[]` | Array of `{ name, cmd[] }` entries used both as required-status-check expectations on the PR and as local close-validation gate invocations. | Each `requiredChecks` entry takes the shape: ```jsonc { "name": "lint", "cmd": ["npm", "run", "lint"] } ``` ### `github.mergeMethods` Repository-level merge-method allowlist applied by bootstrap. | Field | Required | Default | Purpose | | ------------------------ | -------- | ------- | ------------------------------------------------ | | `allow_squash_merge` | No | `true` | Permit squash merges through the UI / `gh`. | | `allow_rebase_merge` | No | `false` | Permit rebase merges. | | `allow_merge_commit` | No | `false` | Permit merge commits. | | `allow_auto_merge` | No | `true` | Enable auto-merge for the repository. | | `delete_branch_on_merge` | No | `true` | Delete the source branch after merge. | ### `github.notifications` | Field | Required | Default | Purpose | | ----------------- | -------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `mentionOperator` | No | `false` | When `true`, friction comments @-mention `operatorHandle` for `medium`-severity dispatches (high always @mentions). | | `commentEvents` | No | `["state-transition", "story-merged", "operator-message"]` | Allowlist of event names that reach the GitHub ticket comment channel. | | `webhookEvents` | No | `["epic-started", "epic-progress", "epic-blocked", "epic-unblocked", "epic-complete"]` | Allowlist of event names that reach `NOTIFICATION_WEBHOOK_URL`. The webhook channel is curated for the epic narrative; story-level events are excluded. | Both fields' enums are pinned in the schema and rejected if extended. To suppress a channel entirely, set its array to `[]`. > **Severity assignment.** Task transitions and `story-run-progress` upserts > fire `low` (frequency-driven). Story state transitions, `wave-run-progress`, > `epic-run-progress`, and epic-completion fire `medium`. Epic blockers and > HITL gates fire `high` (webhook prefix `[Action Required]` when an > allowlisted blocker event reaches the webhook). --- ## `planning` `/plan` tuning. All fields optional. | Field | Required | Default | Purpose | | ------------------------------ | -------- | ---------- | ------------------------------------------------------------------------------------------------ | | `riskHeuristics` | No | `[]` | Free-form rubric for `risk::high` decisions (informational only — `risk::high` does not gate runtime). Accepts a plain array or the `{ append/prepend }` extender form. | | `failOnSharedEditors` | No | (none) | Hard-fail Phase 7 when two Stories declare the same editor. | | `requireExplicitCrossStoryDeps`| No | (none) | Require explicit cross-Story `blocked by` declarations rather than inferring from shared paths. | ### `planning.context` Caps the size of `--emit-context` JSON payloads emitted during `/plan` so a runaway PRD / Tech Spec can't blow the planning agent's context budget. | Field | Required | Default | Purpose | | ------------- | -------- | -------- | --------------------------------------------------------------------------------------------- | | `maxBytes` | No | `50000` | Hard ceiling on the JSON payload size (bytes). Truncation is summary-mode-aware. | | `summaryMode` | No | `'auto'` | `'auto'` truncates intelligently; `'never'` errors over the cap; `'always'` always summarizes. | ### `planning.codebaseSnapshot` Controls the Phase 7 codebase-snapshot fetcher that grounds story decomposition. | Field | Required | Default | Purpose | | -------------------- | -------- | ----------- | ------------------------------------------------------------------------ | | `tier` | No | `'skinny'` | One of `'skinny'` or `'medium'`. `medium` opts into a richer snapshot. | | `include` | No | (see below) | Glob patterns whose matches are included in the snapshot. | | `exclude` | No | (see below) | Glob patterns whose matches are excluded from the snapshot. | | `recentCommitWindow` | No | (none) | Number of recent commits to summarise in the snapshot header. | The shipped `include` default scans `.agents/scripts/**`, `src/**`, `lib/**`, `app/**`, and `packages/**`; the shipped `exclude` default drops `node_modules`, `dist`, `build`, and `coverage`. Override only when the project's source layout differs. The `skinny` tier caps its file list at 250 paths. When the include set matches more than that, the cap is applied with **per-top-level-directory proportional budgeting** (round-robin across the matched top-level trees) rather than a flat lexicographic slice — so a large, dot-prefixed tree like `.agents/scripts/**` can no longer monopolise the budget and truncate away the consumer's own `src/` / `lib/` source. When `.agents/scripts/**` is the only matching tree (the Mandrel-repo dogfood case), the round-robin degenerates to taking the first 250 sorted paths, so that snapshot stays useful. When truncation occurs, `/plan` Phase 7 emits an operator-visible warning naming the dropped file count and suggesting `tier: "medium"` and/or a narrowed `include`. Opt into the richer `medium` tier or narrow `include` if the partial skinny view is insufficient. --- ## `delivery` `/deliver` and `/deliver` tuning. All sub-blocks are optional and fall back to documented defaults (or are no-ops when omitted). ### `delivery.execution` | Field | Required | Default | Purpose | | ----------- | -------- | ------- | ------------------------------------------------------------------ | | `timeoutMs` | No | (none) | Per-spawn timeout (ms) for child processes the framework launches. | ### `delivery.maxTokenBudget` | Field | Required | Default | Purpose | | ----------------- | -------- | ------- | ------------------------------------------------------------------ | | `maxTokenBudget` | No | (none) | Hydrated prompt cap for `hydrate-context` (character-based estimate + section elision). | ### `delivery.docsFreshness` | Field | Required | Default | Purpose | | ------- | -------- | ------- | ------------------------------------------------------------- | | `paths` | No | `[]` | Files refreshed during the post-PR-merge release tagging step. | ### `delivery.deliverRunner` | Field | Required | Default | Purpose | | --------------------------- | -------- | ------- | ------------------------------------------------ | | `concurrencyCap` | No | `3` | Max parallel Story sub-agents per wave. | | `progressReportIntervalSec` | No | `120` | Progress-report cadence (seconds). | ### `delivery.worktreeIsolation` Story-level worktree isolation. When `enabled: true`, `/deliver` runs each Story inside `.worktrees/story-/` instead of moving the main checkout's HEAD. | Field | Required | Default | Purpose | | ----------------------- | --------------- | ---------------- | ----------------------------------------------------------- | | `enabled` | No | `false` | Master switch. | | `root` | Conditional | `.worktrees` | Required when `enabled: true`. Worktree parent directory. | | `nodeModulesStrategy` | No | `clone` (darwin/linux); `per-worktree` (Windows) | One of `per-worktree`, `clone`, `symlink`, `pnpm-store`. `clone` copy-on-write (reflink/clonefile) clones the donor's `node_modules` and skips the per-tree install on a byte-exact lockfile match, falling back to `per-worktree` on any failure. | | `primeFromPath` | No | `null` | Optional source path used to prime `node_modules`. | | `allowSymlinkOnWindows` | No | `false` | Permit symlink strategy on Windows (requires admin/dev mode). | | `reapOnSuccess` | No | `true` | Reap the worktree after a successful Story close. | | `reapOnCancel` | No | `true` | Reap the worktree if the Story is cancelled. | | `bootstrapFiles` | No | `[".env", ".mcp.json"]` | Untracked files copied into each new worktree. | ### `delivery.signals` Friction-detector thresholds consumed by progress-signal listeners. | Field | Required | Default | Purpose | | ---------------------- | -------- | ------- | ------------------------------------------------------------------------ | | `hotspot.p95Multiplier`| No | (none) | Hot-file threshold expressed as a multiplier of the p95 edit frequency. | | `rework.editsPerFile` | No | (none) | Edits to the same file before a rework signal fires. | | `retry.repeatCount` | No | (none) | Identical retried commands before a retry signal fires. | ### `delivery.quality` Quality-gate configuration. Lives under `delivery.quality` and uses the uniform `gates.` shape (seven gates: `lint`, `coverage`, `crap`, `maintainability`, `mutation`, `lighthouse`, `bundle-size`) introduced by Epic #1720 Story #1737. #### `delivery.quality.gateScoping` | Field | Required | Default | Purpose | | --------- | -------- | -------- | ------------------------------------------------------------------------------ | | `scope` | No | `'full'` | One of `'diff'` (only files changed vs `diffRef`) or `'full'` (all targetDirs). | | `diffRef` | No | (none) | Ref used to compute the diff when `scope: 'diff'` (e.g. `main`). | #### `delivery.quality.gates.` — common shape Every gate shares the same envelope: | Field | Required | Purpose | | -------------- | -------- | -------------------------------------------------------------------------------------------------------------- | | `enabled` | No | Master switch. `false` makes all three gate sites self-skip for this tier. | | `baselinePath` | No | Path to the per-tier ratchet baseline file (e.g. `baselines/crap.json`). | | `tolerance` | No | `{ kind: 'absolute' \| 'percent', value: }` — slack permitted when comparing scores against baseline. | | `floors` | No | `{ : { : } }` — absolute floors below which the gate fails regardless of baseline. | | `components` | No | `{ : [, ...] }` — optional component-level grouping for breakdown reporting. | Tier-specific knobs: ##### `gates.lint` No tier-specific knobs beyond the common shape. ##### `gates.coverage` | Field | Required | Default | Purpose | | -------------- | -------- | -------------------------------- | -------------------------------------------------------- | | `coveragePath` | No | `coverage/coverage-final.json` | Per-method coverage artifact consumed by the gate. | | `timeoutMs` | No | `600000` | Wall clock (ms) for `npm run test:coverage` spawned by `coverage-capture.js`. SIGKILL → exit 124 (GNU `timeout` convention) so close-validation can branch on hang-vs-failure. | > **Canonical accessor.** Internal callers MUST read this block via > `getQuality(config)` where `config` is the full envelope returned by > `resolveConfig()` (or any object that exposes `delivery.quality.*`). > Passing a sub-pick such as `getQuality({ agentSettings })` silently > resolves to framework defaults — `agentSettings` is not part of the > post-Epic-#2880 resolver output and `getQuality` reads only > `config?.delivery?.quality`. The same applies to `getBaselines(config)`. ##### `gates.crap` | Field | Required | Default | Purpose | | ------------------- | ----------- | --------------------------- | ---------------------------------------------------------------- | | `targetDirs` | No | `["src"]` | Source dirs to score. Accepts list or `{ append/prepend }` form. | | `newMethodCeiling` | No | `30` | Max CRAP score allowed for methods absent from the baseline. | | `requireCoverage` | No | `true` | When `true`, methods without coverage are skipped (not failed). | | `friction.markerKey`| No | `crap-baseline-regression` | Friction-log marker for regressions. | | `refreshTag` | No | `baseline-refresh:` | Subject prefix the refresh-guardrail expects on baseline-only commits. | | `refreshTimeoutMs` | No | `60000` | Bounded timeout (ms) for `npm run crap:update` in the baseline-attribution refresh path. SIGKILL → exit 124 → Story `agent::blocked`. | ##### `gates.maintainability` | Field | Required | Default | Purpose | | ------------------ | -------- | ----------- | ---------------------------------------------------------------- | | `targetDirs` | No | `["src"]` | Source dirs to score. Accepts list or `{ append/prepend }` form. | | `refreshTimeoutMs` | No | `60000` | Bounded timeout (ms) for `npm run maintainability:update` in the baseline-attribution refresh path. | ##### `gates.mutation` | Field | Required | Default | Purpose | | -------------------- | -------- | ------- | -------------------------------------------------------- | | `strykerConfigPath` | No | `null` | Path to the Stryker config the mutation gate consumes. | ##### `gates.lighthouse` | Field | Required | Default | Purpose | | --------- | -------- | ------- | ---------------------------------------------------------------------------------------- | | `baseUrl` | No | `null` | Base URL Lighthouse audits. | | `routes` | No | `[]` | Array of `{ path, formFactor? }` entries, where `formFactor` is `'mobile'` or `'desktop'`. | ##### `gates.bundle-size` | Field | Required | Default | Purpose | | --------- | -------- | ------- | ---------------------------------------------------------------------------------------- | | `bundles` | No | `[]` | Array of `{ name, path, limit }` entries (e.g. `{ "name": "app", "path": "dist/app.js", "limit": "100kB" }`). | #### `delivery.quality.formatAutofix` | Field | Required | Default | Purpose | | ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------ | | `timeoutMs` | No | (none) | Bounded timeout (ms) for the close-time `npx biome format --write` spawn. SIGKILL maps to exit 124 → Story `agent::blocked`. | #### `delivery.quality.codingGuardrails` | Field | Required | Default | Purpose | | ---------------------- | -------- | ------- | ------------------------------------------------------------------ | | `cyclomaticFlag` | No | (none) | Cyclomatic-complexity value at which the engineer should refactor. | | `cyclomaticMustFix` | No | (none) | Cyclomatic-complexity value that hard-fails the gate. | | `miDropMustRefactor` | No | (none) | MI drop that mandates a refactor. | | `requireSiblingTest` | No | (none) | When `true`, a new function requires a sibling test file. | #### `delivery.quality.autoRefresh` Controls Story-close auto-baseline-refresh for gated metrics. | Field | Required | Default | Purpose | | ------------- | -------- | ------- | -------------------------------------------------------- | | `enabled` | No | (none) | Master switch. | | `miDropCap` | No | (none) | Max MI drop the auto-refresher will absorb before failing. | | `crapJumpCap` | No | (none) | Max CRAP jump the auto-refresher will absorb before failing. | | `scope` | No | (none) | One of `'diff'` or `'full'`. | #### `delivery.quality.baselineEpsilon` Per-kind epsilon (introduced by Story #1964). Sub-epsilon row deltas resolve to prior bytes so cross-environment variance does not rewrite the on-disk baseline. | Field | Required | Default | Purpose | | ----------------- | -------- | ------- | -------------------------------------- | | `maintainability` | No | (none) | Epsilon for maintainability rows. | | `crap` | No | (none) | Epsilon for CRAP rows. | | `coverage` | No | (none) | Epsilon for coverage rows. | | `mutation` | No | (none) | Epsilon for mutation rows. | | `lint` | No | (none) | Epsilon for lint rows. | | `lighthouse` | No | (none) | Epsilon for Lighthouse rows. | | `bundle-size` | No | (none) | Epsilon for bundle-size rows. | ### `delivery.lifecycle` Knobs consumed by the lifecycle event bus (Epic #2172). `timeouts` is a per-event budget map (eventName → seconds) used by `TimeoutWatchdog`; missing entries fall back to in-listener defaults. | Field | Required | Default | Purpose | | ---------------------- | -------- | ------- | ------------------------------------------------------ | | `timeouts` | No | `{}` | `{ : }` watchdog budgets. | | `heartbeatWarnSeconds` | No | (none) | No-progress threshold consumed by `HeartbeatMonitor`. | ### `delivery.epicAudit` `/deliver` Phase 4 (epic-audit) auto-fix budget. | Field | Required | Default | Purpose | | ------------------ | -------- | ------- | -------------------------------------------------------------------- | | `maxFixAttempts` | No | `3` | Max auto-fix retry attempts per finding. `0` disables auto-fix. | | `maxFixScopeFiles` | No | `5` | Max file count a single auto-fix may modify before escalating to `agent::blocked`. | ### `delivery.codeReview` Configuration block for the code-review pipeline that runs at **both** Story-close (`story-close.js`) and Epic-close (`/deliver` Phase 5). Selects the review backend, exposes an escape-hatch for adapter-specific configuration, and sets the auto-fix budget enforced at each close scope. | Field | Required | Default | Purpose | | ------------------ | -------- | ---------- | -------------------------------------------------------------------- | | `provider` | No | `"native"` | ReviewProvider adapter that produces the `Finding[]` consumed by `runCodeReview()`. See enum values below. | | `providerConfig` | No | `{}` | Optional escape hatch for adapter-specific configuration. Reserved so future adapters can be configured without another schema migration; no documented keys in Epic #2815. | | `maxFixAttempts` | No | `3` | Max auto-fix retry attempts per finding. `0` disables auto-fix. Applied at **both** Story-close and Epic-close. | | `maxFixScopeFiles` | No | `5` | Max file count a single auto-fix may modify before escalating to `agent::blocked`. Applied at **both** Story-close and Epic-close. | #### `provider` enum | Value | Behaviour | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `native` | Default. In-process maintainability/lint pass that runs without any external plugin. Always available. | | `codex` | Invokes the `/codex:review` Claude Code plugin via the [`codex-plugin-cc`](https://github.com/openai/codex-plugin-cc) backend. Selecting `codex` when the plugin is not installed hard-fails at factory construction with remediation guidance — there is no silent fallback to `native`. | #### Dual-scope fix budget `maxFixAttempts` and `maxFixScopeFiles` are enforced at two distinct points in the SDLC, using the **same configured values** for both scopes: - **Story-close** — `story-close.js` runs `runCodeReview()` against the Story branch's diff and applies the budget per finding before merging into `epic/`. - **Epic-close** — `/deliver` Phase 5 runs `runCodeReview()` against the integrated Epic branch and applies the same per-finding budget before opening the PR to `main`. Setting `maxFixAttempts: 0` disables auto-fix at both scopes; there is no per-scope override. ### `delivery.failOnConcurrencyHazards` | Field | Required | Default | Purpose | | --------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- | | `failOnConcurrencyHazards` | No | (none) | When `true`, hard-fail the plan if Phase 7 detects shared-editor or unsequenced cross-Story deps. | ### `delivery.feedbackLoop` | Field | Required | Default | Purpose | | ----------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `codeReviewAutoFile` | No | `true` | When `true`, the Epic finalize listener auto-files non-blocking code-review findings as follow-up issues routed by source classification. | | `auditResultsAutoFile` | No | `true` | When `true`, the Epic finalize listener auto-files non-blocking audit-results findings as follow-up issues routed by source classification. | --- ## Root dogfood vs distributed templates Three `.agentrc`-shaped files live in this repository and serve different audiences. They share the same schema but legitimately disagree on a small number of keys. | File | Audience | Role | | --------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `.agentrc.json` (repo root) | The framework dogfooding itself | Live config used when running `/epic-*` and `/deliver` workflows against this repo. Exercises the framework end-to-end on its own source tree. | | `.agents/starter-agentrc.json` | Downstream consumer repos | Bootstrap delta-seed a consumer copies via `cp .agents/starter-agentrc.json .agentrc.json`. Minimum schema-required keys only. | | `.agents/docs/agentrc-reference.json` | Operators and reviewers | Exhaustive editor reference enumerating every schema key with its framework default. Not a copy target. | | Key | Root dogfood | Distributed template (`agentrc-reference.json`) | Why they differ | | ---------------------------------------------------- | ------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `project.commands.lintBaseline` | `npm run lint` | `npx eslint . --format json` | Root piggybacks on the repo's existing lint script; consumer template assumes a generic ESLint setup with structured output. | | `delivery.quality.gates.maintainability.targetDirs` | `[".agents/scripts", "tests"]` | `["src"]` | Root scans the framework's own source tree; consumer template scans the conventional `src/`. | | `delivery.quality.gates.crap.targetDirs` | `[".agents/scripts"]` | `["src"]` | Same reason as maintainability above. | | `github.owner` / `.repo` / `.projectNumber` | Populated for `dsj1984/mandrel` | `[OWNER]` / `[REPO]` / `null` | Shared repo identifiers; placeholders in the template are replaced by `node .agents/scripts/bootstrap.js` (or by hand). | | `github.operatorHandle` | Committed as the `@[USERNAME]` placeholder; each contributor overrides it in gitignored `.agentrc.local.json` | `@[USERNAME]` | Schema-required, but per-contributor: the committed placeholder resolves to null and the lease guards fail closed until you set your own handle locally (see [Per-machine local overrides](#per-machine-local-overrides)). | | `delivery.worktreeIsolation.nodeModulesStrategy` | `per-worktree` | `per-worktree` | npm-only repo (`package-lock.json`); worktree init runs `npm ci` per tree. | When a consumer runs `/mandrel-update`, the [`mandrel-sync-config`](../workflows/helpers/mandrel-sync-config.md) helper validates the project config against the schema, then adds any template-introduced keys the project does not already define. Project-side values that validate are preserved unconditionally — including optional keys the template does not declare. > **Editing rule of thumb:** edit `.agents/docs/agentrc-reference.json` when a > framework default changes; edit `.agents/starter-agentrc.json` only when > the bootstrap seed itself needs new schema-required keys; edit > `.agentrc.json` for changes that only affect this repo's own dogfood > runs. --- ## Baseline conventions The framework writes two distinct kinds of baseline file. They are intentionally separated so a repo-wide grep never confuses one with the other. ### Canonical ratchet baselines — `/baselines/` Committed, schema-pointed baselines that gate every PR via close-validation, the lint ratchet, and the CRAP/MI gates. | File | Owner | Refresh | | --------------------------------- | ------------------------------------ | ---------------------------------------------------------------------- | | `baselines/lint.json` | `lint-baseline.js` | `node .agents/scripts/lint-baseline.js capture` | | `baselines/crap.json` | `update-crap-baseline.js` | `npm run crap:update` | | `baselines/maintainability.json` | `update-maintainability-baseline.js` | `npm run maintainability:update` | These files are the contract. They are read by every gate (Story close, push hook, CI) and are regenerated only via tagged `baseline-refresh:` commits with a non-empty body. The convention is operator-enforced; see the CRAP section of [`quality-gates.md`](quality-gates.md) for the policy. Paths are configured in `delivery.quality.gates..baselinePath`. The default values match the canonical layout above; override only when a project genuinely stores baselines elsewhere. ### Per-wave drift snapshots — `.agents/state/` The Epic runner's progress reporter writes wave-start snapshots so that a resumed run can detect intra-wave drift without re-reading the canonical baseline (which may have been refreshed mid-Epic). | File | Owner | Lifecycle | | --------------------------------------- | ---------------------------------------------------- | ----------------------------------------------- | | `.agents/state/wave-mi-snapshot.json` | `progress-signals/maintainability-drift.js` | Captured at wave-start; overwritten next wave. | | `.agents/state/wave-crap-snapshot.json` | `progress-signals/crap-drift.js` | Captured at wave-start; overwritten next wave. | These are **not** ratchet baselines and must not be committed as such. The filenames intentionally differ from the canonical files so a repo-wide grep for `baselines/maintainability.json` or `baselines/crap.json` only ever hits the canonical paths. The `.agents/state/` directory itself is created on demand by the progress reporter; the framework does not require it to exist ahead of time and does not commit its contents. --- ## How to extend ### Adding a project-specific optional key The schema-driven sync helper preserves every project-side key that validates, including optional keys absent from the distributed template. To add a project-specific knob: 1. Confirm the key is **already declared in the schema** at [`.agents/schemas/agentrc.schema.json`](../schemas/agentrc.schema.json) — if it isn't, the AJV validators will reject it on the next `/mandrel-update`. 2. Set the key in `.agentrc.json`. Don't add it to the template unless it should ship to all consumers. 3. Run `/mandrel-update` to confirm the helper preserves the key on round-trip. ### Extending list-valued keys without losing template defaults `delivery.quality.gates.maintainability.targetDirs`, `delivery.quality.gates.crap.targetDirs`, and `planning.riskHeuristics` accept the deep-merge extender form: ```jsonc { "delivery": { "quality": { "gates": { "crap": { "targetDirs": { "append": ["packages/foo/src", "packages/bar/src"] } } } } } } ``` `{ "append": [...] }` and `{ "prepend": [...] }` extend the resolver's fallback default. A plain array (`["packages/foo/src"]`) replaces the default entirely — useful when the consumer wants exactly its own dirs. ### Per-machine local overrides `.agentrc.local.json` (gitignored) is layered on top of `.agentrc.json` by the resolver. Precedence (highest wins): local → committed `.agentrc.json` → built-in defaults. Object keys **deep-merge** so a local file can override one nested field without restating sibling keys; absent local file is a no-op. Use it for machine-specific tuning (e.g. lower `delivery.deliverRunner.concurrencyCap` on a laptop) that should never reach git. **Per-contributor identity (required).** `github.operatorHandle` is a personal, per-contributor value with two jobs: the `@`-handle the framework @mentions on friction comments, **and** the lease owner the workflow guards ([`ticket-lease.js`](../scripts/lib/orchestration/ticket-lease.js)) assign to a ticket so two contributors cannot drive the same Epic/Story concurrently. Because the lease must distinguish *your* run from *another person's*, a shared committed handle would defeat it — everyone would coordinate under one identity. So each contributor sets their own in `.agentrc.local.json`: ```json { "github": { "operatorHandle": "@your-handle" } } ``` `operatorHandle` is **required** by the schema, but the committed `.agentrc.json` carries only the non-personal placeholder `@[USERNAME]` (so CI and fresh clones validate without naming a real person). The placeholder is **not** a usable identity: [`normalizeOperatorHandle`](../scripts/lib/orchestration/ticket-lease.js) resolves `@[USERNAME]` to `null`, and the lease guards (`/plan`, `/deliver`, `/deliver`) **fail closed** — they throw with a "set your own handle in `.agentrc.local.json`" message rather than running an ownerless, unguarded workflow. Your local overlay replaces the placeholder with your real handle, and the guards proceed. By contrast, `github.owner` / `repo` are **shared** repo identifiers (everyone targets the same `dsj1984/mandrel`) and stay committed with real values. ### Adding a new top-level key (framework change) This is a framework-level change, not a project-level one. The path is: 1. Add the AJV schema in the relevant module under `.agents/scripts/lib/`. 2. Mirror it manually in `.agents/schemas/agentrc.schema.json`. 3. Add a resolver getter in [`config-resolver.js`](../scripts/lib/config-resolver.js). 4. Add tests under `tests/lib/config-*.test.js` and confirm `tests/config-schema-mirror-drift.test.js` passes. 5. Document the key in this file and update `mandrel-sync-config.md` only if the merge semantics differ from the default (project-wins) rule. --- ## Secrets now live in `.env` As of Epic #702 the framework no longer ships an MCP server, so `.mcp.json` is **not** a valid home for framework secrets. Every environment variable the orchestration engine reads is sourced from the process environment only — loaded from `.env` locally, or set in the Claude Code web environment-variables UI for web sessions. ### Keys the framework reads | Variable | Required? | Purpose | | -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GITHUB_TOKEN` | Yes\* | GitHub API auth for all ticketing operations. `GH_TOKEN` is accepted as a synonym. `gh auth token` is a fallback for local sessions. | | `NOTIFICATION_WEBHOOK_URL` | No | POST target for in-band Notifier events (Make.com / Slack / Discord). Unset disables the webhook channel; `log` and `epic-comment` channels still fire. | | `WEBHOOK_SECRET` | No | Shared secret used to sign outbound webhook payloads as `X-Signature-256: sha256=`. Unset ships unsigned payloads. | | `MANDREL_ALLOW_TEST_WEBHOOKS` | No | Set to `1` to keep `NOTIFICATION_WEBHOOK_URL` live inside `npm test` / `npm run test:profile`. Default behaviour scrubs the env var from the test child so no URL resolves and the webhook never fires (see below). | ### Test-mode webhook isolation `npm test` / `npm run test:profile` deliberately strip `NOTIFICATION_WEBHOOK_URL` from the spawned test child's environment (and set `NODE_ENV=test` for the rest of the suite's environment expectations). With the URL scrubbed, `resolveWebhookUrl()` returns nothing and `notify()` never POSTs — that scrub is the primary defense. `notify()` no longer carries a `NODE_ENV=test` band-aid (removed in Story \#3342: it made the signing and error branches untestable). Tests that need to exercise the webhook POST inject a fake fetch via `opts.fetchImpl` instead, so the request is captured in-process and never reaches the real network even if a URL resolves. Operators who keep a real webhook URL in `.env` for development do not need to change anything; the scrub is transparent. To deliberately fire webhooks from a contract test against a sandbox endpoint, set `MANDREL_ALLOW_TEST_WEBHOOKS=1` for that run. \* `GITHUB_TOKEN` / `GH_TOKEN` is required for background scripts and CI; a locally-authenticated `gh auth login` session is an acceptable substitute in interactive developer sessions only. ### Where to put them | Environment | Storage location | | ----------------- | -------------------------------------------------------------------------------------------------------------------- | | Local development | `.env` at the project root (auto-loaded by `config-resolver.js`). The file is `.gitignore`d; provision it per clone. | | Web sessions | Claude Code web UI → environment-variables panel. | | CI | Repo / org secret, surfaced into the runner via `ENV_FILE` or the workflow's `env:` block. | `.mcp.json` is reserved for your MCP host's own discovery of third-party servers (e.g. `@modelcontextprotocol/server-github`, `context7`) and is ignored by the orchestration engine. Any framework-specific keys still present in a `.mcp.json` from a pre-#702 checkout are **dead config** — move them to `.env` (local) or your web session's env-var UI (web). The webhook URL for external delivery is **not** configured in `.agentrc.json`. It is sourced from the `NOTIFICATION_WEBHOOK_URL` process env var only — set it in `.env` locally, in the Claude Code web environment-variables UI for web sessions, or as a repo secret via `ENV_FILE` for GitHub Actions runs. --- ## Permission allowlist maintenance (`/fewer-permission-prompts`) The harness-supplied `/fewer-permission-prompts` skill scans recent Claude Code transcripts, buckets repeated read-only `Bash(...)` and `mcp__*` tool calls by frequency, and proposes an additive allowlist patch for the project's `.claude/settings.json`. Running it on a schedule is the only way to keep the allowlist tracking the framework's actual surface area — without it, every new `.agents/scripts/.js` helper introduced by a framework bump triggers a fresh wave of permission prompts that operators answer by hand, and those hand-tuned allowlists drift project-to-project. ### Cadence Run `/fewer-permission-prompts` **once per `/mandrel-update` invocation**, immediately after the package upgrade re-materializes `.agents/` and before the bump commit lands. The cadence is codified in [`/mandrel-update` Step 3.6](../workflows/mandrel-update.md). The operator who just bumped `.agents/` has the freshest transcript context in the active session, which is exactly what the skill scans, so this is the cheapest time to surface new high-frequency calls. A bump that introduces no new scripts produces a "no new high-frequency calls" report from the skill — that is a valid outcome, not a reason to skip the step on the next bump. Silence-by-omission is the failure mode this cadence is meant to eliminate. ### Review discipline Treat the skill's output as a **PR-reviewable artifact**, not an auto-applied change. The skill never edits `.claude/settings.json` directly; it emits a proposed additive patch the operator approves entry-by-entry. **Accept** narrowly-scoped read-only entries: - `Bash(node .agents/scripts/.js *)` for helper scripts the framework just introduced. - `Bash(gh issue view *)`, `Bash(gh pr view *)`, and other read-only `gh` invocations that already appear in agent workflows. - `mcp__github__get_*`, `mcp__github__list_*`, `mcp__github__search_*` and similarly read-only MCP tool entries. **Reject** anything that grants: - Write permissions on the filesystem outside the worktree (`Bash(rm -rf *)`, `Bash(git push --force *)`, `mcp__github__delete_*`, `mcp__github__merge_pull_request`, etc.). - Network egress to non-framework endpoints. - Destructive shells (`Bash(gh release delete *)`, `mcp__github__push_files` against `main`). When in doubt, leave the entry off the patch — a missed allowlist addition costs one permission prompt on the next run; a wrongly-added destructive permission costs trust. The rejected entries surface again on the next cadence run if they remain high-frequency, so the cost of deferral is bounded. Stage the accepted `.claude/settings.json` diff alongside the `/mandrel-update` bump commit so the reviewer sees the framework pointer move and the allowlist response in the same diff. --- ## CLI subcommand quick-reference `mandrel --help` prints the full subcommand list. Each subcommand that mutates state supports `--dry-run` to preview without writing. The table below covers every dispatch-visible subcommand: | Subcommand | What it does | Key flags | | ---------- | ------------ | --------- | | `init` | Install and configure mandrel in the current project. | `--assume-yes`, `--skip-github`, `--dry-run` | | `sync` | Re-materialize `.agents/` from the installed package payload. | `--dry-run`, `--force` | | `sync-commands` | Rebuild `.claude/commands/` from `.agents/workflows/`. | — | | `doctor` | Run readiness checks and report remedies. | — | | `update` | Upgrade mandrel to the newest published version. | `--dry-run`, `--install-cmd` | | `migrate` | Apply version-keyed migrations for a version range. | `--from`, `--to`, `--dry-run` | | `explain` | Print resolved config values with sources. | `--json` | | `uninstall` | Reverse a recorded install using the install ledger. | `--include-github`, `--dry-run` | ### `mandrel explain` Prints every resolved config key — its effective value, its source layer (`[agentrc]` or `[default]`), and a one-line description. Secret-shaped values are shown as ``. Useful when debugging unexpected behavior caused by config layering. ```bash mandrel explain # human-readable report mandrel explain --json # JSON report for scripting ``` ### `mandrel sync-commands` Regenerates the flat `.claude/commands/` tree from `.agents/workflows/`. The bootstrap wires a `UserPromptSubmit` hook so this runs automatically on every Claude Code prompt; manual invocations are only needed when the hook is absent or the commands/ tree is manually deleted. ```bash mandrel sync-commands ``` ### `mandrel uninstall` Reverses a recorded install using the install ledger (`.agents/.install-manifest.json`). Restoration is marker-based and non-destructive: operator-authored content that pre-existed the install is preserved; only install-created files and framework additions are removed. GitHub-side state is never acted on automatically; it is surfaced as a manual checklist. ```bash mandrel uninstall # reverse all local mutations mandrel uninstall --dry-run # preview without writing mandrel uninstall --include-github # acknowledge GitHub-side follow-ups ``` --- ## Cross-references - JSON Schema mirror — [`.agents/schemas/agentrc.schema.json`](../schemas/agentrc.schema.json) - Resolver entry point — [`config-resolver.js`](../scripts/lib/config-resolver.js) - Sync helper — [`mandrel-sync-config.md`](../workflows/helpers/mandrel-sync-config.md) - Bootstrap script — [`bootstrap.js`](../scripts/bootstrap.js) - Quality gates runbook (CRAP onboarding, MI ratchet, lint ratchet) — [`quality-gates.md`](quality-gates.md) - Activation pointers (slash commands, personas, skills) — [`.agents/README.md`](../README.md)