Skip to content
Docs Get started Mirrored upstream
config.md
Docs source v0.164.2 13d15fec6a14 View this file at the pinned commit ↗

Configuration

Detent separates machine settings, instance settings and project workflows:

  • global.yaml contains machine settings only for an enrolled runner, which reads ~/.config/detent-runner/global.yaml.
  • Cloud stores project membership, allowed projects, project rank and organization/project model selection in its database.
  • Each project has a checked-in detent.yaml machine contract, with optional machine-local overrides in detent.local.yaml. The project contract can also live in legacy WORKFLOW.md YAML frontmatter.

An enrolled runner reads its current allowed projects from Cloud, clones linked repositories under workspace_root (default ~/detent-runner), and loads their committed default-branch detent.yaml, WORKFLOW.md and AGENTS.md. It uses the host's Git authentication. One project's missing repository binding or failed clone is an instance setup failure; it does not stop valid projects or revoke its Cloud grant. An empty allowed-project list runs nothing.

The runner file may set the Hub URL, identity file, organization, display name, capacity, heartbeat and lease timing, provider-capacity file, CPU/memory/IO pressure limits, startup pacing, port, log level, cache and workspace root:

apiVersion: detent/v1
kind: GlobalConfig
service_name: detent.runner
workspace_root: /absolute/path/to/detent-runner
client:
  hub_url: https://cloud.detent.build/organizations/org_example
  identity_file: /absolute/private/runner/identity.json
  organization_id: org_example
  display_name: Build host
  capacity: 4
  heartbeat_interval_seconds: 30
  lease_ttl_seconds: 90
global:
  max_concurrent_agents: 4

Enrolled runners reject unknown keys at startup, including nested machine-setting keys. Errors name each unsupported key path. Removed keys include projects, weight, priority, scheduling, fair_share, global.agents.model_selection, client.native_projects and other local project policy defaults, even when their values are malformed. Remove these keys before upgrading; the one-release warning-and-ignore transition has ended. Before releasing this transition, the release owner must confirm the compatibility release shipped, verify that no live runner file still contains removed keys, and authorize the subsequent release. An unchanged legacy file will fail startup and may leave its service restarting. Cloud supplies project membership, rank and model selection; runners have no file-defined project or model-selection fallback. Local instances store these settings in SQLite.

Existing checkouts under workspace_root are matched by Git origin to Cloud-allowed repositories. They do not supply workflow paths or policy. Matching checkouts retain their project aliases and directories. With no repository workspace root, Git's registered Detent issue worktrees preserve a distinct retained worktree root, branch and dirty source. Explicit repository workspace roots remain authoritative. Multiple matching checkouts or retained roots require the operator to select the retained source; existing files remain untouched.

This page is the reference for machine and project configuration. Each project's workflow continues to come from its repository.

agent.no_progress_timeout_ms remains readable for older project files but is ignored. Worker sessions use their absolute duration bound, and local validation waits use the gate lock deadline.

Priority settings order ready work (INV-10). Stored project rank and dispatch_priority_by_state, dispatch_priority_by_label, and prioritize_unblockers choose the next eligible request. They never reserve idle slots or cancel running workers. max_concurrent_agents and max_concurrent_agents_by_state remain independent capacity ceilings.

Host Go caches

Workers inherit Go's host cache paths and operator environment settings. They retain per-attempt temporary directories. Startup removes legacy project caches under each configured workspace root; doctor and status report host cache paths and sizes.

global:
  cache:
    max_age: 48h
    # max_bytes: 53687091200 # optional explicit 50 GiB size target

When omitted, max_bytes is 10% of the total capacity of the volume holding GOCACHE, capped at 40 GiB (20 GiB fallback if capacity is unavailable). It is a retained-size budget for the managed runner cache, not a hard filesystem limit or a claim brake. Native Cloud runners admit new work while each cache and workspace volume has enough free space for its reserve: 10% of total volume capacity, capped at 40 GiB. Insufficient free space or an unavailable storage observation reports zero capacity for new work. Existing jobs keep their leases and recent cache files. Free space does not change the cache target. Doctor reports the fallback, last size eviction, and a target above 10% of total volume capacity.

The existing workspace reaper expires build-cache entries older than max_age. The effective age is at least 25 hours, preserving Go's last-day cache reuse and its one-hour access-time granularity for native operator caches. When the runner-owned cache exceeds max_bytes, that same cleanup waits under the existing admission lock for protected consumers to finish, then reclaims eligible entries oldest first under the exclusive consumer lock. New consumers resume after cleanup. Metadata (trim.txt, README, and ownership locks) is never removed. The module cache is reported and shared but is not trimmed. Concurrent builds can grow the managed cache between sweeps and exceed the budget while active. Unrelated host processes and native operator caches are not controlled by managed cache reclamation.

GitHub completion CI policy

Omitting gate.required_status_checks retains aggregate CI evaluation. Explicit gate.required_status_checks: [] uses the PR base branch's native required checks. If the branch requires none, absent or pending optional CI does not block promotion or merging. Reported failed CI always blocks. Native missing, pending, and failed checks still block. A nonempty list retains the existing configured-check behavior.

Issue session allowance

Code and rework share a fixed allowance of three sessions without a merged PR. The next code dispatch runs one read-only triage pass, publishes its explanation, and leaves the issue in Human Review. An operator move out of Human Review renews the allowance, as does a merged PR. Detent-instance moves, new PR heads, changed CI signatures, ordinary lane moves, and acknowledgements alone do not renew it; instance-attributed failures do not consume it. Completed, successful PR deliveries without a recorded error also do not consume it; waiting on the completion gate is not a failed delivery. The existing triage comment records the operator reset timestamp when comment updates are supported.

The former agent.auto_promote.rework_limit and agent.auto_promote.no_progress_limit keys are retired and have no effect. Remove them from existing configuration. There is no replacement tuning key. See INV-3 for the consolidation and historical-reader compatibility contract.

Session limits by complexity level

Model selection is configured in Cloud organization settings. Projects inherit that default and may override it in their Cloud project settings. Runners receive the effective selection from the Hub. global.agents.model_selection and agents.model_selection in detent.yaml or legacy workflow frontmatter are ignored with a compatibility warning for one release; the next release rejects them. Remove those keys from runner and repository files.

Cloud complexity levels may include max_session_duration_ms and max_session_tokens alongside model and effort. The selected level (stage override, matching rule, then default) supplies these limits once at session start. An omitted limit inherits agent's flat value; explicit 0 disables that limit. Values must be non-negative. Selection disabled or an ineligible backend uses the flat values. Per-issue detent-agent model and effort blocks retain their existing behavior, including clamping effort to the stage's ceiling.

The running session keeps its resolved values across turns, checkpoints, and fallbacks. Later label or configuration changes do not change its limits or reset attempt counts or lifetime usage. Turn inactivity, no-progress limits, context multipliers, and token exemption settings retain their existing behavior. detent doctor reports each level's effective limits for the project, plus the flat limits used by ineligible backends.

An issue without a detent-agent effort uses the default level (subject to existing stage and role rules).

Terminal attempt recovery

Set recovery.terminal_attempt_retry_limit in detent.yaml or detent.local.yaml to control recovery from consecutive terminal attempts without a pushed work product or linked pull request:

recovery:
  terminal_attempt_retry_limit: 0

The default is 3. The value preserves the existing consecutive-failure threshold for values of 2 or greater; 1 explicitly permits one recovery attempt, and 0 disables automatic recovery for a qualifying failure.

Setting Failure 1 Failure 2 Failure 3 Recovery after parking
0 Blocked No automatic attempt No automatic attempt Operator reviews the failure and moves the issue to Todo
1 Todo; one recovery attempt Blocked No attempt before recovery from parking Existing breaker cooldown
3 (default) Todo Todo Blocked Existing breaker cooldown

Thus 1 and 2 both park on the second qualifying failure. Higher values park on that numbered failure. Negative values are rejected. This compatibility rule keeps the existing default at two redispatches before parking.

The zero policy persists an operator-owned hold, including the latest failure evidence. It also enforces the hold from the current completion when saving the attempt fails or no durable attempt record is available. Restarting Detent or waiting for the breaker cooldown does not release that hold. If the tracker rejects the Blocked transition, Detent holds the issue locally and retries that transition during reconciliation without starting a worker. Changing the setting alone does not authorize release of an existing operator-owned hold. Retry counts are reconstructed from durable issue attempt history after restart; successful attempts and pushed work reset the sequence. Service-restart recovery remains resumable and does not consume this limit.

Durable provider capacity, GitHub REST capacity, and forge availability waits retain their existing handling and do not consume generic terminal retries. Transient overload failures remain qualifying terminal failures. Workspace preparation retains its separate fixed threshold of three failures. This policy is independent of automatic promotion, validators, and Rework limits.

detent doctor reports the effective setting, the qualifying failure on which Detent parks, and whether parking requires external review or uses cooldown recovery. Local overlays preserve an explicit zero.

Repository policy with Hub execution

The customer host resolves the project files and an administrator explicitly approves the resulting descriptor. For native projects, that approved revision stores the shared behavior and instructions consumed by every authorized runner. Credentials, host paths, capacity, isolation hooks and backend routing stay local. Repository compatibility projects retain their file-based approval semantics. Use detent hub policy inspect --config /path/global.yaml --project orders to review the resolved descriptor. Authenticated native inspection reads the approved shared configuration; offline inspection resolves the local proposal.

Configuration precedence and authorization are applied in this order:

Order Layer Effect
1 Parser defaults Existing defaults and legacy compatibility are unchanged.
2 Project definition anchor projects[].workflow selects the definition directory, including an external root. detent.yaml plus prompt-only WORKFLOW.md use schema 1; legacy frontmatter remains supported. Mixed structured authority is rejected. No competing checkout file is searched.
3 Machine overlays Existing detent.local.yaml and WORKFLOW.local.md rules apply: mappings merge, explicit scalars/sequences replace, supported empty/false values clear, and local prose appends.
4 Host configuration Existing flags/environment/global precedence, identity, paths, authorization filters, pool/backend limits, budgets and host ceilings remain enforced on customer infrastructure. Repository requirements cannot increase host permissions or capacity.
5 Hub policy approval Native runners consume the approved shared behavior and instructions while retaining host configuration. Compatibility runners must resolve the approved effective files and local overrides. Both require an exact descriptor match; missing, stale or contradictory policy denies dispatch.
6 Run pin Claims atomically pin the approved policy. The customer runner rechecks approval before dispatch, claim adoption and renewal, and checks its loaded workflow before credentials or workspace creation.

Use an administrator-selected workflow_ref to read shared files from a trusted Git commit. The loader resolves the ref once and reads shared files at that commit; edits to the working branch do not change it. Shared behavior changes in local overlays still need approval. An external definition is authorized by reviewing its resolved content digest through the same approval command. Approvals must be performed outside the implementation worker; do not give workers the Hub administrator credential.

hub policy inspect prints schema 1, the source revision, source/configuration digests, a content-derived policy_id, selected runner requirements and gate metadata. Native descriptors also contain shared configuration, workflow prose, commands and admission guidance. Their digests identify portable behavior rather than host-specific source bytes or Git revisions. Host paths, credentials and transport settings are excluded. Compatibility descriptors contain only digests and bounded metadata; their underlying content stays on customer infrastructure.

After reviewing the files and descriptor, an administrator runs:

detent hub policy approve --config /path/global.yaml --project orders

The command reads DETENT_HUB_ADMIN_TOKEN (or --admin-token-env), resolves the selected local definition and uploads its descriptor. Replacement requires --expected-policy-id policy_... to prevent a stale approval from overwriting another administrator's decision. For repository-controlled native workflows, an enrolled runner automatically applies definitions committed on the bound repository's default-branch history through the same Hub approval transaction. It verifies the configured origin, committed contents and Git ancestry before reporting provenance. Feature-branch commits and uncommitted edits still require explicit administrator approval. Other native shared behavior or instruction changes require one reviewed approval for the project. Planning, validation and automatic promotion can be enabled together; the existing workflow refresh applies the approved combination to idle runners without copying files. Host configuration changes do not produce competing native approvals. Existing native approvals without shared configuration adopt this model through the same replacement approval path. Compatibility projects still require new approval for effective file, permitted local adjustment, environment or trusted source revision changes on each host.

Active leases retain their policy, and Hub refuses a replacement while they remain active. Finish or cancel those attempts, approve the new descriptor, and let native runners refresh the shared revision. Compatibility processes still require restart or the existing managed configuration application to adopt a different approved identity. Invalid or unapproved reloads retain the last-known-good configuration and report an actionable error. Resumed provider sessions must match the policy stored with their original attempt, including after Hub is disabled; missing or mismatched recovery identities are rejected. Automatic thread reuse falls back to a fresh attempt under the current policy. Standalone sessions that never had a Hub policy retain legacy recovery.

Administrators can revoke the current approval through the policy API. This immediately denies claims, lease renewal and new run events. Worker heartbeat checks observe the revocation and stop work through existing lease-loss handling; release remains available. Historical revisions and lease pins are retained.

Plan-review stops, gate kinds, human approval, automatic-promotion opt-outs, CI/check requirements, validators, security audits and merge methods remain project-specific. Two repositories sharing a runner retain separate approvals and gates. Hub approval is not a human plan/PR approval. GitHub's repository automatic-merge setting remains owned by that repository; onboarding inspection reports it without changing it. Host restrictions and revoked authority win over an approved policy or a Hub allowance. Hub has no browser setting that silently relaxes repository policy.

Effective policy appears in project settings, work-attempt receipts, health workflow provenance and detent doctor output. Policy mismatches leave work waiting without creating an execution attempt or consuming execution retries.

Named runner requirements

Declare profiles in the existing shared or legacy configuration and select one with runners.profile:

runners:
  profile: build
  profiles:
    build:
      required_tags: [linux, go]
      machine_id: machine_0123456789abcdef
      runner_id: runner_0123456789abcdef

Profiles and tags use ASCII letters/digits with -, _ and ., up to 64 characters. Tags are trimmed, lowercased, sorted and deduplicated; profile names must already be lowercase. At most 32 profiles and 32 tags per profile are accepted. Selectors are optional stable IDs with machine_ or runner_ prefixes; display names are not selectors. Every required tag and every explicit selector must match. Unknown selectors never widen to another host. An empty profile selection has no extra routing requirements. Explicit empty lists and selector strings in local overlays retain their existing clearing semantics and require approval as part of the effective definition.

Profiles declare requirements, never enrollment or capabilities. Machine registration still happens on an authorized physical host. The Hub checks machine pins and matches runner IDs only against authenticated enrolled runner identities. Legacy worker tokens cannot assert an enrolled runner ID. Required tags return selector_no_match until the authorized tag-placement deliverable #2185 is available. Self-reported machine capabilities never satisfy privileged tags. See runner enrollment for the separate host authorization path.

Host configuration

At startup, Detent resolves global.yaml in this order. The first matching rule wins.

Order Rule Path
1 --config <path> Direct file path from the CLI flag
2 CONFIG=<file> Direct file path from the environment
3 CONFIG_HOME=<dir> <dir>/global.yaml
4 os.UserConfigDir() <config-dir>/detent/global.yaml
5 Legacy home config ~/.detent/global.yaml

os.UserConfigDir() maps to %AppData%\detent\global.yaml on Windows, ~/Library/Application Support/detent/global.yaml on macOS, and ~/.config/detent/global.yaml on Linux while honoring XDG_CONFIG_HOME.

DETENT_CONFIG and DETENT_HOME remain deprecated fallbacks for one release. Detent uses CONFIG_HOME instead of HOME because HOME is standard process state, not Detent configuration.

Use detent config path to print the resolved config path and the rule that selected it.

Runtime settings resolve in this order: explicit flag, environment variable, global.yaml, then built-in default.

Setting Flag Environment global.yaml key Default
Environment --env ENV, then DETENT_ENV env prod
Log level --log-level LOG_LEVEL, then DETENT_LOG_LEVEL log_level info
Log max size LOG_MAX_SIZE_BYTES, then DETENT_LOG_MAX_SIZE_BYTES log_max_size_bytes 52428800
Log backups LOG_MAX_BACKUPS, then DETENT_LOG_MAX_BACKUPS log_max_backups 5
GitHub token GITHUB_TOKEN github_token required for GitHub projects
API token DETENT_API_TOKEN api_token open on loopback, fail closed on non-loopback
Loopback peer read trust trust_loopback_peer_read false
Hub scheduler client.hub_url disabled
Hub worker token DETENT_HUB_TOKEN client.token_env names the environment variable required when Hub scheduling is enabled
tmux window status $TMUX detection ops.tmux_window_status enabled inside tmux
Web port --port PORT port 4000
Instance name instance_name short hostname
Service name service_name detent
Health webhook notifications.health.webhook.url disabled
Health notification debounce notifications.health.debounce_seconds 300
Health webhook timeout notifications.health.webhook.timeout_ms 5000
Automatic update checks update.auto_check_enabled true for Hub runners, otherwise false
Update check interval update.check_interval_hours Hub heartbeat interval for Hub runners, otherwise 6 hours
Automatic update apply when idle update.auto_apply_enabled true for Hub runners, otherwise false
Maximum automatic update deferral update.max_deferral_hours 6

Automatic apply waits for an idle runtime until update.max_deferral_hours elapses, then pauses new dispatches and lets in-flight attempts finish within their configured session ceilings before applying and restarting. Add [detent-critical] to a GitHub release body to bypass the idle wait and start that drain immediately.

detent cloud serve and detent hub serve read no global.yaml; they resolve the log level from --log-level, then LOG_LEVEL, then DETENT_LOG_LEVEL, default to info, reject any value other than debug, info, warn or error, and write JSON logs to stderr for journald.

The web host resolves from --host, then the first registered workflow's server.host, then the built-in 127.0.0.1 default. It is not a top-level global.yaml key.

Host pressure admission

On Linux, Detent refreshes Pressure Stall Information (PSI) at admission time, subject to each signal's configured poll interval. Pressure above any configured threshold holds new dispatches; it does not cancel or reap agents that are already running.

global:
  memory:
    max_agent_rss_bytes: 8589934592
    pressure_some_avg60_threshold: 10
    poll_interval_ms: 1000
  io:
    pressure_full_avg10_threshold: 5
    degraded_max_concurrent_agents: 0
    poll_interval_ms: 1000
  cpu:
    pressure_some_avg10_threshold: 80
    degraded_max_concurrent_agents: 0
    poll_interval_ms: 1000
    go_build_budget: 0

go_build_budget is the host-wide number of Go compile, link, asm, cgo, and vet processes that all worker attempts may run at once. The default of 0 uses the host's CPU count. For workers whose workspace root has a go.mod, Detent sets GOFLAGS=-toolexec=<detent go budget wrapper>, so every go build, go test, go vet, and go install a worker runs, including commands the agent types itself, waits for a free slot before each tool process starts. The same environment sets -p and GOMAXPROCS to a quarter of the budget (at least 1) so each go command's package parallelism and test binaries stay within the host's share. Flags the host environment already sets in GOFLAGS are kept, and an explicit -p or -toolexec wins. The budget only queues tool processes; it never fails, parks, or cancels work, and it runs unqueued if its slot directory cannot be prepared. Workspaces without a go.mod keep their environment unchanged. The shared GOCACHE is unchanged and build and test cache hits are preserved. Changing this value requires a restart.

degraded_max_concurrent_agents is an opt-in host-wide progress floor for IO and CPU pressure. Its default of 0 preserves the conservative hard stop. Set it to 1 to allow one worker across all named pools while that signal remains constrained. If IO and CPU pressure are active together, the lower configured floor wins. The pressure ceiling only blocks new admissions; it never preempts running workers, and pool, project/state, worker-host, startup pacing, and backend limits can reduce capacity further.

A guard engages only above its configured threshold. Once engaged, IO and CPU guards stay constrained until the measured average falls to 80% of the trigger threshold, which prevents dispatch from flapping when PSI hovers around the boundary. For example, an IO threshold of 5 engages above 5 and recovers at or below 4.

The defaults shown above are starting points, not universal safe values. With the default degraded capacity of 0, the sample IO threshold of 5 is a fleet-wide hard stop whenever IO full avg10 stays above 5%, including when an unrelated host process creates the pressure. IO full is the strongest build host signal because it measures time when every non-idle task is stalled on IO. CPU uses some, because system-level CPU full is defined to report zero. Memory some remains useful for reclaim and swap pressure, but a compile-heavy host can be IO- or CPU-saturated while memory PSI stays at zero. See the Linux PSI interface for the kernel definitions.

Treat configured pool capacity as the hard ceiling and use PSI as the dynamic admission brake. Before enabling a low threshold, observe /proc/pressure/io and /proc/pressure/cpu during representative Detent and unrelated compile, test, browser, database, and media workloads. Calibrate the trigger above the host's acceptable sustained baseline, then lower it if the interactive host becomes unresponsive before the guard engages. Raising the hard ceiling based only on free memory can overload a build host.

The ten-second IO and CPU windows respond faster than the sixty-second memory window. Keep global.startup.jitter_seconds and max_spawn_per_second conservative enough for those averages to react during service startup; PSI then constrains and restores admission as contention changes. On systems without PSI, or when a pressure file cannot be read, Detent fails open and reports the unsupported or unavailable signal rather than inventing a zero reading.

detent state, /api/v1/state, and /health expose the current values, thresholds, read errors, configured degraded capacity, effective pressure capacity, constraint start time and duration, and whether each signal is holding dispatch under memory_pressure, io_pressure, and cpu_pressure. Durable scheduler decisions include pressure capacity, usage, availability, reason, and duration. Work-attempt capacity snapshots include the active pressure capacity, reason, and duration.

When Detent starts inside tmux, it renames the current window to a compact running, ready, waiting, and blocked count summary. It restores the original name on clean shutdown. Set ops.tmux_window_status: false to disable this behavior; Detent never invokes tmux when $TMUX is absent.

Use github_token: gh in global.yaml to resolve the token from gh auth token at startup. Literal token values also work but should not be committed. github_token: gh-auth, ${gh auth token}, and $(gh auth token) are accepted aliases. If neither GITHUB_TOKEN nor github_token is set, Detent falls back to existing per-workflow tracker.api_key handling.

Set trust_loopback_peer_read: true to grant tokenless read-scope API access to direct loopback TCP peers even when Detent binds a non-loopback address or has an api_token. The setting hot-reloads and applies only to GET routes; admin routes and mutations still require credentials. Authentication uses only the request's direct peer address and never trusts forwarded-client headers. Do not enable this setting when a reverse proxy runs on the same host: every request relayed by that proxy has a loopback direct peer, including requests that originally came from remote clients.

Use instance_name to distinguish browser tabs and the dashboard navbar when several Detent instances are open at once. Detent resolves the display name from the first non-empty value in this order: top-level instance_name in global.yaml, global.identity.name, the short hostname, then empty. Names are trimmed, must be a single line, and are capped at 40 characters in the web UI.

service_name names the background service: the launchd job com.digitaldrywood.NAME and the systemd user unit NAME.service. It defaults to detent; detent hub runner register writes service_name: detent.runner. Give each configuration on one host its own name. Names use lowercase letters, digits, dots and hyphens.

A project listed under client.native_projects whose committed detent.yaml uses the GitHub tracker (github or github_local) uses the Hub instead (tracker.kind: hub_native), so a runner host can use a repository checkout as it is. Any other tracker kind is treated as a deliberate local choice and kept.

Configure client.hub_url to move candidate discovery and claiming to a Detent Hub. The machine registers its identity, project and pool capabilities, capacity, operating system, architecture, and binary version. It then heartbeats while polling the Hub and renewing fenced work leases. The token is read from the environment variable named by client.token_env in legacy mode; the token is never stored in the configuration file. For scoped native runners, use client.identity_file instead: an absolute path to the private host-generated identity created by detent hub runner init and enrolled with detent hub runner enroll. The client renews this credential automatically. Its organization, machine ID and explicit native projects must match enrollment. See runner onboarding and migration.

Enrolled native runners can opt into provider-aware dispatch with client.provider_capacity_file, an absolute path to a bounded JSON report file. The runner reads it on heartbeats and again before starting provider work. See provider capacity reports for its safe schema, shared-account declarations and reservation semantics. This setting does not change backend credentials, model routes, effort defaults or fallback policy.

client:
  hub_url: https://hub.example.com
  token_env: DETENT_HUB_TOKEN
  machine_id: build-mac-01
  display_name: Build Mac 01
  capacity: 4
  heartbeat_interval_seconds: 30
  lease_ttl_seconds: 90
  request_timeout_ms: 10000

In legacy mode, machine_id defaults to the configured global identity, instance name, or hostname. Enrolled mode uses the immutable host-generated machine ID. capacity defaults to global.max_concurrent_agents. The heartbeat interval must be shorter than the lease TTL. Hub outages mark project refresh health degraded and back off with the normal scheduling loop; they do not consume issue retry, no-progress, or work-failure budgets. GitHub remains in use for event ingestion, reconciliation, write-back, and fresh safety checks, but the scheduling claim cycle does not depend on a GitHub read.

Native issue projects use tracker.kind: hub_native in their repository workflow configuration. Enrolled runners obtain the stable project IDs and organization through their identity and current Cloud grants. No local client.native_projects mapping is needed. Native issue creation, full-body refresh, discussion, history and scheduling use the Hub v2 API. There is no fallback scheduler. The Hub API documents project provisioning, workflow states and typed operations.

A native project reuses the repository WORKFLOW.md as its prompt. Detent appends a native completion contract to every native run prompt that overrides tracker-specific steps in that file: the agent commits on the attempt branch, never pushes or opens pull requests on the forge, never runs gh or calls the GitHub API, and does not post Workpad or issue comments or change issue state. The runner records the Change Request from those commits. Native projects make no GitHub REST or GraphQL calls by default: workers do not receive the instance GitHub credential, and checkpoints do not look up or open pull requests. No separate key turns this on; an explicit worker.github_token is the only opt-in for worker GitHub access. The contract is added at render time, so it does not change the approved policy digest. detent doctor (check Project <id> native workflow instructions) and detent hub policy inspect warn when a native project's WORKFLOW.md still mentions GitHub-only steps such as Codex Workpad, a gh command, pull request, detent-status or the GitHub API.

Health notifications deliver fleet and project needs-attention transitions to one generic webhook. They are disabled when notifications.health.webhook.url is absent, preserving the silent default. Configure the host-wide channel in global.yaml:

notifications:
  health:
    debounce_seconds: 300
    webhook:
      url: https://alerts.example.com/detent
      headers:
        Authorization: Bearer replace-me
      timeout_ms: 5000

The five-minute default debounce applies independently to entry and recovery. Project identities are stable per project and cause, currently ci_unavailable and dispatch_stall; a separate fleet identity represents the aggregate transition while any project cause remains active. A flap that clears before the debounce expires emits neither entry nor recovery. Once an entry is emitted, exactly one recovery is emitted after the identity remains healthy for the debounce window.

Webhook payloads use event name detent.health.transition and include a stable event ID, host and instance names, scope, optional project ID, entered state, causes, optional wait reasons, and entry timestamps. Delivery state and pending events are stored in the runtime SQLite database before sending, so a restart does not re-fire active conditions or discard unsent transitions. Failed sends retry at most five times with exponential backoff starting at 30 seconds and capped at 15 minutes. Every failure is logged and appears in health_notification_failures on /health; detent doctor reports both retrying and exhausted deliveries. Notification configuration changes require a restart. Treat configured headers as secrets and keep host-specific values out of checked-in project configuration.

Automatic update checks and apply are enabled by default when client.hub_url is configured. These runners check the Hub's running version through its authenticated native capabilities response at the configured heartbeat interval, unless update.check_interval_hours is set. They fetch that exact published release rather than GitHub latest. A runner ahead of the Hub stays on its installed version; an unreachable Hub, missing version or unpublished target does not trigger a fallback update. Explicit auto_check_enabled: false or auto_apply_enabled: false settings opt out and survive configuration writes.

Detent reports the last check, available or applied version, and next check on /health. detent doctor surfaces the last and next check times and reports whether the host is opted in. Automatic apply only replaces release-installer binaries; other install sources remain notification-only. A successful apply uses the normal graceful drain path, then re-executes the replaced binary on POSIX systems or exits cleanly for an external supervisor to restart it on other platforms.

detent doctor prints the resolved runtime values and their sources, with the GitHub token redacted.

Choose a starting point

The root detent.yaml and WORKFLOW.md configure the Detent project itself. They are not templates for other projects. Use the documentation samples below when configuring another repository.

  • docs/templates contains paired detent.*.yaml and WORKFLOW.*.md presets for supported tracker and deliverable setups.
  • config.example.yaml is the smallest working project config.
  • config.annotated.yaml is a realistic GitHub setup with explanations beside each deliberate choice.
  • config.reference.yaml contains every supported project key. It is fully commented so you can copy only what you need.

Keep detent.yaml portable and checked in. Put machine-specific paths or secrets in detent.local.yaml, a credential reference, or host configuration. The local file overlays the shared file recursively and is not a second, independent configuration.

How the subsystems fit together

Identity, tracker, and polling

identity names the Detent persona that owns work. tracker connects that persona to its work source; native projects use tracker.kind: hub_native, which keeps issues in the Hub. The tracker declares workflow states in one ordered tracker.lanes list; state maps, transition automation, admission, planning, stop behavior, and routing all refer back to those names. polling controls how often Detent refreshes the tracker when no event gives it a fresher signal. Its refresh_failure_threshold controls how many consecutive failures after a successful refresh promote project and fleet health to needs_attention; a project whose first refresh fails needs attention immediately.

Each lane has a name and a role: active means dispatchable, holding means non-dispatchable and non-terminal, and terminal means finished. File order is the order on Cloud boards and lists, lane pickers, and approved repository workflow policies.

tracker:
  lanes:
    - {name: Backlog, role: holding}
    - {name: Todo, role: active}
    - {name: In Progress, role: active}
    - {name: Rework, role: active}
    - {name: Merging, role: active}
    - {name: Blocked, role: holding}
    - {name: Human Review, role: holding}
    - {name: Done, role: terminal}
    - {name: Cancelled, role: terminal}

tracker.observed_states, tracker.active_states, and tracker.terminal_states are rejected in detent.yaml, legacy WORKFLOW.md frontmatter, and Cloud Workflow settings, including empty and null values. Use tracker.lanes for all new and updated workflow configuration. Run detent config migrate detent.yaml (or detent config migrate WORKFLOW.md for legacy frontmatter) to rewrite the file in place. The command preserves lane semantics, comments outside the replaced lists, and the Markdown prompt; running it again leaves the file unchanged. An explicit local lanes list replaces the shared lane list in full. Cloud-authored Markdown is migrated automatically by the Hub, preserving each project's stored lane order and approval provenance. Historical policies and approvals remain readable.

dependency_auto_unblock, blocked_recovery, and blocker_auto_promote are opt-in tracker automations: the first advances work whose dependencies cleared, the second revives agent-recoverable PR maintenance, and the third promotes blockers when they enter configured states.

GitHub and Linear trackers also receive a default Statuspage base URL through tracker.status_page_url; set that key to override the provider endpoint. Detent follows redirects and polls the unauthenticated Statuspage summary and unresolved-incident feeds at a low-frequency baseline, increasing freshness only while a Detent-observed tracker_unavailable condition is active. These feeds decorate health output only: malformed, slow, or unreachable status pages never create a condition or influence dispatch and recovery. Statuspage incident webhooks are a possible future optimization, not part of the current polling integration.

The tracker setting github_rest_min_remaining_reserve is an absolute reserve for the REST core resource (default 1000). It does not reserve requests in smaller resources such as search; those resources still enforce GitHub's primary and secondary rate limits and shared backoff. Recovery probes, reserve diagnostics, and scheduler lookup gates use the same resource rule. The aggregate REST quota snapshot retains core capacity; resource-specific budgets continue to report search usage separately. Responses without a resource name retain the core reserve for compatibility. No configuration migration or new setting is required; the worker's separate core-budget policy is unchanged.

Workspace, deliverable, and worker placement

workspace controls isolation, source and output roots, branch creation, cache sharing, and cleanup. deliverable selects pull requests or file artifacts and their review destination. worker distributes sessions across optional SSH hosts; per-state and global concurrency limits remain under agent.

SSH workers run the installed detent binary on each selected host over an SSH stdio channel. Workspace setup, hooks, provider processes, gates, and Git pushes execute there; session records, Workpads, lane decisions, and merge coordination stay with the orchestrator. Hosts use normal SSH configuration and host-key verification, with noninteractive authentication and no agent forwarding.

worker:
  # Per runner host; omitted prices inherit Sprites public rates.
  compute_rates:
    local:
      cpu_hour_usd: 0.07
      memory_gb_hour_usd: 0.04375
  ssh_hosts: [example-laptop, example-build-host, local]
  host_selection: preference
  max_concurrent_agents_per_host: 2
  host_caps:
    example-laptop: 2
    example-build-host: 6
    local: 1

Sandboxed workers can opt into localhost servers and connections for HTTP tests with worker.allow_local_binding. It defaults to false. Set the instance default in the global config:

global:
  worker:
    allow_local_binding: true

Override that default in a project's detent.yaml or machine-local detent.local.yaml:

worker:
  allow_local_binding: false

An omitted project value inherits the instance default; explicit false disables it even when the instance default is true. The effective grant participates in Cloud policy approval. It keeps the sandbox tier and file restrictions, but permits connections to other localhost services as well as temporary test servers. Codex also skips its proxy's private-network destination check with this grant; proxy domain rules still apply. Restricted Codex turns keep networking disabled.

A project can grant additional exact DNS hosts through worker.extra_network_domains in detent.yaml or detent.local.yaml:

worker:
  extra_network_domains:
    - fonts.googleapis.com
    - fonts.gstatic.com

The list adds to Detent's dependency/provider hosts. It defaults to empty and participates in Cloud policy approval. Wildcards, URLs, IP addresses, ports, duplicates and uppercase hosts are rejected. Filesystem restrictions and the sandbox tier remain in effect; restricted Codex turns keep networking disabled.

preference fills hosts in list order to their project-specific cap. The default least_loaded selects the least busy available host and retains retry affinity when that host has capacity. host_caps overrides the fallback max_concurrent_agents_per_host for individual configured hosts. Include local where local execution belongs in the order; an empty list means local execution. The existing project and instance agent limits still apply.

Install matching Detent binaries, Git, gh, and the configured provider on every remote host. Provider subscription authentication must already be available there. The noninteractive SSH environment must provide a writable TMPDIR, TMP, or TEMP; workers never fall back to host scratch space. Paths below the orchestrator's home directory are mapped below the remote user's home directory; other absolute paths must exist on that host. Hook references to the home path are mapped too. Provision the source checkout there, or allow Detent to clone its HTTPS origin into the mapped source root on first use.

Detent checks SSH reachability and the worker protocol before dispatch, caches the result for 15 seconds, and skips unavailable hosts. A lost channel cancels the remote lifecycle and uses the existing instance retry path. A retry on a different host starts a fresh provider session. Existing workspace reaping and reconciliation also visit configured remote hosts. GitHub credentials cross only the encrypted channel and use the same private temporary GH_CONFIG_DIR as local workers; they are removed when the worker exits and never written to the source checkout or persistent Git configuration.

Remote workers support GitHub, Linear, and hub_native trackers. Native runs capture artifacts and attempt diffs on the selected host through SSH; upload journals, credentials, lease fencing, Change Request publication, landing reports, and provider reservations remain central. Interrupted transfers retain their journals for existing recovery. Both endpoints must support SSH protocol version 2.

GitHub-capable workers never inherit GITHUB_TOKEN, GH_TOKEN, their enterprise variants, or the host's GitHub CLI configuration. When omitted or empty, worker.github_token defaults to the resolved top-level github_token (including github_token: gh), except for tracker.kind: hub_native projects, which never inherit the instance credential. An explicit project worker credential overrides that default; with neither credential configured, the worker policy is disabled. For native landing pushes, HTTPS uses the runner host's GitHub CLI login when no worker token is handed off; SSH uses the host's SSH key. An explicitly handed off worker token remains the Git credential for HTTPS operations in that worker. worker.github_token controls agent GitHub access; it is not required for native landing. Agent GitHub CLI calls retain their private GH_CONFIG_DIR in both cases. detent doctor includes a named landing push credential check that runs a noninteractive dry run without changing remote refs. Set the worker override to gh to resolve gh auth token in the Detent process and copy only the resolved token into the worker's isolated GH_CONFIG_DIR. An environment reference remains available when permission or revocation isolation is useful:

worker:
  github_token: gh
  github_token_resolution_timeout_ms: 15000
  github_rest_min_remaining_reserve: 1250
  github_rest_poll_interval_ms: 60000

The token-resolution timeout applies to each gh auth token invocation. Detent tries the command up to three times with short backoff before recording a same-attempt infrastructure wait; these failures do not consume issue attempt or failure-breaker budgets. Increase the timeout when the host credential store can legitimately take longer than 15 seconds.

After resolving gh, Detent classifies an exact orchestrator token or any token that resolves to the same GitHub user as shared_budget: GitHub's primary REST limit is per user, so a second token changes permissions and revocation without creating another rate-limit pool. Shared-pool telemetry uses consumer: shared_pool and reports usage attribution as indeterminate rather than assigning the pool's global usage to workers. A different GitHub user or App installation is distinct_principal, the true rate-limit-isolation mode.

The default worker reserve is 1250, above the default orchestrator dispatch floor of 1000. Shared-mode workers therefore brake first so dispatch can continue. Detent probes the worker credential at launch and no more often than the configured interval, and stops or refuses a worker at its reserve. Keep literal tokens out of checked-in project files.

workpad.structured_only requires machine-readable workpad status instead of accepting legacy narrative signals. GitHub native blocked-by relations and recognized current issue-body dependency lines form one blocker list. Native metadata wins for duplicate references; an empty native list does not discard body declarations. Fenced examples never add blockers. Historical comments are diagnostic only, with unmatched references reported as ignored in issue explanations. Unsupported repositories use body declarations alone. Failed blocker-state lookups propagate errors rather than returning dispatch candidates.

The existing dependencies.source values (merged and native_only) are deprecated compatibility aliases for this same behavior; neither disables current body declarations. Remove a body declaration to remove its blocker (and remove the native relation as well if one exists).

Agents, backends, and routing

agent is the orchestration policy: concurrency, turn and session limits, retry brakes, spend limits, shutdown, dispatch priority, automatic promotion, state-specific instructions, learned context, skill drafts, and follow-up work. agents.backends defines pluggable Codex, Claude Code, or Pi processes, while agents.routes selects a backend and model by role or issue selector.

codex is the legacy/default Codex backend configuration and remains the fallback when agents.backends is empty. A Codex backend inherits omitted values from it. Claude Code backend options are interpreted only when the backend kind is claude_code.

Pi uses kind: pi_agent, protocol: rpc, and command: pi by default. The backend provider and existing routes select its provider and exact model ID. Pi requires native-trusted isolation; sandboxed/read-only turns, resume, Detent dynamic tools and persisted history hydration are unsupported in this initial integration. See Pi backend configuration and limitations for a complete route example, authentication and an opt-in smoke test.

Implementation workers on automatically owned Git branches request a bounded recovery checkpoint when a duration, turn, or no-progress limit stops them. agent.checkpoint_interval_ms also enables periodic checkpoints; 0 disables the periodic trigger. Each checkpoint allows up to two minutes for a read-only worker to select reviewed issue files and for Detent to commit and publish them with lease and remote checks. Periodic checkpoints retain the session deadline and accumulated budget usage. Identical work does not create new commits.

Checkpoint publication may associate a draft PR and never satisfies completion, review, or CI gates. If delivery is unavailable, the owned workspace remains recoverable. Its Git metadata contains detent-checkpoint.json, written before worker execution, so an interrupted or killed worker does not imply that a checkpoint epilogue ran. Human branches and backends without the handshake continue to rely on local workspace recovery.

agent.auto_promote and gate work together. Auto-promotion decides when an item may leave its source state; the gate decides whether CI, automated review, human approval, a validator, or an artifact status is sufficient. plan optionally inserts a plan-review stop before implementation.

The validator records evidence for each acceptance criterion on the immutable Change round. gate.validator.unverified_criteria defaults to rework: any gap returns the work for evidence, with the criterion named. Set it to pass-with-disclosure to allow a pass while retaining the unverified list for review. Minimum score and blocked finding severities still apply. Criteria come from the issue contract's Acceptance criteria section, or its first required section when a project uses a custom contract without that heading.

Operations, budgets, and observability

active_hours optionally limits new agent dispatches to recurring weekday and wall-clock spans in a required IANA timezone. Use window values such as Mon-Fri 22:00-06:00; ranges that end before their start wrap past midnight, and 00:00-24:00 covers a full day. A project entry in global.yaml overrides global.active_hours, and either host-local value overrides the project's checked-in setting. Window close drains existing agents instead of stopping them.

Detent evaluates membership on each scheduler tick, preserving local wall-clock hours through daylight-saving changes and restarts. Spring forward can shorten a wrapping window by one hour, while fall back can lengthen it by one hour. detent doctor previews opening and closing times in both the named timezone and UTC. A temporary detent resume <id> --for <duration> override admits work outside the window until its persisted expiry; manual pause remains the stronger, independent gate.

observability controls dashboard refresh, efficiency anomaly thresholds, and optional OTLP export. budget is the project-wide spend policy; agent.budget can further constrain agent sessions. release enables release-readiness automation after merged work accumulates.

Release automation requires tracker.kind: github, tracker.repository: owner/repository, and release.enabled: true. It is unavailable for hub_native with either plain git delivery (deliverable.github_pull_request: false, the default) or GitHub pull request delivery (deliverable.github_pull_request: true). Config validation rejects release.enabled: true for native projects, including the release-unavailable warning in the returned error so creation and reload failures surface it. Leave release.enabled: false when migrating to hub_native. Opting into GitHub pull requests does not enable release automation.

observability.stranded_active_threshold_seconds controls when Health and detent doctor report an issue that remains in In Progress without a live worker and when Detent recovers it automatically. Issues with an open pull request or recoverable workspace work move to Rework; issues confirmed to have no recovery artifacts move to Todo. Detent holds the issue in place when artifact evidence is unavailable. The default is 600 seconds, which tolerates normal gaps between a completed session and prompt re-dispatch.

hooks run lifecycle commands around worktree creation, agent execution, and cleanup. Treat them as trusted code: they run with the Detent process permissions and should be short, deterministic, and safe to retry.

hooks.runner_setup names a repository-relative script for project tools on a registered runner. It runs from the project's source checkout during runner preparation, before any work is claimed, and is checked before subsequent dispatch. It uses hooks.shell and hooks.timeout_ms (60 seconds by default; increase this for installers). The script need not have executable permission.

hooks:
  runner_setup: scripts/runner-setup.sh
  shell: bash
  timeout_ms: 300000

Each runner records the successful SHA-256 of the script content, shell and source checkout path in its local store, scoped to its runner identity and project. When the checkout is a Git clone, the runner also writes the hash to .git/detent-runner-setup inside it. Unchanged setup does not run for later attempts or after restarting with the same store. Changing the script, shell or checkout path, or recreating the checkout, runs setup again before the next dispatch. With workflow_ref, the script comes from the same Git revision as the project's configuration; otherwise it comes from the source checkout, including local edits. Paths must remain inside the repository. Keep setup self-contained: changes to files sourced by the script do not change its recorded hash.

Setup is trusted host code. Make it idempotent, non-interactive, and free of secrets; the runner closes standard input and inherits its host environment, including native tool caches and supplied scratch directories. Install CLIs into a directory already on the runner's PATH. Shell exports do not persist into later agent turns. Setup failures are instance-owned for that project on that runner, appear in runner logs and the project's local-check API report, and prevent claims and dispatch without consuming an issue failure allowance. The existing project polling retries setup until it succeeds. Other projects and runners remain eligible.

Use workspace hooks for worktree or attempt preparation. Move project tool installation from hooks.after_create, hooks.before_run, or Sprite pool customer bootstrap into this script. Pool bootstrap is reserved for prerequisites that cannot live in a readable repository file, such as fetching credentials through a customer's secret service or obtaining the checkout.

Intake, retrospectives, routines, and admission

intake.sources, retro, routines, and backlog_admission are scheduled features of repository trackers. tracker.kind: hub_native rejects intake.sources and routines, retro requires a github or memory tracker, and backlog_admission needs candidate selectors that native trackers do not provide. Native cutover refuses a workflow that still enables any of them.

Fleet staleness warnings

observability.staleness detects work items that exceed lane-specific aging thresholds, non-empty dispatch or merge queues that stop advancing, and repeated identical scheduler decisions. Lane residency starts at the most recent entry. Repeated entries can still raise a cumulative warning when their residency exceeds the lane threshold within observability.staleness.lane_reentry_window_hours. Human-gate lanes emit one reminder at their threshold, then remain quiet until the lane changes or observability.staleness.human_gate_rearm_hours passes. Sticky breaker and operator parks with a recorded cause are excluded from lane aging because the Blocked lane already carries their operator signal. A park_cause_stale warning replaces lane aging when recorded evidence is cleared or can no longer be verified.

Operator-facing warnings use the same authorization selector as board and runtime rows. Item warnings outside that selector are excluded before they are logged, delivered, or counted. Project-level liveness warnings are computed from authorized dispatch and merge queues. Repeated decisions about the authorization boundary itself, including authorization_selector_declined, remain visible even when the affected item is out of scope because the selector decision is the condition the operator must correct.

A Blocked item observed from tracker state without a recorded blocker cause is classified as blocked, cause unrecorded. The card keeps that classification beside its lane provenance, and an eventual lane-aging warning names the missing cause and the remedy instead of presenting an unexplained generic Blocked age.

The repeated-decision detector considers only current, non-terminal items. Closed and merged items and items in a configured terminal state are excluded. observability.staleness.repeated_decision_benign_reasons lists scheduler reasons that represent healthy waits rather than operator-actionable stalls. Its defaults cover active workers, dependency waits, global-capacity waits, and GitHub REST pacing. Set the list explicitly to replace those defaults for a project. Values must exactly match a scheduler-emitted reason. Configuration validation reports the configured value and the emitted reason set when they do not match; for example, unauthorized does not match authorization_selector_declined. This validation is also surfaced by detent doctor during config resolution.

The detector evaluates decisions inside observability.staleness.repeated_window_hours, but the orchestrator retains only the 500 most recent scheduler decisions. Its effective history is therefore whichever is smaller: the configured time window or the retained 500-decision sample.

Warnings appear in the dashboard, /health, and detent doctor. Configure observability.staleness.webhook.url to push each newly active warning as a detent.staleness.warning JSON event. The payload includes a stable warning ID, the affected project or item, the reason, and age and threshold values in seconds. A delivered warning is not sent again on every scheduler tick. A dashboard dismissal persists until the warning condition changes, such as a new lane entry or a new scheduler cause.

Unstarted GitHub checks

tracker.github_unstarted_check_threshold_seconds controls when a GitHub check that remains queued without a started_at timestamp appears as unstarted. The 15-minute default is intentionally non-zero because a cold or busy runner pool can legitimately take several minutes to pick up a job; setting the threshold too low would turn ordinary queueing into an operator-facing signal.

Codex deliverable elicitation

codex.deliverable_elicitation_allowlist declares exact MCP mutation tuples that an unattended Codex worker may confirm for its configured deliverable. The same field is available under agents.backends[].options for a routed Codex backend. It defaults to an empty list. Each entry requires server, tool, and repository; server-only entries are invalid.

codex:
  approval_policy: never
  deliverable_elicitation_allowlist:
    - server: codex_apps
      tool: github.create_pull_request
      repository: digitaldrywood/detent
    - server: codex_apps
      tool: github.update_pull_request
      repository: digitaldrywood/detent
    - server: codex_apps
      tool: github.add_comment_to_issue
      repository: digitaldrywood/detent
    - server: codex_apps
      tool: github.update_issue_comment
      repository: digitaldrywood/detent
    - server: codex_apps
      tool: github.update_issue
      repository: digitaldrywood/detent
    - server: codex_apps
      tool: github.create_issue
      repository: digitaldrywood/detent

The allowlist is a narrow exception to codex.approval_policy, not a broader approval mode. With either never or on-request, Detent accepts an MCP elicitation only when it correlates to exactly one pending tool call on the same server, thread, and turn; the correlated tool is a supported workflow deliverable mutation; and both its repository argument and the corresponding active work-item repository match the configured tuple. Pull-request mutations match the linked pull-request repository. Issue and Workpad mutations match the tracked issue repository and are accepted only when it is also the deliverable repository. Detent declines these direct Codex issue mutations when the linked pull request belongs to another repository because MCP elicitation cannot rewrite the already-pending arguments through the visibility-aware publication protector. Connector-backed issue writes remain available and apply that protector before sending content. Pull-request and issue creation and updates use repository_full_name; issue comment creation and updates use repo_full_name. Missing or ambiguous correlation, other mutation tools including github.delete_issue, and repository mismatches are declined. Command, file-change, permission, and user-input requests retain their existing handling.

Allowlisting github.create_issue authorizes issue creation only on the workflow's own tracked repository. This supports workers filing scoped issues they discover while delivering the current work item; it does not authorize issue deletion, transfer, or creation in any other repository. Those mutations remain non-deliverable.

Allowlisting github.update_issue permits its full exposed mutation surface: title, body, state, assignees, milestone, and labels may all be replaced. The allowlist correlates and constrains the repository, but does not constrain the other approved arguments.

With approval_policy: never and an empty allowlist, non-browser MCP elicitations are declined exactly as before. chrome-devtools empty-form tool approval remains a separate built-in behavior and is unchanged by this list.

Generated field reference

The generated block reflects the YAML-tagged Go structs reachable from the project config. It reads effective defaults through the real normalization path and derives validation text by exercising boundary values through the real loader and validators. “None” means no field-level rule was surfaced; subsystem interactions can still constrain a value.

Run go generate ./... after changing the config structs, defaults, or validators. make check compares the committed generated artifacts with fresh rendering and fails on drift.

budget.per_issue_max_usd and agent.budget.per_issue_max_usd are estimate-based admission backstops, not mid-session spend ceilings. Detent checks cumulative issue spend plus a preflight session estimate before starting the agent. Actual session cost can exceed that estimate and the configured backstop. Once the next projected dispatch would exceed the backstop, Detent parks the issue on a hard hold until an operator raises or disables the cap, or explicitly retries after resetting the hold. refusal_cooldown_seconds applies only to resettable budget pacing and never clears a per-issue hard hold.

Key Type Default Required Validation
active_hours object see child fields Conditional .timezone: is required when active_hours is set.timezone: must be a valid IANA timezone.windows: must contain at least one recurring window
active_hours.timezone string none No None
active_hours.windows list<string> [] No values: must use - :-:
agent object see child fields No None
agent.auto_promote object see child fields No None
agent.auto_promote.allowed_issue_labels list<string> [] No labels must not be blank
agent.auto_promote.enabled boolean false No None
agent.auto_promote.gate_wait_state string "source" No must be one of source, review
agent.auto_promote.gate_wait_timeout_action string "human_review" No must be one of merge, human_review
agent.auto_promote.gate_wait_timeout_seconds integer 3600 No must be greater than 0
agent.auto_promote.optout_label string none No None
agent.auto_promote.pass_state string "Merging" No None
agent.auto_promote.quiet_seconds integer 600 No must be greater than or equal to 0
agent.auto_promote.rework_state string "Rework" No None
agent.auto_promote.source_state string "Human Review" No None
agent.budget object see child fields No None
agent.budget.billing_mode string none No must be one of metered, subscription
agent.budget.enabled boolean false No None
agent.budget.override_max_duration_seconds integer 172800 No must be greater than 0
agent.budget.override_max_multiplier number 3 No must be greater than 0
agent.budget.per_day_max_usd number 50 No must be greater than 0
agent.budget.per_issue_max_usd number 5 No must be greater than 0
agent.budget.pricing_path string "priv/pricing/models.yaml" No is required
agent.budget.refusal_cooldown_seconds integer 3600 No must be greater than or equal to 0
agent.checkpoint_interval_ms integer 0 No must be greater than or equal to 0
agent.dispatch_priority_by_label list<string> [] No labels must not be blank
agent.dispatch_priority_by_state list<string> [] No state names must be uniquestate names must not be blank
agent.effort object see child fields No None
agent.effort.code string none No must be one of low, medium, high, xhigh, max, ultracode
agent.effort.merge string none No must be one of low, medium, high, xhigh, max, ultracode
agent.effort.rework string none No must be one of low, medium, high, xhigh, max, ultracode
agent.experimental_thread_resume boolean true No None
agent.failure_breaker object see child fields No None
agent.failure_breaker.cooldown_seconds integer 3600 No must be greater than 0
agent.failure_breaker.same_class_limit integer 5 No must be greater than 0
agent.failure_breaker.window_seconds integer 3600 No must be greater than 0
agent.followups object see child fields No None
agent.followups.enabled boolean true No None
agent.instructions_by_state mapping<string, string> {} No state "invalid" must reference a configured workflow state
agent.instructions_by_transition mapping<string, mapping<string, string>> {} No source state "invalid" must reference a configured workflow state
agent.knowledge object see child fields No None
agent.knowledge.enabled boolean true No None
agent.knowledge.max_bytes integer 65536 No None
agent.knowledge.sources list<object> [] No None
agent.knowledge.sources[].name string none No None
agent.knowledge.sources[].path string none Conditional must not be blank
agent.lessons object see child fields No None
agent.lessons.enabled boolean false No None
agent.lessons.max_entries integer 50 No must be greater than 0
agent.lessons.path string ".detent/lessons.md" No must be a relative path inside the workspace
agent.lessons.postmortem_max_tokens integer 1024 No must be greater than 0
agent.lessons.recall_n integer 10 No must be greater than or equal to 0
agent.lifetime_limit_override_label string "allow-lifetime-limit" No None
agent.lifetime_session_limit integer 120 No must be greater than or equal to 0
agent.lifetime_token_limit integer 750000000 No must be greater than or equal to 0
agent.max_concurrent_agents integer 10 No must be greater than 0
agent.max_concurrent_agents_by_state mapping<string, integer> {} No limits must be positive integers
agent.max_retry_backoff_ms integer 300000 No must be greater than 0
agent.max_session_context_multiplier number 0 No must be greater than or equal to 0
agent.max_session_duration_ms integer 7200000 No must be greater than or equal to 0
agent.max_session_token_override_field string none No None
agent.max_session_token_override_label string none No None
agent.max_session_tokens integer 0 No must be greater than or equal to 0
agent.max_turn_duration_ms integer 0 No must be greater than or equal to 0
agent.max_turns integer 20 No must be greater than 0
agent.merge_fast_path object see child fields No None
agent.merge_fast_path.enabled boolean true No None
agent.merge_fast_path.fairness_age_seconds integer 7200 No must be greater than 0
agent.merge_worker_max_duration_ms integer 21600000 No must be greater than 0
agent.merge_worker_startup_timeout_ms integer 240000 No must be greater than 0
agent.no_progress_spend_limit_usd number 3 No must be greater than or equal to 0
agent.no_progress_timeout_ms integer 5400000 No must be greater than or equal to 0
agent.no_progress_token_limit integer 25000000 No must be greater than or equal to 0
agent.output_truncation object see child fields No None
agent.output_truncation.max_bytes integer 0 No must be greater than or equal to 0
agent.overload_retry_delay_ms integer 45000 No must be greater than 0
agent.prioritize_unblockers boolean true No None
agent.rate_window_pacing object see child fields No None
agent.rate_window_pacing.floor_percent number 20 No must be greater than 0 and less than or equal to 100
agent.rate_window_pacing.mode string "proportional" No must be one of proportional, off, floor
agent.rate_window_pacing.stale_after_seconds integer 900 No must be greater than 0
agent.resume_orphaned_sessions boolean true No None
agent.shutdown object see child fields No None
agent.shutdown.drain_timeout_ms integer 75000 No must be greater than or equal to 0
agent.skills object see child fields No None
agent.skills.creation object see child fields No None
agent.skills.creation.enabled boolean true No None
agent.skills.creation.max_drafts_per_run integer 1 No must be greater than 0
agent.skills.enabled boolean true No None
agent.skills.max_skills_in_prompt integer 100 No must be greater than 0
agent.skills.path string ".detent/skills" No must be a relative path inside the workspace
agent.stop_run object see child fields No None
agent.stop_run.target_state string "Blocked" No must be included in tracker.lanes with role holding
agents object see child fields No None
agents.backends list<object> [] No None
agents.backends[].command string none Conditional is required
agents.backends[].disabled boolean false when configured No None
agents.backends[].id string none Conditional is required
agents.backends[].kind string none Conditional is required
agents.backends[].options mapping see child fields No None
agents.backends[].options.allowed_tools list<string> [] for Claude Code No None
agents.backends[].options.approval_policy string or mapping {"reject":{"mcp_elicitations":true,"rules":true,"sandbox_approval":true}} for Codex No None
agents.backends[].options.deliverable_elicitation_allowlist list<object> [] for Codex Conditional values.repository must be owner/namevalues.server is requiredvalues.tool is required
agents.backends[].options.deliverable_elicitation_allowlist[].repository string none No None
agents.backends[].options.deliverable_elicitation_allowlist[].server string none No None
agents.backends[].options.deliverable_elicitation_allowlist[].tool string none No None
agents.backends[].options.disallowed_tools list<string> [] for Claude Code No None
agents.backends[].options.effort string none for Claude Code No must be one of low, medium, high, xhigh, max, ultracode
agents.backends[].options.extra_args list<string> [] for Claude Code No None
agents.backends[].options.include_partial_messages boolean false for Claude Code No None
agents.backends[].options.model_provider string none for Codex No must be a sanitized label containing only letters, numbers, dots, underscores, or hyphens
agents.backends[].options.permission_mode string "bypassPermissions" for Claude Code No must be one of default, acceptEdits, bypassPermissionsmust not be plan for unattended workers
agents.backends[].options.read_timeout_ms integer 5000 for Codex No must be greater than or equal to 0
agents.backends[].options.service_tier string none for Codex No None
agents.backends[].options.session_dir string none for Pi No None
agents.backends[].options.shell string platform default shell No None
agents.backends[].options.stall_timeout_ms integer Codex: 300000; Claude Code: 0; Pi: 0 No must be greater than or equal to 0
agents.backends[].options.thinking_level string none for Pi No must be one of off, minimal, low, medium, high, xhigh, max
agents.backends[].options.thread_sandbox string "workspace-write" for Codex No None
agents.backends[].options.tools list<string> ["read","bash","edit","write","grep","find","ls"] for Pi No must contain only Pi built-in tools: read, bash, edit, write, grep, find, ls
agents.backends[].options.turn_sandbox_policy mapping<string, value> {} for Codex No None
agents.backends[].options.turn_timeout_ms integer Codex: 3600000; Claude Code: 0; Pi: 0 No must be greater than or equal to 0
agents.backends[].protocol string none No must be app-server for codexmust be headless for claude_codemust be rpc for pi_agent
agents.backends[].provider string none No must be a sanitized label containing only letters, numbers, dots, underscores, or hyphens
agents.routes list<object> [] No None
agents.routes[].backend string none Conditional is requiredmust reference a configured backend
agents.routes[].default boolean false when configured No None
agents.routes[].disabled boolean false when configured No None
agents.routes[].model string none No None
agents.routes[].model_field string none No None
agents.routes[].name string none No None
agents.routes[].role string none No None
agents.routes[].selector object see child fields No None
agents.routes[].selector.and list<mapping> [] No None
agents.routes[].selector.assignee_in list<string> [] No None
agents.routes[].selector.author_in list<string> [] No None
agents.routes[].selector.fields list<object> [] No None
agents.routes[].selector.fields[].name string none No None
agents.routes[].selector.fields[].value string none No None
agents.routes[].selector.labels object see child fields No None
agents.routes[].selector.labels.exclude list<string> [] No None
agents.routes[].selector.labels.include list<string> [] No None
agents.routes[].selector.or list<mapping> [] No None
agents.routes[].selector.priority_in list<integer> [] No values must be integers 1 through 4
backlog_admission object see child fields No None
backlog_admission.authors object see child fields No None
backlog_admission.authors.allow list<string> [] No None
backlog_admission.authors.allow_association list<string> [] No requires author association, but tracker.kind hub_native cannot supply itvalues must be one of OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, FIRST_TIME_CONTRIBUTOR, NONE
backlog_admission.auto_admit boolean false No None
backlog_admission.auto_admit_by_label mapping<string, boolean> {} No None
backlog_admission.auto_admit_min_confidence number 0.9 No None
backlog_admission.criteria_section string none Conditional is required
backlog_admission.effort_file string "WORKFLOW.md" No must be WORKFLOW.md or AGENTS.md
backlog_admission.effort_section string none Conditional is required when require_effort is true
backlog_admission.enabled boolean false No None
backlog_admission.exclude_labels list<string> [] No None
backlog_admission.max_candidates_per_run integer 50 No must be greater than 0
backlog_admission.max_open_proposals integer 10 No must be greater than 0
backlog_admission.max_proposals_per_run integer 3 No must be greater than 0
backlog_admission.proposal_expiry_days integer 7 No must be greater than 0
backlog_admission.require_effort boolean false No None
backlog_admission.schedule string "*/15 * * * *" No must be a valid five-field cron expression
backlog_admission.sources object see child fields No must configure at least one selector
backlog_admission.sources.labels list<string> [] No requires candidate selector labels, but tracker.kind hub_native does not declare complete label reads
backlog_admission.sources.states list<string> [] No requires candidate selector states, but tracker.kind hub_native does not declare itvalues must differ from target_statevalues must name a configured workflow state
backlog_admission.sources.untracked boolean false No requires candidate selector untracked, but tracker.kind hub_native does not provide github label status drift
backlog_admission.target_state string none Conditional is requiredmust name a configured workflow state
budget object see child fields No None
budget.billing_mode string none No must be one of metered, subscription
budget.enabled boolean false No None
budget.override_max_duration_seconds integer 172800 No must be greater than 0
budget.override_max_multiplier number 3 No must be greater than 0
budget.per_day_max_usd number 50 No must be greater than 0
budget.per_issue_max_usd number 5 No must be greater than 0
budget.pricing_path string "priv/pricing/models.yaml" No is required
budget.refusal_cooldown_seconds integer 3600 No must be greater than or equal to 0
codex object see child fields No agents.backends.protocol must be app-server for codex
codex.approval_policy string or mapping {"reject":{"mcp_elicitations":true,"rules":true,"sandbox_approval":true}} No None
codex.command string "codex app-server" No is required
codex.deliverable_elicitation_allowlist list<object> [] No None
codex.deliverable_elicitation_allowlist[].repository string none No must be owner/name
codex.deliverable_elicitation_allowlist[].server string none Conditional is required
codex.deliverable_elicitation_allowlist[].tool string none Conditional is required
codex.model_provider string none No must be a sanitized label containing only letters, numbers, dots, underscores, or hyphens
codex.read_timeout_ms integer 5000 No must be greater than 0
codex.service_tier string none No None
codex.shell string platform default shell No None
codex.stall_timeout_ms integer 300000 No must be greater than or equal to 0
codex.thread_sandbox string "workspace-write" No None
codex.turn_sandbox_policy mapping<string, value> {"type":"workspaceWrite"} No None
codex.turn_timeout_ms integer 3600000 No must be greater than 0
deliverable object see child fields No warning: release.enabled: release automation is unavailable for tracker.kind hub_native, including deliverable.github_pull_request: true; requires tracker.kind github
deliverable.github_pull_request boolean false No None
deliverable.kind string "pull_request" No must be one of pull_request, artifact
deliverable.merge_method string "squash" No must be one of squash, merge, rebase
deliverable.output_root string none No None
deliverable.review_url string none No None
dependencies object see child fields No None
dependencies.source string "merged" No must be one of merged, native_only
gate object see child fields No None
gate.approval_label string "human-approved" No None
gate.artifact object see child fields No None
gate.artifact.pass_statuses list<string> ["approved","complete","completed","pass","passed","valid"] No None
gate.artifact.rework_statuses list<string> ["changes_requested","failed","invalid","rework"] No None
gate.artifact.status_field string "validation_status" No None
gate.artifact.wait_statuses list<string> ["pending","review","reviewing","waiting"] No None
gate.automated_review string "required" No must be one of required, optional, off
gate.ci_failure_action string "rework" No must be one of skip, rework
gate.ci_trigger_label string none No None
gate.ci_trigger_label_stagger_seconds integer none No must be greater than 0
gate.kind string "command" No must be one of command, human_review, artifact
gate.landing_mode string "per_landing" No must be per_landing
gate.local_status string none No None
gate.require_automated_review boolean true No None
gate.required_status_checks list<string> [] No None
gate.run string "make check" No None
gate.security_audit object see child fields No None
gate.security_audit.block_on list<string> ["p1","p2"] No severities must be p1, p2, or p3
gate.security_audit.enabled boolean true No None
gate.security_audit.max_attempts integer 3 No must be greater than 0
gate.security_audit.max_diff_bytes integer 262144 No must be greater than 0
gate.security_audit.model string none No None
gate.security_audit.turn_timeout_ms integer 1200000 No must be greater than 0
gate.transient_ci_retry_limit integer 2 No must be greater than or equal to 0
gate.validator object see child fields No None
gate.validator.block_on list<string> ["p1"] No None
gate.validator.enabled boolean false No None
gate.validator.max_attempts integer 3 No must be greater than 0
gate.validator.max_inline_diff_bytes integer 65536 No must be greater than or equal to 0
gate.validator.min_score number 0.8 No must be greater than 0 and less than or equal to 1
gate.validator.model string none No None
gate.validator.turn_timeout_ms integer 0 No must be greater than or equal to 0
gate.validator.unverified_criteria string "rework" No must be rework or pass-with-disclosure
hooks object see child fields No None
hooks.after_create string none No None
hooks.after_run string none No None
hooks.before_remove string none No None
hooks.before_run string none No None
hooks.runner_setup string none No must be a repository-relative file path
hooks.shell string platform default shell No None
hooks.timeout_ms integer 60000 No must be greater than 0
identity object see child fields No None
identity.assignee_required boolean false No None
identity.github_login string none No None
identity.name string none Conditional must not be blank
identity.owner_field string none No must be blank when identity.ownership_mode is assignee
identity.ownership_mode string none No identity.owner_field must be blank when identity.ownership_mode is assigneemust be one of assignee, field
intake object see child fields Conditional tracker.repository is required for intake sources
intake.sources list<object> [] No requires tracker.kind githubvalues path filters require a scheduled source
intake.sources[].creates object see child fields No None
intake.sources[].creates.body string "{details}" when configured No None
intake.sources[].creates.labels list<string> [] No None
intake.sources[].creates.status string "Backlog" when configured No must name a configured tracker state
intake.sources[].creates.title string "[{source}] {summary}" when configured No None
intake.sources[].cron string none No None
intake.sources[].dedupe_by string "fingerprint" when configured No None
intake.sources[].enabled boolean true when configured No None
intake.sources[].exclude_paths list<string> [] No None
intake.sources[].kind string none Conditional is required
intake.sources[].match string none No must use field:value syntax
intake.sources[].name string none Conditional is requiredmust contain only lowercase letters, numbers, dots, underscores, or hyphens
intake.sources[].paths list<string> [] No None
intake.sources[].scan string none No None
intake.sources[].secret string none Conditional is required for webhook sources
observability object see child fields No None
observability.dashboard_enabled boolean true No None
observability.dispatch_stall_threshold_seconds integer 7200 No must be greater than 0
observability.efficiency object see child fields No None
observability.efficiency.anomaly_dwell_multiple number 3 No must be greater than 0
observability.efficiency.anomaly_sessions_multiple number 3 No must be greater than 0
observability.efficiency.anomaly_tokens_multiple number 3 No must be greater than 0
observability.otlp object see child fields No None
observability.otlp.endpoint string none No must be an absolute http or https URL
observability.otlp.headers mapping<string, string> {} No None
observability.otlp.service_name string "detent" No None
observability.otlp.timeout_ms integer 5000 No None
observability.park_review_threshold integer 3 No must be greater than 0
observability.refresh_ms integer 1000 No must be greater than 0
observability.render_interval_ms integer 16 No must be greater than 0
observability.staleness object see child fields No None
observability.staleness.enabled boolean true No None
observability.staleness.human_gate_rearm_hours integer 168 No must be greater than 0
observability.staleness.lane_reentry_window_hours integer 168 No must be greater than 0
observability.staleness.lanes list<object> [{"State":"Human Review","ThresholdHours":72,"HumanGate":true},{"State":"Blocked","ThresholdHours":48,"HumanGate":false},{"State":"Merging","ThresholdHours":2,"HumanGate":false}] No None
observability.staleness.lanes[].human_gate boolean true No None
observability.staleness.lanes[].state string "Human Review" Conditional is required
observability.staleness.lanes[].threshold_hours integer 72 No must be greater than 0
observability.staleness.no_completion_hours integer 24 No must be greater than 0
observability.staleness.no_merge_hours integer 12 No must be greater than 0
observability.staleness.repeated_decision_benign_reasons list<string> ["already_running","blocked_by_dependency","github_rest_capacity_paused","github_rest_recovery","global_capacity_full","outside_active_window","project_capacity_full","provider_rate_window_backpressure","ready_merge_control_limit"] No contains "invalid", which does not match a scheduler-emitted reason; emitted reasons include ["already_running" "authorization_selector_declined" "blocked_by_dependency" "github_rest_capacity_paused" "github_rest_recovery" "global_capacity_full" "outside_active_window" "provider_rate_window_backpressure"]contains "todo", which does not match a scheduler-emitted reason; emitted reasons include ["already_running" "authorization_selector_declined" "blocked_by_dependency" "github_rest_capacity_paused" "github_rest_recovery" "global_capacity_full" "outside_active_window" "provider_rate_window_backpressure"]
observability.staleness.repeated_decision_count integer 20 No must be greater than 0
observability.staleness.repeated_window_hours integer 24 No must be greater than 0
observability.staleness.webhook object see child fields No None
observability.staleness.webhook.headers mapping<string, string> {} No None
observability.staleness.webhook.timeout_ms integer 5000 No None
observability.staleness.webhook.url string none No must be an absolute http or https URL
observability.stranded_active_threshold_seconds integer 600 No must be greater than 0
operator object see child fields No None
operator.actions list<string> ["return_retired_parks","clear_closed_dependencies","restore_stuck_merging"] No must contain only return_retired_parks, clear_closed_dependencies, restore_stuck_merging, merge_when_wedged
operator.merge_wedge_seconds integer 7200 No must be greater than 0
plan object see child fields No agents.backends.options.permission_mode must not be plan for unattended workers
plan.approval_label string "plan-approved" No None
plan.enabled boolean false No None
plan.review string "human" No must be one of human, automated, both
plan.stop string "Plan Review" No None
polling object see child fields No None
polling.conditional boolean true No None
polling.interval_ms integer 120000 No must be at least 60000must be greater than 0
polling.refresh_failure_threshold integer 3 No must be greater than 0
recovery object see child fields No None
recovery.terminal_attempt_retry_limit integer 3 No must be greater than or equal to 0
release object see child fields No warning: release.enabled: release automation is unavailable for tracker.kind hub_native, including deliverable.github_pull_request: true; requires tracker.kind github
release.enabled boolean false No release.max_age_hours must be greater than 0 when release.enabled is truerelease.min_merged_issues must be greater than 0 when release.enabled is truerelease.require_green_ci must be true when release.enabled is truetracker.repository must be owner/name when release.enabled is true
release.flaky_check_names list<string> [] No must not be empty when release.rerun_flaky_once is true
release.max_age_hours integer 24 No must be greater than 0 when release.enabled is true
release.min_merged_issues integer 5 No must be greater than 0 when release.enabled is true
release.require_green_ci boolean true No must be true when release.enabled is true
release.required_check_names list<string> [] No None
release.rerun_flaky_once boolean false No release.flaky_check_names must not be empty when release.rerun_flaky_once is true
release.version_bump string "auto" No must be auto
retro object see child fields No None
retro.allow_public_cross_project_details boolean false No None
retro.daily_issue_cap integer 3 when configured No must be greater than 0
retro.enabled boolean false No requires tracker.kind github
retro.fallback_threshold integer 3 when configured No must be at least 2
retro.labels list<string> ["retro"] when configured No None
retro.lookback_days integer 7 when configured No must be greater than 0
retro.min_occurrences integer 2 when configured No must be at least 2
retro.product_repository string "digitaldrywood/detent" when configured No must be owner/name
retro.receipt_baseline_multiple number 4 when configured No must be greater than 1
retro.schedule string "0 3 * * *" when configured No must be a valid five-field cron expression
retro.single_occurrence_severity string "critical" when configured No must be one of info, warning, high, critical
retro.target_state string "Backlog" when configured No must name a configured tracker state
review object see child fields No agent.auto_promote.gate_wait_state must be one of source, review
review.human boolean false No None
routines list<object> [] No requires tracker.kind github
routines[].labels list<string> [] Conditional labels must not be blank
routines[].max_findings_per_run integer 3 when configured No must be greater than 0
routines[].max_open_findings integer 10 when configured No must be greater than 0
routines[].name string none No must be a sanitized label containing only letters, numbers, dots, underscores, or hyphens
routines[].prompt string none Conditional is required
routines[].schedule string none Conditional is requiredmust be a valid five-field cron expression
routines[].target_state string "Todo" when configured No must name a configured workflow state
runners object see child fields No None
runners.profile string none No must name a declared runner profile
runners.profiles mapping<string, mapping> {} No names must be lowercase ASCII tokens of at most 64 characters
runners.profiles.<name> object see child fields No None
runners.profiles.<name>.machine_id string none No None
runners.profiles.<name>.required_tags list<string> [] No None
runners.profiles.<name>.runner_id string none No None
schedule_ownership object see child fields No None
schedule_ownership.backend string "github_ref" No must be github_ref
schedule_ownership.branch string "detent-schedule-coordination" No must be a valid branch name
schedule_ownership.enabled boolean false No must be true when scheduled work is configured; add a schedule_ownership block with enabled: true and a shared key
schedule_ownership.endpoint string none No None
schedule_ownership.heartbeat_seconds integer 300 No must be greater than zeromust leave more than twice max_clock_skew_seconds before lease expiry
schedule_ownership.key string none Conditional is required when enabled is true
schedule_ownership.lease_seconds integer 900 No must be greater than zero
schedule_ownership.max_clock_skew_seconds integer 15 No must be less than half lease_secondsmust not be negative
schedule_ownership.repository string none No must use owner/name syntaxnative schedule ownership requires an explicit schedule_ownership.repository to opt into GitHub coordination
schedule_ownership.retry_seconds integer 120 No must be greater than zero
server object see child fields No None
server.board_snapshot_stale_after_seconds integer 900 No must be greater than 0
server.host string "127.0.0.1" No is required
server.kanban object see child fields No None
server.kanban.allowed_transitions mapping<string, list<string>> {} No None
server.kanban.issue_state_field_id integer 0 No must be greater than 0 when set
server.kanban.mode string "read_only" No must be one of read_only, integration
server.kanban.show_blocked_alerts boolean false No None
server.port integer none No must be greater than or equal to 0
tracker object see child fields No intake.sources[].creates.status must name a configured tracker stateretro.target_state must name a configured tracker state
tracker.api_key string none Conditional or GitHub App credentials are required for github
tracker.assignee string none No None
tracker.authorization object see child fields No None
tracker.authorization.and list<mapping> [] No None
tracker.authorization.assignee_in list<string> [] No None
tracker.authorization.author_in list<string> [] No None
tracker.authorization.fields list<object> [] No None
tracker.authorization.fields[].name string none Conditional must not be blank
tracker.authorization.fields[].value string none Conditional must not be blank
tracker.authorization.labels object see child fields No None
tracker.authorization.labels.exclude list<string> [] No None
tracker.authorization.labels.include list<string> [] No None
tracker.authorization.or list<mapping> [] No None
tracker.authorization.priority_in list<integer> [] No values must be integers 1 through 4
tracker.auto_provision boolean true No None
tracker.blocked_recovery object see child fields No None
tracker.blocked_recovery.breaker_cooldown_seconds integer 86400 No must be greater than or equal to 0
tracker.blocked_recovery.enabled boolean false Conditional tracker.blocked_recovery.reason_codes must not be empty when tracker.blocked_recovery.enabled is truetracker.blocked_recovery.source_states must not be empty when tracker.blocked_recovery.enabled is truetracker.blocked_recovery.target_state is required when tracker.blocked_recovery.enabled is truetracker.lanes with role active must include tracker.blocked_recovery.target_state when tracker.blocked_recovery.enabled is true
tracker.blocked_recovery.reason_codes list<string> ["merge_conflict","stale_base","missing_current_head_ci"] No must contain only merge_conflict, stale_base, missing_current_head_cimust not be empty when tracker.blocked_recovery.enabled is true
tracker.blocked_recovery.source_states list<string> ["blocked"] No must not be empty when tracker.blocked_recovery.enabled is truestate names must be uniquestate names must not be blank
tracker.blocked_recovery.target_state string "Rework" Conditional is required when tracker.blocked_recovery.enabled is truetracker.lanes with role active must include tracker.blocked_recovery.target_state when tracker.blocked_recovery.enabled is true
tracker.blocker_auto_promote object see child fields No None
tracker.blocker_auto_promote.blocker_states list<string> ["backlog","blocked","human review"] No state names must be uniquestate names must not be blank
tracker.blocker_auto_promote.enabled boolean false Conditional tracker.blocker_auto_promote.target_state is required when tracker.blocker_auto_promote.enabled is true
tracker.blocker_auto_promote.source_states list<string> [] No state names must be uniquestate names must not be blank
tracker.blocker_auto_promote.target_state string "Todo" Conditional is required when tracker.blocker_auto_promote.enabled is true
tracker.claims object see child fields No None
tracker.claims.enabled boolean false Conditional tracker.claims.heartbeat_seconds must be greater than 0 when tracker.claims.enabled is truetracker.claims.lease_field must not be blank when tracker.claims.enabled is truetracker.claims.ttl_seconds must be greater than 0 when tracker.claims.enabled is true
tracker.claims.heartbeat_seconds integer 0 No must be greater than 0 when tracker.claims.enabled is true
tracker.claims.lease_field string none Conditional must not be blank when tracker.claims.enabled is true
tracker.claims.ttl_seconds integer 0 No must be greater than 0 when tracker.claims.enabled is true
tracker.dependency_auto_unblock object see child fields No None
tracker.dependency_auto_unblock.enabled boolean false Conditional tracker.dependency_auto_unblock.source_states must not be empty when tracker.dependency_auto_unblock.enabled is truetracker.dependency_auto_unblock.target_state is required when tracker.dependency_auto_unblock.enabled is truetracker.lanes with role active must include Rework when tracker.dependency_auto_unblock.enabled is true
tracker.dependency_auto_unblock.readiness string "terminal_or_merged" No must be one of terminal, terminal_or_merged
tracker.dependency_auto_unblock.source_states list<string> ["blocked"] No must not be empty when tracker.dependency_auto_unblock.enabled is truestate names must be uniquestate names must not be blank
tracker.dependency_auto_unblock.target_state string "Todo" Conditional is required when tracker.dependency_auto_unblock.enabled is true
tracker.endpoint string "https://api.github.com/graphql" for github; unused otherwise No None
tracker.github_app_id string none Conditional is required for github app
tracker.github_app_installation_id string none Conditional is required for github app
tracker.github_app_private_key string none Conditional or tracker.github_app_private_key_path is required for github app
tracker.github_app_private_key_path string none Conditional tracker.github_app_private_key or tracker.github_app_private_key_path is required for github app
tracker.github_graphql_min_remaining_reserve integer 1000 No must be greater than 0
tracker.github_graphql_warn_remaining integer 500 No must be greater than 0
tracker.github_rest_debug_logging boolean false No None
tracker.github_rest_fanout_max_requests integer 80 No must be greater than or equal to 0
tracker.github_rest_min_remaining_reserve integer 1000 No must be greater than 0
tracker.github_status_source string "project_v2" No None
tracker.github_unstarted_check_threshold_seconds integer 900 No must be greater than 0
tracker.github_webhook_secret string none No None
tracker.http_idle_conn_timeout_ms integer 90000 No must be greater than 0
tracker.http_max_idle_conns integer 100 No must be greater than 0
tracker.http_max_idle_conns_per_host integer 32 No must be greater than 0
tracker.issues list<object> [] No None
tracker.issues[].assigned_to_worker boolean false when configured No None
tracker.issues[].assignee_id string none No None
tracker.issues[].assignees list<string> [] No None
tracker.issues[].author_association string none No None
tracker.issues[].author_id string none No None
tracker.issues[].blocked_by list<object> [] No None
tracker.issues[].blocked_by[].human_completion_ready boolean false when configured No None
tracker.issues[].blocked_by[].human_owned boolean false when configured No None
tracker.issues[].blocked_by[].id string none No None
tracker.issues[].blocked_by[].identifier string none No None
tracker.issues[].blocked_by[].source string none No None
tracker.issues[].blocked_by[].state string none No None
tracker.issues[].blocker_reason string none No None
tracker.issues[].branch_name string none No None
tracker.issues[].child_issues list<object> [] No None
tracker.issues[].child_issues[].human_completion_ready boolean false when configured No None
tracker.issues[].child_issues[].human_owned boolean false when configured No None
tracker.issues[].child_issues[].id string none No None
tracker.issues[].child_issues[].identifier string none No None
tracker.issues[].child_issues[].source string none No None
tracker.issues[].child_issues[].state string none No None
tracker.issues[].closed boolean false when configured No None
tracker.issues[].closed_at mapping none No None
tracker.issues[].closed_reason string none No None
tracker.issues[].comments list<object> [] No None
tracker.issues[].comments[].author_authorized boolean false when configured No None
tracker.issues[].comments[].author_display_name string none No None
tracker.issues[].comments[].author_kind string none No None
tracker.issues[].comments[].author_login string none No None
tracker.issues[].comments[].backend string none No None
tracker.issues[].comments[].body string none No None
tracker.issues[].comments[].can_delete boolean false when configured No None
tracker.issues[].comments[].can_edit boolean false when configured No None
tracker.issues[].comments[].created_at mapping none No None
tracker.issues[].comments[].id string none No None
tracker.issues[].comments[].local boolean false when configured No None
tracker.issues[].comments[].target_type string none No None
tracker.issues[].comments[].updated_at mapping none No None
tracker.issues[].comments[].url string none No None
tracker.issues[].created_at mapping none No None
tracker.issues[].deliverable object none No None
tracker.issues[].deliverable.external_id string none No None
tracker.issues[].deliverable.kind string none No None
tracker.issues[].deliverable.metadata mapping<string, string> {} No None
tracker.issues[].deliverable.path string none No None
tracker.issues[].deliverable.review_url string none No None
tracker.issues[].deliverable.validation_status string none No None
tracker.issues[].description string none No None
tracker.issues[].field_updated_at mapping<string, mapping> {} No None
tracker.issues[].fields mapping<string, string> {} No None
tracker.issues[].id string none No None
tracker.issues[].identifier string none No None
tracker.issues[].issue_contract mapping none No None
tracker.issues[].labels list<string> [] No None
tracker.issues[].metadata mapping<string, string> {} No None
tracker.issues[].model_override string none No None
tracker.issues[].number integer 0 when configured No None
tracker.issues[].pr_association_checked_at mapping see child fields No None
tracker.issues[].pr_association_source string none No None
tracker.issues[].pr_number integer none No None
tracker.issues[].pr_repository string none No None
tracker.issues[].priority integer none No None
tracker.issues[].priority_name string none No None
tracker.issues[].pull_request object none No None
tracker.issues[].pull_request.activity_at mapping none No None
tracker.issues[].pull_request.base_ref string none No None
tracker.issues[].pull_request.base_sha string none No None
tracker.issues[].pull_request.branch_name string none No None
tracker.issues[].pull_request.check_run_count integer 0 when configured No None
tracker.issues[].pull_request.checks list<object> [] No None
tracker.issues[].pull_request.checks[].conclusion string none No None
tracker.issues[].pull_request.checks[].details_url string none No None
tracker.issues[].pull_request.checks[].duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.checks[].failure_detail string none No None
tracker.issues[].pull_request.checks[].id integer 0 when configured No None
tracker.issues[].pull_request.checks[].name string none No None
tracker.issues[].pull_request.checks[].queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.checks[].status string none No None
tracker.issues[].pull_request.checks[].workflow_run_id integer 0 when configured No None
tracker.issues[].pull_request.ci_duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.ci_queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.ci_status string none No None
tracker.issues[].pull_request.codex_review_api_state string none No None
tracker.issues[].pull_request.codex_review_body_severity string none No None
tracker.issues[].pull_request.codex_review_findings list<object> [] No None
tracker.issues[].pull_request.codex_review_findings[].body string none No None
tracker.issues[].pull_request.codex_review_findings[].line integer 0 when configured No None
tracker.issues[].pull_request.codex_review_findings[].path string none No None
tracker.issues[].pull_request.codex_review_findings[].url string none No None
tracker.issues[].pull_request.codex_review_source string none No None
tracker.issues[].pull_request.codex_review_state string none No None
tracker.issues[].pull_request.codex_review_submitted_at mapping none No None
tracker.issues[].pull_request.diff_fingerprint string none No None
tracker.issues[].pull_request.draft boolean false when configured No None
tracker.issues[].pull_request.head_sha string none No None
tracker.issues[].pull_request.hydration_degraded_reason string none No None
tracker.issues[].pull_request.hydration_next_retry_at mapping none No None
tracker.issues[].pull_request.hydration_unavailable_reason string none No None
tracker.issues[].pull_request.labels list<string> [] No None
tracker.issues[].pull_request.latest_codex_review_commit_sha string none No None
tracker.issues[].pull_request.latest_codex_review_state string none No None
tracker.issues[].pull_request.latest_codex_review_submitted_at mapping none No None
tracker.issues[].pull_request.merge_queue_entry object none No None
tracker.issues[].pull_request.merge_queue_entry.base_sha string none No None
tracker.issues[].pull_request.merge_queue_entry.batching object see child fields No None
tracker.issues[].pull_request.merge_queue_entry.batching.max_group_size integer 0 when configured No None
tracker.issues[].pull_request.merge_queue_entry.batching.min_group_size integer 0 when configured No None
tracker.issues[].pull_request.merge_queue_entry.batching.min_group_wait_seconds integer 0 when configured No None
tracker.issues[].pull_request.merge_queue_entry.depth integer 0 when configured No None
tracker.issues[].pull_request.merge_queue_entry.enqueued_at mapping none No None
tracker.issues[].pull_request.merge_queue_entry.estimated_time_to_merge_seconds integer 0 when configured No None
tracker.issues[].pull_request.merge_queue_entry.head_sha string none No None
tracker.issues[].pull_request.merge_queue_entry.id string none No None
tracker.issues[].pull_request.merge_queue_entry.position integer 0 when configured No None
tracker.issues[].pull_request.merge_queue_entry.state string none No None
tracker.issues[].pull_request.merge_queue_entry.url string none No None
tracker.issues[].pull_request.mergeable_state string none No None
tracker.issues[].pull_request.merged_at mapping none No None
tracker.issues[].pull_request.node_id string none No None
tracker.issues[].pull_request.number integer 0 when configured No None
tracker.issues[].pull_request.required_check_failures list<object> [] No None
tracker.issues[].pull_request.required_check_failures[].conclusion string none No None
tracker.issues[].pull_request.required_check_failures[].details_url string none No None
tracker.issues[].pull_request.required_check_failures[].duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.required_check_failures[].failure_detail string none No None
tracker.issues[].pull_request.required_check_failures[].id integer 0 when configured No None
tracker.issues[].pull_request.required_check_failures[].name string none No None
tracker.issues[].pull_request.required_check_failures[].queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.required_check_failures[].status string none No None
tracker.issues[].pull_request.required_check_failures[].workflow_run_id integer 0 when configured No None
tracker.issues[].pull_request.running_checks list<string> [] No None
tracker.issues[].pull_request.slow_checks list<object> [] No None
tracker.issues[].pull_request.slow_checks[].conclusion string none No None
tracker.issues[].pull_request.slow_checks[].details_url string none No None
tracker.issues[].pull_request.slow_checks[].duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.slow_checks[].failure_detail string none No None
tracker.issues[].pull_request.slow_checks[].id integer 0 when configured No None
tracker.issues[].pull_request.slow_checks[].name string none No None
tracker.issues[].pull_request.slow_checks[].queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.slow_checks[].status string none No None
tracker.issues[].pull_request.slow_checks[].workflow_run_id integer 0 when configured No None
tracker.issues[].pull_request.stale_successful_checks list<object> [] No None
tracker.issues[].pull_request.stale_successful_checks[].conclusion string none No None
tracker.issues[].pull_request.stale_successful_checks[].details_url string none No None
tracker.issues[].pull_request.stale_successful_checks[].duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.stale_successful_checks[].failure_detail string none No None
tracker.issues[].pull_request.stale_successful_checks[].id integer 0 when configured No None
tracker.issues[].pull_request.stale_successful_checks[].name string none No None
tracker.issues[].pull_request.stale_successful_checks[].queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.stale_successful_checks[].status string none No None
tracker.issues[].pull_request.stale_successful_checks[].workflow_run_id integer 0 when configured No None
tracker.issues[].pull_request.state string none No None
tracker.issues[].pull_request.status_context_count integer 0 when configured No None
tracker.issues[].pull_request.transient_failed_checks list<object> [] No None
tracker.issues[].pull_request.transient_failed_checks[].conclusion string none No None
tracker.issues[].pull_request.transient_failed_checks[].details_url string none No None
tracker.issues[].pull_request.transient_failed_checks[].duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.transient_failed_checks[].failure_detail string none No None
tracker.issues[].pull_request.transient_failed_checks[].id integer 0 when configured No None
tracker.issues[].pull_request.transient_failed_checks[].name string none No None
tracker.issues[].pull_request.transient_failed_checks[].queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.transient_failed_checks[].status string none No None
tracker.issues[].pull_request.transient_failed_checks[].workflow_run_id integer 0 when configured No None
tracker.issues[].pull_request.unresolved_review_threads list<object> [] No None
tracker.issues[].pull_request.unresolved_review_threads[].body string none No None
tracker.issues[].pull_request.unresolved_review_threads[].line integer 0 when configured No None
tracker.issues[].pull_request.unresolved_review_threads[].path string none No None
tracker.issues[].pull_request.unstarted_check_count integer 0 when configured No None
tracker.issues[].pull_request.unstarted_checks list<object> [] No None
tracker.issues[].pull_request.unstarted_checks[].conclusion string none No None
tracker.issues[].pull_request.unstarted_checks[].details_url string none No None
tracker.issues[].pull_request.unstarted_checks[].duration_seconds integer 0 when configured No None
tracker.issues[].pull_request.unstarted_checks[].failure_detail string none No None
tracker.issues[].pull_request.unstarted_checks[].id integer 0 when configured No None
tracker.issues[].pull_request.unstarted_checks[].name string none No None
tracker.issues[].pull_request.unstarted_checks[].queue_seconds integer 0 when configured No None
tracker.issues[].pull_request.unstarted_checks[].status string none No None
tracker.issues[].pull_request.unstarted_checks[].workflow_run_id integer 0 when configured No None
tracker.issues[].pull_request.url string none No None
tracker.issues[].stage_updated_actor object see child fields No None
tracker.issues[].stage_updated_actor.kind string none No None
tracker.issues[].stage_updated_actor.login string none No None
tracker.issues[].stage_updated_at mapping none No None
tracker.issues[].state string none No None
tracker.issues[].title string none No None
tracker.issues[].unblocker_count integer 0 when configured No None
tracker.issues[].updated_at mapping none No None
tracker.issues[].url string none No None
tracker.issues[].workpad_signal mapping none No None
tracker.kind string none Yes backlog_admission.authors.allow_association requires author association, but tracker.kind hub_native cannot supply itbacklog_admission.sources.labels requires candidate selector labels, but tracker.kind hub_native does not declare complete label readsbacklog_admission.sources.states requires candidate selector states, but tracker.kind hub_native does not declare itbacklog_admission.sources.untracked requires candidate selector untracked, but tracker.kind hub_native does not provide github label status driftintake.sources requires tracker.kind githubis requiredmust be one of github, hub_native; migrate retired trackers to hub_nativeretro.enabled requires tracker.kind githubroutines requires tracker.kind githubwarning: release.enabled: release automation is unavailable for tracker.kind hub_native, including deliverable.github_pull_request: true; requires tracker.kind github
tracker.lanes list<object> [{"name":"Backlog","role":"holding"},{"name":"Human Review","role":"holding"},{"name":"Blocked","role":"holding"},{"name":"Todo","role":"active"},{"name":"In Progress","role":"active"},{"name":"Closed","role":"terminal"},{"name":"Cancelled","role":"terminal"},{"name":"Canceled","role":"terminal"},{"name":"Duplicate","role":"terminal"},{"name":"Done","role":"terminal"}] No agent.stop_run.target_state must be included in tracker.lanes with role holdingwith role active must include Rework when tracker.dependency_auto_unblock.enabled is truewith role active must include tracker.blocked_recovery.target_state when tracker.blocked_recovery.enabled is truewith role holding state names must not be blank
tracker.lanes[].name string none Conditional must not be blank
tracker.lanes[].role string none No must be active, holding or terminal
tracker.local_sqlite object see child fields No None
tracker.local_sqlite.path string none No None
tracker.local_sqlite.project_id string none No None
tracker.priority_map string or mapping {"High":2,"Low":4,"Medium":3,"No priority":null,"Urgent":1} No option names must not be blankranks must be integers 1 through 4 or null
tracker.project_slug string none Conditional is required for github project_v2
tracker.repository string none Conditional is required for intake sourcesmust be owner/name when release.enabled is true
tracker.state_map string or mapping {} No state names must not be blank
tracker.status_field string "Status" No None
tracker.status_label_prefix string "detent:" No None
tracker.status_page_url string "https://www.githubstatus.com" for github; unused otherwise No must be an absolute http or https base URL without credentials, path, query, or fragment
tracker.write_probe_issue string none No None
worker object see child fields No .extra_network_domains: extra network domains must be unique lowercase exact DNS hosts
worker.allow_local_binding boolean global.worker.allow_local_binding, otherwise false No None
worker.compute_rates mapping<string, mapping> {} No None
worker.extra_network_domains list<string> [] No None
worker.github_rest_min_remaining_reserve integer 1250 No must be greater than 0
worker.github_rest_poll_interval_ms integer 60000 No must be greater than or equal to 60000
worker.github_token string top-level github_token No None
worker.github_token_resolution_timeout_ms integer 15000 No must be greater than 0
worker.host_caps mapping<string, integer> {} No .invalid must be greater than 0.invalid must name a configured worker.ssh_hosts entry
worker.host_selection string "least_loaded" No must be least_loaded or preference
worker.max_concurrent_agents_per_host integer none No must be greater than 0
worker.ssh_hosts list<string> [] No must contain unique SSH destinations or localworker.host_caps.invalid must name a configured worker.ssh_hosts entry
workpad object see child fields No None
workpad.structured_only boolean false No None
workspace object see child fields No agent.lessons.path must be a relative path inside the workspaceagent.skills.path must be a relative path inside the workspace
workspace.auto_branch boolean true No None
workspace.cleanup_idle_ttl_ms integer 86400000 No must be greater than 0
workspace.cleanup_sweep_interval_ms integer 600000 No must be greater than 0
workspace.kind string "local_git" No must be one of local_git, filesystem
workspace.output_root string none No None
workspace.root string OS temporary directory + /detent_workspaces No None
workspace.source_root string none No None

Per-change security audit

gate.security_audit.enabled defaults to true for every tracker. The audit reviews each immutable base/head before merge. P1/P2 findings send the change to Rework when confirmed; non-blocking findings remain recorded on the Change or pull request. A potentially P1/P2 lead that cannot be established from the supplied evidence has certainty undetermined, no severity, and an explicit unresolved fact. The same stage verifies it once using the effective model selection's complex model in a fresh session, with all supplied evidence and the original deadline and capacity reservation. Confirmed findings go to Rework; disproved leads remain recorded as rejected with their reasoning. If verification still cannot decide, native completion uses the existing Blocked human_action park, naming the lead, both verdicts and the unresolved fact. Both audit verdicts are retained on the Change. Prior unresolved findings remain blocking when their context is absent; their unresolved severity cannot change. Backend and runner failures are instance failures and do not become findings.

For tracker.kind: hub_native, audits run for both plain git delivery (deliverable.github_pull_request: false, the default) and GitHub pull request delivery (deliverable.github_pull_request: true). Migrating to the native tracker requires no audit opt-in: keep the default or explicitly set gate.security_audit.enabled: true in the approved project policy. GitHub pull request delivery is not required for the audit.

The effective model selection's stages.security_audit selects the backend, model and effort. Codex and Claude use their ordinary authentication, including subscription and metered accounts. Each audit consumes provider tokens and runtime, reported with other stage usage; metered accounts incur their usual provider charges. Native direct landing also audits the integrated base/head before push. A rebase, an earlier batch member or a concurrent base update requires fresh evidence for the resulting commit and incurs another audit.

To disable it, explicitly set the existing project gate policy in detent.yaml (or update that policy through the project's existing configuration API/MCP owner):

gate:
  security_audit:
    enabled: false

An explicit false wins over the default. Repository-owned native policy changes still require approval through the existing trusted-revision policy owner.