# 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`](../.agents/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`](../.agents/schemas/agentrc.schema.json),
which is itself a mirror of the AJV schemas under
[`.agents/scripts/lib/`](../.agents/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/full-agentrc.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, maxTickets, 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-hydrator files. |
| `github`      | No       | Ticketing provider config: owner/repo, branch protection, merge methods, notifications. |
| `planning`    | No       | `/epic-plan` tuning: ticket budget, risk heuristics, codebase snapshot, context cap. |
| `delivery`    | No       | `/epic-deliver` and `/story-deliver` tuning: quality gates, worktree isolation, runners, lifecycle. |
| `$schema`     | No       | JSON Schema pointer for editor tooling.                                            |

---

<!-- BEGIN GENERATED:agentrc -->

> Generated by `node .agents/scripts/generate-config-docs.js` from
> [`.agents/schemas/agentrc.schema.json`](../.agents/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<string>` | — | — |
| `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` | No | `string` | — | — |
| `branchProtection` | No | `object` | — | Nested configuration block. |
| `branchProtection.enforce` | No | `boolean` | — | — |
| `branchProtection.requiredChecks[]` | No | `array<branchProtectionCheck>` | — | 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<enum>` | — | — |
| `notifications.webhookEvents` | No | `array<enum>` | — | — |

### `planning` (optional)

| Key | Required | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `riskHeuristics` | No | `string[]` or `{ append?, prepend? }` | — | — |
| `maxTickets` | No | `integer` | — | — |
| `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<string>` | — | — |
| `codebaseSnapshot.exclude` | No | `array<string>` | — | — |
| `codebaseSnapshot.recentCommitWindow` | No | `integer` | — | — |
| `failOnSharedEditors` | No | `boolean` | — | — |
| `requireExplicitCrossStoryDeps` | No | `boolean` | — | — |

### `delivery` (optional)

| Key | Required | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `execution` | No | `object` | — | Nested configuration block. |
| `execution.timeoutMs` | No | `integer` | — | — |
| `hydration` | No | `object` | — | Context hydrator output mode (Epic #2648). prose-legacy delegates to context-hydration-engine.legacy.js for one release; remove hydration.outputMode and the legacy module together in the cutover PR. |
| `hydration.outputMode` | No | `"envelope"` \| `"prose-legacy"` | `"envelope"` | envelope: typed ContextEnvelope pipeline (default). prose-legacy: frozen pre-Epic prose hydrator — one-release sunset. |
| `hydration.fullSkillBodies` | No | `boolean` | `false` | When true, embed full SKILL.md bodies; when false, prefer policy capsules from skills.index.json. |
| `maxTokenBudget` | No | `integer` | — | — |
| `docsFreshness` | No | `object` | — | Nested configuration block. |
| `docsFreshness.paths` | No | `array` | — | — |
| `deliverRunner` | No | `object` | — | Nested configuration block. |
| `deliverRunner.concurrencyCap` | No | `integer` | — | — |
| `deliverRunner.progressReportIntervalSec` | No | `integer` | — | — |
| `worktreeIsolation` | No | `object` | — | Nested configuration block. |
| `worktreeIsolation.enabled` | No | `boolean` | — | — |
| `worktreeIsolation.root` | No | `string` | — | — |
| `worktreeIsolation.nodeModulesStrategy` | No | `"per-worktree"` \| `"symlink"` \| `"pnpm-store"` | — | — |
| `worktreeIsolation.primeFromPath` | No | `string` \| `null` | — | — |
| `worktreeIsolation.allowSymlinkOnWindows` | No | `boolean` | — | — |
| `worktreeIsolation.reapOnSuccess` | No | `boolean` | — | — |
| `worktreeIsolation.reapOnCancel` | No | `boolean` | — | — |
| `worktreeIsolation.bootstrapFiles` | No | `array<string>` | — | — |
| `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<map>` | — | — |
| `quality.gates.lint.components` | No | `object<map>` | — | — |
| `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<map>` | — | — |
| `quality.gates.coverage.components` | No | `object<map>` | — | — |
| `quality.gates.coverage.coveragePath` | No | `string` | — | — |
| `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<map>` | — | — |
| `quality.gates.crap.components` | No | `object<map>` | — | — |
| `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.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<map>` | — | — |
| `quality.gates.maintainability.components` | No | `object<map>` | — | — |
| `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.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<map>` | — | — |
| `quality.gates.mutation.components` | No | `object<map>` | — | — |
| `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<map>` | — | — |
| `quality.gates.lighthouse.components` | No | `object<map>` | — | — |
| `quality.gates.lighthouse.baseUrl` | No | `string` \| `null` | — | — |
| `quality.gates.lighthouse.routes[]` | No | `array<lighthouseRoute>` | — | 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<map>` | — | — |
| `quality.gates.bundle-size.components` | No | `object<map>` | — | — |
| `quality.gates.bundle-size.bundles[]` | No | `array<bundleDeclaration>` | — | Each item (`bundleDeclaration`) has: name, path, limit. |
| `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` | — | — |
| `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<map>` | — | — |
| `lifecycle.heartbeatWarnSeconds` | No | `integer` | — | — |
| `epicAudit` | No | `object` | — | Nested configuration block. |
| `epicAudit.maxFixAttempts` | No | `integer` | — | Maximum auto-fix retry attempts per finding in /epic-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.maxFixAttempts` | No | `integer` | — | Maximum auto-fix retry attempts per finding in /epic-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. |
| `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. |

<!-- END GENERATED:agentrc -->

## `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 framework submodule (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`](../.agents/scripts/lib/config-resolver.js).

### `project.docsContextFiles`

| Field              | Required | Default | Purpose                                                                                            |
| ------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------- |
| `docsContextFiles` | No       | `[]`    | Files the context-hydrator includes when assembling agent prompts. Resolved against `paths.docsRoot`. |

---

## `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 `ensureMainBranchProtection(...)`. |
| `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`

`/epic-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. |
| `maxTickets`                   | No       | (none)  | Soft cap on tickets a single Epic may decompose.                                                 |
| `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 `/epic-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.

---

## `delivery`

`/epic-deliver` and `/story-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)  | Soft cap on per-call token budget (planning hint).                 |

### `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`, `/story-deliver` runs
each Story inside `.worktrees/story-<id>/` 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              | `per-worktree`   | One of `per-worktree`, `symlink`, `pnpm-store`.              |
| `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              | `[]`             | Untracked files (e.g. `.env`) 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.<tier>` 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.<tier>` — 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: <number> }` — slack permitted when comparing scores against baseline. |
| `floors`       | No       | `{ <workspace>: { <metric>: <number> } }` — absolute floors below which the gate fails regardless of baseline. |
| `components`   | No       | `{ <component>: [<glob>, ...] }` — 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.       |

##### `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       | `{}`    | `{ <eventName>: <seconds> }` watchdog budgets.         |
| `heartbeatWarnSeconds` | No       | (none)  | No-progress threshold consumed by `HeartbeatMonitor`.  |

### `delivery.epicAudit`

`/epic-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`

`/epic-deliver` Phase 5 (code-review) auto-fix budget. Same shape as
`epicAudit`.

| 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.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 `/story-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/full-agentrc.json`       | Operators and reviewers             | Exhaustive editor reference enumerating every schema key with its framework default. Not a copy target.                             |

| Key                                                  | Root dogfood                          | Distributed template (`full-agentrc.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` / `.projectOwner` / `.operatorHandle` | Populated for `dsj1984/mandrel` | `[OWNER]` / `[REPO]` / `null` / `null` / `@[USERNAME]` | Repo-specific identifiers; placeholders in the template are replaced by `node .agents/scripts/bootstrap.js` (or by hand). |
| `delivery.worktreeIsolation.nodeModulesStrategy`     | `per-worktree`                        | `per-worktree`                             | npm-only repo (`package-lock.json`); worktree init runs `npm ci` per tree.                                                       |

When a consumer runs `/agents-update`, the
[`agents-sync-config`](../.agents/workflows/helpers/agents-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/full-agentrc.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 --refresh`                       |
| `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 [`docs/quality-gates.md`](quality-gates.md) for the policy.

Paths are configured in `delivery.quality.gates.<tier>.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`](../.agents/schemas/agentrc.schema.json)
   — if it isn't, the AJV validators will reject it on the next
   `/agents-update`.
2. Set the key in `.agentrc.json`. Don't add it to the template unless it
   should ship to all consumers.
3. Run `/agents-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. Use it for machine-specific tuning (e.g. lower
`delivery.deliverRunner.concurrencyCap` on a laptop) that should never reach
git.

### 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`](../.agents/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 `agents-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=<hmac>`. Unset ships unsigned payloads.                                 |

\* `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/<name>.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 `/agents-update`
invocation**, immediately after the submodule pointer moves and before
the bump commit lands. The cadence is codified in
[`/agents-update` Step 3.6](../.agents/workflows/agents-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/<name>.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
`/agents-update` bump commit so the reviewer sees the framework pointer
move and the allowlist response in the same diff.

---

## Cross-references

- JSON Schema mirror —
  [`.agents/schemas/agentrc.schema.json`](../.agents/schemas/agentrc.schema.json)
- Resolver entry point —
  [`config-resolver.js`](../.agents/scripts/lib/config-resolver.js)
- Sync helper —
  [`agents-sync-config.md`](../.agents/workflows/helpers/agents-sync-config.md)
- Bootstrap script —
  [`bootstrap.js`](../.agents/scripts/bootstrap.js)
- Quality gates runbook (CRAP onboarding, MI ratchet, lint ratchet) —
  [`docs/quality-gates.md`](quality-gates.md)
- Activation pointers (slash commands, personas, skills) —
  [`.agents/README.md`](../.agents/README.md)
