Hi, welcome to Detent. If you are reading this as a human, pause here and paste the prompt below into Codex or Claude Code. Detent is meant to be driven from the top down by AI agents, so the fastest way to get moving is to let an agent inspect the repo, interrogate the onboarding runbook, and guide you through the right setup path. You can keep reading by hand too; nobody will revoke your keyboard.
You are onboarding Detent with me. Treat this as an AI-driven project, not a
manual README skim.
Treat https://github.com/digitaldrywood/detent as the canonical Detent source
repository. Do not assume the current working directory is the Detent source
checkout or the target repository being onboarded. Use GitHub as the
first-class Detent documentation source when a verified local checkout is
absent, stale, or not desired. Do not clone Detent by default; cloning is only
an optional fallback when remote reads are unavailable or I explicitly ask for a
local source checkout. Keep the Detent source repository separate from any
target repository being onboarded; Detent may be a reference/source repository,
not the target.
Pin the Detent docs to a concrete canonical commit before relying on them. Run
`DETENT_DOCS_COMMIT="$(gh api repos/digitaldrywood/detent/git/ref/heads/main --jq '.object.sha')"`
and record `DETENT_DOCS_ACCESS_METHOD=github_api`,
`DETENT_DOCS_REPOSITORY=digitaldrywood/detent`, `DETENT_DOCS_REF=main`, and
`DETENT_DOCS_COMMIT` in the initial evidence packet. Also run
`detent --format json version` when the binary is available and report installed
binary version, binary commit, binary build date, and
`DETENT_BINARY_MATCHES_CANONICAL` against `DETENT_DOCS_COMMIT`. If a verified
local Detent checkout is present, you may additionally set `DETENT_SOURCE_ROOT`
and run `git -C "$DETENT_SOURCE_ROOT" fetch origin main:refs/remotes/origin/main`,
`git -C "$DETENT_SOURCE_ROOT" rev-parse HEAD`, and
`git -C "$DETENT_SOURCE_ROOT" rev-parse refs/remotes/origin/main`; report the
local source root, local `HEAD`, canonical `origin/main`, and
`DETENT_SOURCE_MATCHES_CANONICAL`. If the local checkout is absent, stale, or
cannot be proven current, read Detent docs from GitHub at `DETENT_DOCS_COMMIT`
instead of cloning or relying on local files.
From the pinned Detent documentation source, read README.md, AGENTS.md and
CLAUDE.md if present, docs/ONBOARDING.md, CONTRIBUTING.md, build and language
manifests, .github/workflows, install scripts, docs/templates, workflow
examples, and any existing WORKFLOW.md or global.yaml examples. Use
`gh api repos/digitaldrywood/detent/contents/<path>?ref="$DETENT_DOCS_COMMIT"`
or raw GitHub URLs pinned to the same commit. Detent can drive any project with
a clear workflow and validation gate, so use the repository evidence to
identify the stack, tools, and commands instead of starting from one language.
Do not inspect a target repository's
ProjectV2 boards, labels, issues, WORKFLOW.md, validation commands, or runtime
docs until the identity gate below is explicit and confirmed.
Use the pinned Detent documentation source's docs/ONBOARDING.md as the interrogation
guide. First determine which path applies: a new Detent install, an existing
Detent install that must be found and verified, or a new repository/project
being added to an existing Detent install. Distinguish reference repositories from the target repository being onboarded. In Phase 0.5, run
`detent onboarding draft-answers --output pretty` from the target checkout, or
pass `--target-source-root` if you are currently in the Detent source checkout.
When a local Detent checkout is available, also pass
`--detent-source-root "$DETENT_SOURCE_ROOT"` so the draft records source and
binary freshness evidence.
To write the candidate identity record, run
`detent onboarding draft-answers --answers "$ONBOARDING_DIR/answers.env" --write`.
Treat the draft as a candidate, not confirmation. The command should infer and
restate an identity candidate from the current git checkout before asking for
raw answer fields, using only identity-safe local evidence first: `pwd`,
`git rev-parse --show-toplevel`, `git remote get-url origin`, the Detent
documentation source identity, the installed Detent config path, and
registered project ids. If the current working directory is a GitHub checkout
and is not the canonical Detent source checkout, propose it as the target
candidate. If the current working directory is the Detent source checkout, do
not propose Detent as the target unless I explicitly say I am onboarding Detent
itself. Explain that the customer/workstream id is only a stable local
workstream id. Present the candidate in human-facing language first, then show
the `answers.env` representation. Set `IDENTITY_CONFIRMED=true` only after I
confirm the restatement, then run `detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase identity`.
If I volunteer a status-source answer before identity is confirmed, such as
"use label for this repo", preserve it as a pending decision outside
`answers.env`. Restate it as pending in the conversation. For label mode, say:
"I have your status-source choice as label mode. I will keep it pending until
the identity gate validates, then append GITHUB_MODE=label and run the decision
validator without asking again." Do not write `GITHUB_MODE` to `answers.env`
until the identity phase passes; after identity validation succeeds, append
`GITHUB_MODE=label` and run the decision validator without asking again. For
non-label modes, carry the selected `GITHUB_MODE` value the same way.
If the `detent` binary is not installed yet, follow the Detent source README's
Install path or Bootstrap On A New Machine steps 1-3 first, verify the binary
with `detent version`, and defer `detent onboarding validate-answers` until the
binary is available.
For an existing install, find and verify the detent binary, config path, running
service or dashboard, registered projects, GitHub auth, Codex auth, and
read-only doctor status with `detent doctor --port 0` before recommending
changes. Do not pass `--allow-write-probes` until the mutation gate passes and I
explicitly confirm mutation. For a new install, follow the bootstrap flow and
verify each step before moving on. For adding a project, treat existing
registered projects as examples only; do not reuse tracker mode, status
namespace, validation gate, dashboard bind, workspace root, scheduling priority,
auto-promote policy, review policy, or mutation scope unless I explicitly
accept that setting for this customer/project.
Present findings with evidence and ask only the next necessary human decisions.
Ask and record `GITHUB_MODE` explicitly after the identity phase and before
target-specific discovery; never infer ProjectV2, issue-field, or label mode
from existing projects. Recommendations can cite evidence, but they are not
selected answers. Before recommending review, auto-promotion, dependency
unblock, or merging settings, ask in plain English whether I want full
autopilot, a Human Review gate, or conservative/manual approval. If I ask for
maximum automation, map that to `DELIVERY_PROFILE=full_autopilot` and summarize
the behavior before showing `answers.env` fields. Do not recommend
`AUTO_PROMOTE_ENABLED=false` or stopping at Human Review unless I selected
review gate or conservative/manual; do not create, link, mutate, or delete GitHub Projects, issue fields, labels,
issues, PRs, `WORKFLOW.md`, or `global.yaml`, or dispatch agents, until Phase 2
answers are recorded in `answers.env`, `detent onboarding validate-answers`
passes for the selected phase, and I explicitly confirm the mutation step.
Defaults are recommendations only; never execute a defaulted GitHub or config
mutation without my confirmation.
A detent is the catch that holds a moving part at a fixed position until it is deliberately released — the click-stop on a dial, the notch on a ratchet. Detent holds each piece of work at a defined stop on your board and only lets it advance when a gate is cleared.
Detent is status-driven agentic work orchestration, shipped as a single Go
binary, with code as its first proven domain. Today it can use a GitHub
ProjectV2 board as the source of truth, or it can run boardless from a
repository's GitHub issue Status field or repository status labels while
Detent supplies the Kanban view. For every code issue you mark ready it creates
an isolated Git worktree,
dispatches a Codex coding agent against a workflow contract you wrote, runs
your validation gate, opens a pull request, waits for review, and merges through
a serialized train — with all of it live on a web dashboard and a terminal UI.
The same status-to-gated-review-to-done shape is the trajectory for non-code
work: validation gates are now pluggable, while non-git or non-PR deliverables
remain follow-up work described in
Execution Seams.
It is a system, not an agent. You specify the work — the issues, acceptance criteria, review gates, and merge rules — and Detent runs that process with rigor, isolation, and parallelism across many issues at once. The intelligence stays in your spec; the runtime supplies the discipline.
See it for real:
digitaldrywood/detent-orchestration
is Detent's own production config — it dispatches the agents that build Detent
itself. Copy it as a template, and use
Bootstrap On A New Machine
to go from a bare machine to a running board. To onboard a repository, verify an
existing install, or add a new project to an existing Detent host, use the
agent-executable Project Onboarding runbook.
Configured GitHub status is the state machine; ProjectV2 board status, the boardless issue field, or repository status labels drive everything.
- You write the contract. Each project has a
WORKFLOW.md: the tracker binding, board states, the agent prompt, the validation gate, and the review policy. - You mark an issue
Todo. Detent claims it, creates an isolated Git worktree from your source checkout, and dispatches a Codex agent with the contract — moving the issue toIn Progress. - The agent works in its own branch, runs your validation gate, and opens
or updates a PR. Review-gate workflows move the issue to
Human Review; autopilot workflows leave it active withstatus: completein the Workpad. - Gates decide. The workflow decides whether promotion to
Mergingwaits inHuman Review, waits in the active lane, requires a current-head automated PR review, or only needs linked PR + green CI + quiet time. Unresolved feedback sends it toReworkfor another pass. - The merge train is serialized — one rebase, CI-watch, and merge at a
time, so concurrent candidates never invalidate each other's CI — then the
issue is
Done. - One host, many repos.
global.yamlruns multiple projects with weights, priority, pause, and fair scheduling. The web dashboard and terminal UI show live counts, running agents, token / budget / rate-limit state, and board flow.
The full state table, connector model, and merge-train config are in Concepts.
See How Detent compares to Symphony, Copilot, Cursor, Hermes, and OpenClaw.
Detent grew out of OpenAI's Symphony — the
open SPEC.md for orchestrating Codex coding agents from a project board instead
of supervising them interactively ("manage work, not agents"). Symphony ships as
a spec plus an Elixir reference implementation that polls a Linear board.
Detent takes that idea from spec to a shipped system, and diverges where it
counts:
- A product, not a spec. One CGO-free Go binary for macOS, Linux, and
Windows —
go install, Homebrew, or copy a single file. No BEAM service to adapt, nothing to stand up. - GitHub Projects v2, not Linear. Issues, status columns, priorities, labels, blockers, comments, and pull requests are the state machine.
- Multi-project from one host.
global.yamlruns many repositories with weights, priority, pause, and fair scheduling. - Explicit gates + a serialized merge train. CI, optional automated PR
review criteria, and a one-at-a-time
Merginglane, so what lands is always green. - Pluggable validation gates. Code defaults use
make check, CI, and automated review, while workflow authors can choose whether a command gate requires automated PR review or instead only waits for CI and the quiet window. A human approval-label gate is available when the workflow explicitly wants one. - A real operator surface. A live dashboard (charts, trends, timelines,
hover detail, budget and rate-limit state) and terminal UI,
detent doctorpreflight checks, cross-platform config discovery, and a GoReleaser pipeline.
The difference is the interaction model. Autonomy-first tools center a persistent assistant: you talk to an agent, it keeps sessions and memory, picks its own tools, and acts on your behalf — you steer it and course-correct when it drifts. Detent inverts that. You write the issue first — scope, acceptance criteria, tests, dependency order, review policy, merge rule — and the board state decides when it is eligible. The runtime executes your contract in an isolated worktree and will not land the work until the gates you encoded pass. You are not steering an agent; you are running your own engineering process at scale.
Concretely: "add OAuth token rotation" in an autonomy-first tool starts as a prompt and becomes a supervision loop — review the plan, inspect partial edits, redirect when it misses migrations or tests. In Detent it starts as an issue that names the storage change, CLI behavior, migration, rollback, and tests; the worker produces a reviewable PR and does not merge until your gates are green.
The goal is not to replace engineers or hide work behind opaque behavior — it is to scale the judgment of engineers who already have a high bar. The system does not try to be smarter than you; it tries to be as disciplined as you would be, every time, in parallel.
Two choices define Detent's footprint: GitHub Projects as the board and Codex as the coding agent. Both are deliberate.
The reference design Detent grew from polls a Linear board while code, pull requests, and CI live in GitHub — two systems for one unit of work. That split forces you to map Linear issue IDs onto GitHub PR numbers and to read discussion in two places: planning comments in Linear, review comments in GitHub. Detent puts the whole state machine in one system. A GitHub Project is the board; its issues are the work items, its pull requests are the deliverables, and its comments and reviews are where every conversation happens. One ID space, one place to look.
It is also cheaper at the scale where orchestration matters:
- Cost. GitHub Projects has no per-seat charge and ships with repositories most teams already pay for. Linear's Business plan is $16/user/month — about $9,600/year at 50 seats — and its free tier is capped.
- API headroom. Authenticated GitHub REST allows 5,000 requests/hour (more for GitHub Apps); Linear allows 1,500 requests/hour against a complexity budget. A poller driving many repositories wants the larger ceiling.
Detent dispatches agents non-interactively, headless, many at once. The important question is how that mode is metered.
- A ChatGPT plan (Plus, Pro, Business) covers Codex CLI usage including
scripted
codex execautomation, billed against the subscription you already have. - Claude Code keeps interactive terminal and IDE use on subscription usage
limits. Effective June 15, 2026, Anthropic moves headless
claude -p, the Agent SDK, Claude Code GitHub Actions, and third-party Agent SDK apps to a separate monthly Agent SDK credit. That credit is per-user, does not roll over, and overages move to usage credits at standard API rates when enabled.
For an orchestrator that runs agents around the clock in parallel, the Codex subscription is the default that makes the economics work. Detent still supports explicit backend routing, including Claude Code, so operators can choose a backend per role when the limits, auth mode, and isolation trade-offs fit that work.
On Windows, use the package manager that already manages your developer tools. Use Winget when the Detent package is available from the Windows Package Manager community source:
winget install --id DigitalDrywood.Detent --source wingetUse Scoop when you want a user-local install managed from a Scoop bucket:
scoop bucket add digitaldrywood https://github.com/digitaldrywood/scoop-bucket
scoop install detentUse the PowerShell installer for bootstrap, CI images, or machines where you do not want to configure a Windows package manager first:
irm https://raw.githubusercontent.com/digitaldrywood/detent/main/install.ps1 | iexThe PowerShell installer downloads the Windows release archive, verifies the
SHA-256 checksum, installs detent.exe to %LOCALAPPDATA%\detent\bin, and
adds that directory to the user PATH. Set DETENT_INSTALL_DIR to override the
install directory. Winget and Scoop installs also expose detent.exe on PATH; verify any Windows install with detent --version.
Install the latest Linux release with the shell installer:
curl -fsSL https://raw.githubusercontent.com/digitaldrywood/detent/main/install.sh | shThe shell installer downloads the Linux release archive, verifies the SHA-256
checksum, installs detent to /usr/local/bin when writable or
$HOME/.local/bin otherwise, and prints PATH guidance when the chosen install
directory is not already on PATH. Set DETENT_INSTALL_DIR to override the
install directory. Source checkouts can also run the repository-local shell
installer:
./install.shUse a native Linux package when you want apt, dnf, rpm, or another system package workflow to own the binary, removal, and upgrades:
DETENT_VERSION=0.5.2 # release version without leading v
DETENT_ARCH=amd64 # or arm64
curl -LO "https://github.com/digitaldrywood/detent/releases/download/v${DETENT_VERSION}/detent_${DETENT_VERSION}_linux_${DETENT_ARCH}.deb"
sudo apt install "./detent_${DETENT_VERSION}_linux_${DETENT_ARCH}.deb"
detent --versionDETENT_VERSION=0.5.2 # release version without leading v
DETENT_ARCH=amd64 # or arm64
curl -LO "https://github.com/digitaldrywood/detent/releases/download/v${DETENT_VERSION}/detent_${DETENT_VERSION}_linux_${DETENT_ARCH}.rpm"
sudo rpm -Uvh "./detent_${DETENT_VERSION}_linux_${DETENT_ARCH}.rpm"
detent --versionUse the shell installer for a user-local install without sudo, for Linux
distributions that do not use .deb or .rpm, or for bootstrap scripts that
should fall back to go install when a release asset is unavailable.
Use Homebrew on macOS or Linux when you already manage CLI tools with Homebrew:
brew install digitaldrywood/tap/detentUse Go on any platform when you want to build from source instead of using a release archive:
go install github.com/digitaldrywood/detent/cmd/detent@latestAfter installing, check for updates with:
detent update --checkRelease-installer installs can update with detent update; use
detent update --yes for non-interactive automation and
detent update --format json for machine-readable status. The legacy
detent update --json flag remains supported. On Windows, replacement is
staged and completes after the running detent.exe exits. Package-manager
installs should be upgraded by the package manager:
winget upgrade --id DigitalDrywood.Detent
scoop update detent
brew upgrade digitaldrywood/tap/detentNative Linux packages are owned by the system package manager; install a newer
.deb with sudo apt install ./detent_<version>_linux_<arch>.deb, or a newer
.rpm with sudo rpm -Uvh ./detent_<version>_linux_<arch>.rpm or the distro
wrapper you normally use. Go-installed binaries offer an
interactive choice: run
go install github.com/digitaldrywood/detent/cmd/detent@latest, switch to the
checksum-verified release binary, or abort. detent update --yes runs the Go
install command for go-installed binaries; detent update --from-release
switches the detected Go-installed binary to the release asset and pins future
updates to release-binary management. Source builds still print the recommended
command instead of overwriting the binary.
CI runs the Installer Smoke confidence job on Ubuntu and Windows against the
current GitHub Release assets after merges to main, on release tags, on the
nightly schedule, and from manual workflow dispatch. The job runs install.sh
and install.ps1 in release mode, checks checksum output, confirms the
requested install directory and installer lock metadata, then runs
detent update --check and detent update --yes from the release-installer
install.
Release self-updates verify SHA256 checksums fetched from GitHub releases. The
checksum verifier supports detached minisign signature assets named
<checksum>.minisig, but enforcement is gated until the binary embeds the
pinned minisign public key for the release stream. Until that release signing
key is provisioned in #337, update integrity still depends on GitHub TLS plus
the published checksum file.
Requirements:
- Go 1.26 or newer when installing with
go installor building from source. - The OpenAI Codex CLI installed and signed
in, so
codex app-serverruns on the host that dispatches agents. Detent drives every agent through this app-server. Verify withcodex --version. To route selected work to a local Ollama model without Detent code changes, see Local Models With Codex And Ollama. - The Claude Code CLI installed and signed in when routing selected roles to
claude_code. Verify withclaude --version. Detent does not store Claude credentials; it uses the ambientclaudeCLI login or theANTHROPIC_API_KEYenvironment visible to the Detent worker. - The GitHub CLI (
gh) for authentication and GitHub lookups (optional but assumed throughout this guide). - A GitHub token for the selected tracker mode. ProjectV2 mode usually needs
repo,read:org,read:project, and writeproject. Boardless issue-field mode needs repository issue access plus organization issue-field read access; classic PATs userepoandread:org.
Detent uses stable process exit codes so scripts and agents can branch on the failure class.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General or unexpected error |
| 2 | Auth or GitHub token problem |
| 3 | Input validation error |
| 4 | Not found or config conflict |
When the resolved output format is JSON, command failures write one RFC 9457-style problem object to stderr. Human-readable pretty-mode errors are unchanged.
{
"type": "https://detent.dev/errors/project_not_found",
"code": "project_not_found",
"title": "Project not found",
"detail": "project \"ap\" not found",
"exit_code": 4,
"suggested_fix": "available: api, web, infra\ndid you mean \"api\"? see `detent config path`, then retry",
"did_you_mean": ["api"],
"docs_url": "https://detent.dev/docs/cli#project-not-found"
}Envelope fields:
| Field | Required | Meaning |
|---|---|---|
type |
Yes | Stable problem type URL, using the code slug. |
code |
Yes | Stable machine-readable slug. |
title |
Yes | Short human title for the error class. |
detail |
Yes | Specific failure detail. |
exit_code |
Yes | Process exit code for the failure. |
suggested_fix |
No | Actionable next step when Detent has a hint. |
did_you_mean |
No | Candidate correction list when Detent has suggestions. |
docs_url |
No | Documentation URL for the error class. |
Stable JSON error codes:
Cut releases from main by pushing a semver tag:
git tag v0.1.0 && git push origin v0.1.0Tags matching v* trigger the release workflow, which runs GoReleaser and
publishes the GitHub Release archives, checksums, Homebrew formula, and Windows
package-manager manifests. Scoop publishing targets
digitaldrywood/scoop-bucket; Winget publishing pushes to the
digitaldrywood/winget-pkgs fork and opens a pull request against
microsoft/winget-pkgs. GoReleaser generates the manifests during snapshots and
skips publishing when SCOOP_BUCKET_GITHUB_TOKEN or WINGET_GITHUB_TOKEN is
not configured.
CI runs GoReleaser Snapshot after merges to main, on v* release tags, on
pull requests, on the nightly schedule, and from manual workflow dispatch so
release packaging is validated before the PR merge lane and after it lands.
Required branch checks must not pass as path- or event-dependent no-ops on pull
requests when the same check name runs real validation on main.
The quickest compatibility setup is one GitHub ProjectV2 board and one local
repository checkout. New projects can also run boardless: Detent reads and
writes either a repository's organization-level GitHub issue Status field or
repository status labels, then shows workflow visibility in Detent's own
Kanban/dashboard surface.
- Authenticate GitHub access for the mode you want:
# ProjectV2-backed board mode.
gh auth login --scopes "repo,read:org,read:project,project"
# For existing auth:
gh auth refresh -h github.com --scopes "repo,read:org,read:project,project"
gh auth status 2>&1 | rg '\brepo\b'
gh auth status 2>&1 | rg '\bread:org\b'
gh auth status 2>&1 | rg '\bread:project\b'
gh auth status 2>&1 | rg "(^|[[:space:],'\"])project([[:space:],'\"]|$)"
# Boardless issue-field mode with a classic PAT.
gh auth login --scopes "repo,read:org"
gh auth status 2>&1 | rg '\brepo\b'
gh auth status 2>&1 | rg '\bread:org\b'
# Boardless label mode with a classic PAT.
gh auth login --scopes "repo"
gh auth status 2>&1 | rg '\brepo\b'Fine-grained PATs and GitHub Apps should grant Issues repository read/write when Detent will move work or post comments, Pull requests read/checks read for PR gates, Issue Fields organization read for issue-field status discovery, and repository label access for label mode. Issue-field writes use the issue field values API and require issue or pull request repository write permission plus push access to the repository. Label mode uses repository label reads/writes and issue label updates. If Kanban integration mode is enabled in a release that supports it, comment submission also requires issue/PR comment write.
- Choose the GitHub status source.
For the current/default compatibility path, use a GitHub ProjectV2 board. Find
the node id and use the id field, which starts with PVT_, as
tracker.project_slug:
gh project list --owner <org-or-user> --format json --limit 20The gh project list command verifies the token can read ProjectV2 boards.
The write project scope is verified when Detent first performs an intentional
board mutation, such as provisioning fields or editing an issue status.
Detent auto-provisions any missing Status and Priority options on the board
the first time it runs, so you do not have to hand-create every column — but the
option names it creates and reads must match the states in your WORKFLOW.md.
GitHub uses single-select option order as board column order; Detent keeps the
known status options in canonical board order and leaves extra custom options
after the required Detent states.
For issue-field mode, create or reuse an organization-level single-select issue
field named Status and make sure it is available to the repository. GitHub
issue fields are issue-only: linked PR cards in Detent derive status from the
linked issue, not from a PR field. Discover the field and options with:
gh api /orgs/<org>/issue-fields --jq '.[] | select(.name == "Status")'For label mode, create or reuse repository labels for the effective Detent
states. Detent applies tracker.state_map, slugifies the resulting state name,
and prefixes it with tracker.status_label_prefix, which defaults to
detent:. With the default release flow, the required labels are
detent:backlog, detent:todo, detent:in-progress, detent:blocked,
detent:human-review, detent:rework, detent:merging, and detent:done.
With tracker.auto_provision enabled, Detent creates missing repository status
labels for configured workflow states on startup.
Discover existing labels with:
gh api repos/<owner>/<repo>/labels --paginate --jq '.[].name'- Create a
WORKFLOW.mdin the repository you want Detent to work on.
ProjectV2-backed board mode:
---
tracker:
kind: github
github_status_source: project_v2
project_slug: PVT_replace_with_project_id
write_probe_issue: owner/repo#123
http_max_idle_conns: 100
http_max_idle_conns_per_host: 32
http_idle_conn_timeout_ms: 90000
github_graphql_warn_remaining: 500
github_graphql_min_remaining_reserve: 1000
github_rest_min_remaining_reserve: 1000
github_rest_fanout_max_requests: 80
github_rest_debug_logging: false
active_states:
- Todo
- In Progress
- Rework
- Merging
observed_states:
- Backlog
- Human Review
- Blocked
terminal_states:
- Done
- Cancelled
state_map:
Cancelled: Done
priority_map:
Urgent: 1
High: 2
Medium: 3
Low: 4
No priority: null
dependency_auto_unblock:
enabled: false
source_states:
- Blocked
target_state: Todo
readiness: terminal_or_merged
blocker_auto_promote:
enabled: false
blocker_states:
- Backlog
- Blocked
- Human Review
target_state: Todo
polling:
interval_ms: 120000
workspace:
root: /absolute/path/to/detent-workspaces
source_root: /absolute/path/to/project-checkout
auto_branch: true
cleanup_idle_ttl_ms: 86400000
cleanup_sweep_interval_ms: 600000
agent:
max_concurrent_agents: 5
max_turns: 20
max_retry_backoff_ms: 300000
overload_retry_delay_ms: 45000
no_progress_spend_limit_usd: 3
failure_breaker:
same_class_limit: 5
window_seconds: 3600
cooldown_seconds: 3600
resume_orphaned_sessions: true
max_concurrent_agents_by_state:
Merging: 1
dispatch_priority_by_state:
- Merging
- Rework
- In Progress
- Todo
dispatch_priority_by_label:
- bug
- regression
- enhancement
prioritize_unblockers: true
auto_promote:
enabled: false
quiet_seconds: 600
optout_label: requires-human-review
allowed_issue_labels: []
gate_wait_state: review
gate_wait_timeout_seconds: 3600
gate_wait_timeout_action: human_review
rework_limit: 3
skills:
enabled: true
path: .detent/skills
max_skills_in_prompt: 50
creation:
enabled: true
max_drafts_per_run: 1
codex:
command: codex app-server
approval_policy: never
thread_sandbox: workspace-write
turn_sandbox_policy:
type: workspaceWrite
networkAccess: true
gate:
kind: command
run: make check
automated_review: required
required_status_checks: []
ci_failure_action: rework
transient_ci_retry_limit: 2
validator:
enabled: false
# Recommended cheap override when enabled: gpt-5.4-mini.
# Watch rework-rate per validator model once cache/model telemetry lands.
model: ""
min_score: 0.8
max_attempts: 3
max_inline_diff_bytes: 65536
block_on:
- p1
server:
host: 127.0.0.1
port: 4000
hooks:
timeout_ms: 60000
---
You are working on {{ issue.identifier }}: {{ issue.title }}.
Read the issue description, follow repository instructions, keep changes
scoped to the issue, run the project validation gate, and prepare the work for
human review.Set github_rest_fanout_max_requests to 0 to disable the per-cycle fanout cap while retaining the REST remaining-reserve guard.
Tag-based projects can opt into release cadence in the same workflow file:
release:
enabled: true
min_merged_issues: 5
max_age_hours: 24
require_green_ci: true
version_bump: auto
rerun_flaky_once: false
flaky_check_names: []Set tracker.repository: owner/repo as well, including in ProjectV2 mode, so
Detent can inspect commits and file failure issues on the tracked board.
The policy triggers after either threshold is reached, requires every check on
the default-branch head to finish green, creates an annotated semantic-version
tag, and observes the Release GitHub Actions workflow through completion.
feat commits bump the minor version; other conventional commits bump the
patch version. Failed candidate CI and failed release workflows create a
fingerprint-deduplicated detent:todo issue instead of retrying indefinitely.
The flaky rerun option is disabled by default and only reruns named workflow
checks once.
Boardless issue-field mode:
---
tracker:
kind: github
github_status_source: issue_field
repository: owner/repo
status_field: Status
write_probe_issue: owner/repo#123
active_states:
- Todo
- In Progress
- Rework
- Merging
observed_states:
- Backlog
- Human Review
- Blocked
terminal_states:
- Done
- Cancelled
state_map:
Cancelled: Done
workspace:
root: /absolute/path/to/detent-workspaces
source_root: /absolute/path/to/project-checkout
agent:
max_concurrent_agents_by_state:
Merging: 1
gate:
kind: command
run: make check
required_status_checks: []
ci_failure_action: rework
transient_ci_retry_limit: 2
validator:
enabled: false
# Recommended cheap override when enabled: gpt-5.4-mini.
# Watch rework-rate per validator model once cache/model telemetry lands.
model: ""
min_score: 0.8
max_inline_diff_bytes: 65536
block_on:
- p1
---
You are working on {{ issue.identifier }}: {{ issue.title }}.Boardless label mode:
---
tracker:
kind: github
github_status_source: label
repository: owner/repo
status_label_prefix: "detent:"
active_states:
- Todo
- In Progress
- Rework
- Merging
observed_states:
- Backlog
- Human Review
- Blocked
terminal_states:
- Done
- Cancelled
state_map:
Cancelled: Done
workspace:
root: /absolute/path/to/detent-workspaces
source_root: /absolute/path/to/project-checkout
agent:
max_concurrent_agents_by_state:
Merging: 1
gate:
kind: command
run: make check
required_status_checks: []
ci_failure_action: rework
transient_ci_retry_limit: 2
validator:
enabled: false
# Recommended cheap override when enabled: gpt-5.4-mini.
# Watch rework-rate per validator model once cache/model telemetry lands.
model: ""
min_score: 0.8
max_inline_diff_bytes: 65536
block_on:
- p1
---
You are working on {{ issue.identifier }}: {{ issue.title }}.In label mode, the detent:* labels are tracker state, not workstream filters.
Use tracker.authorization.labels.*, projects[].authorization, and
agent.dispatch_priority_by_label for selecting or ranking work by ordinary
labels such as documentation, bug, or enhancement.
agent.prioritize_unblockers defaults to true and ranks an unlabeled issue
ahead of otherwise-equal peers when it directly unblocks waiting work. State,
tracker priority, and every configured dispatch-label tier remain higher.
The fleet /kanban board stays read-only, as do observer dashboards and
shared dashboards where users should not mutate GitHub from Detent. For a
trusted operator-owned project board, default server.kanban.mode: integration before mutation authorization. Skipped pre-mutation write probes
are not evidence for read_only; mutation still requires authorization and
post-authorization detent doctor --allow-write-probes to prove ProjectV2
status write, issue-field status write, or status-label update for the selected
status source, plus issue/PR comment write:
server:
kanban:
mode: integration
# Set show_blocked_alerts: true only when red blocked states should appear
# as one compact top-of-board alert; dependency waits stay on cards.
# show_blocked_alerts: true
# Use mode: read_only for observer/shared dashboards, no-writes choices,
# or failed post-authorization write probes.
# allowed_transitions:
# In Progress: [Blocked, Cancelled]
# Rework: [Blocked, Cancelled]
# Merging: [Blocked, Cancelled]
# QA: [Blocked, Human Review]When allowed_transitions is omitted, integration mode keeps conservative
defaults for manual moves from execution states: active work such as
In Progress, Rework, and Merging can only move to configured exception
states such as Blocked or Cancelled. Add source-specific entries to allow a
project workflow to expose extra manual moves without changing Detent's UI code.
Use agent.instructions_by_state and agent.instructions_by_transition when a
workflow stage needs prompt text that should stay out of the main
WORKFLOW.md body. The runner appends matching workflow instructions after the
base prompt and before Detent's deliverable and validation-gate blocks. State
keys and transition source/target keys must reference configured workflow
states.
For a non-code editorial workflow such as Research -> Draft -> Review -> Package -> Publish, the stage-specific instructions can live in frontmatter:
tracker:
active_states: [Research, Draft, Review, Package]
terminal_states: [Publish, Cancelled]
agent:
instructions_by_state:
Research: |
Gather source notes, links, and open questions before drafting.
Draft: |
Write the article body and keep unresolved facts clearly marked.
Review: |
Address editorial feedback and leave a concise change summary.
Package: |
Prepare final assets, metadata, and publication handoff notes.
instructions_by_transition:
Review:
Package: |
Confirm all review comments are resolved before packaging.workspace.source_root is the checked-out repository Detent uses for
git worktree add; workspace.root is where per-issue worktrees are created.
If workspace.source_root is omitted, Detent falls back to the project
workdir from global config, then to workspace.root for older single-root
setups.
workspace.cleanup_idle_ttl_ms controls how long non-active observed
workspaces can sit idle before cleanup. Terminal issues are cleaned on the next
poll when observed. workspace.cleanup_sweep_interval_ms controls the startup
and periodic idle cleanup cadence.
polling.interval_ms defaults to 120000 and must be at least 60000.
polling.conditional defaults to true. GitHub label and issue-field trackers
cache response ETags for issue lists and REST hydration endpoints, send
If-None-Match on later polls, and reuse the cached representation after a
304 Not Modified. An authorized conditional request that returns 304 does
not consume GitHub's primary REST quota. This makes a 60000 interval practical
for an idle REST-backed board while preserving the default cadence for existing
workflows. Set conditional: false to restore unconditional requests; trackers
without conditional-request support continue using their existing polling path.
See GitHub's conditional request guidance.
REST telemetry distinguishes total_requests, conditional_requests,
not_modified_requests, and billable_requests. Endpoint contributors expose
the same breakdown, and the REST rate-limit card's cycle cost uses billable
requests rather than free 304 checks.
Each GitHub-backed project can opt into near-real-time external updates by
setting a webhook secret in its WORKFLOW.md:
tracker:
kind: github
github_status_source: label
repository: digitaldrywood/detent
github_webhook_secret: $DETENT_GITHUB_WEBHOOK_SECRET
polling:
interval_ms: 60000
conditional: trueSet DETENT_GITHUB_WEBHOOK_SECRET in the Detent process environment. Configure
the repository webhook with:
- Payload URL:
https://<detent-host>/api/v1/webhooks/github - Content type:
application/json - Secret: the same high-entropy value
- Events: Issues, Pull requests, Check suites, and Labels
Detent verifies X-Hub-Signature-256 with HMAC-SHA256, routes the delivery only
to projects whose configured repository and secret match, and queues a fetch for
the affected issue. Pull-request and check-suite deliveries resolve Detent's
issue from its generated branch name. A repository-level label event has no
single issue target, so it queues a normal conditional refresh. Unknown
repositories never trigger a fleet-wide fallback. See GitHub's
webhook signature validation
and event payload reference.
For a ProjectV2 tracker spanning multiple repositories, omit tracker.repository
to let the connector verify project membership after signature validation. Set
tracker.repository when the project is repository-scoped and strict routing is
preferred.
GitHub must be able to reach the payload URL. For local testing, GitHub documents forwarding deliveries with smee.io. For a persistent host without public ingress, an outbound tunnel such as a Cloudflare Tunnel published application can route a public HTTPS hostname to Detent. Configure relays and reverse proxies to preserve the raw request body and GitHub signature headers; modifying either causes signature verification to fail.
GitHub projects can turn external events and repository scans into normal board
issues with an intake policy in WORKFLOW.md. Intake is disabled by default;
an omitted or empty sources list starts no receiver or scanner work.
intake:
sources:
- name: production-errors
kind: sentry
secret: $DETENT_SENTRY_INTAKE_SECRET
match: level:error
creates:
status: Backlog
labels: [bug]
title: "[{source}] {summary}"
body: "{details}"
dedupe_by: fingerprint
- name: weekly-todos
kind: schedule
cron: "0 6 * * 1"
scan: stale-todos
creates:
status: Backlog
labels: [maintenance]
title: "{summary}"
body: "{details}"
dedupe_by: fingerprintWebhook kinds are webhook, sentry, datadog, and slack. Send JSON to
POST /api/v1/intake/<project-id>/<source-name> with the resolved source secret
in either X-Detent-Intake-Token or Authorization: Bearer <secret>. Generic
JSON uses summary, details, and fingerprint; the named adapters also map
their common nested event fields into those values. match uses
field:value against flattened JSON fields. dedupe_by can name any flattened
field and defaults to fingerprint.
Templates can reference {source}, {kind}, {summary}, {details},
{fingerprint}, and flattened payload fields. Detent stores a hashed intake
marker in the issue body. A later event with the same source and fingerprint
updates that issue and adds configured labels without resetting its status, so
an operator promotion from Backlog remains intact. New issues receive the
configured starting status and then enter the existing label, issue-field, or
ProjectV2 gate pipeline. ProjectV2 intake also requires tracker.repository so
Detent knows where to create the repository issue before adding it to the board.
The built-in stale-todos scanner walks the project source root for TODO and
FIXME entries while skipping generated/dependency trees such as .git,
node_modules, vendor, tmp, dist, and build. Scanner schedules use
standard five-field cron expressions.
Host-local policy can override the workflow policy inside a project entry in
global.yaml using the same intake shape. An explicit sources: [] override
disables workflow-defined intake for that project.
Each project can opt into an evidence-based reflection loop over its runtime telemetry. The pass runs after agent completions and on a daily schedule, groups recurring inefficiency patterns, and creates or updates fingerprinted board issues for human triage.
retro:
enabled: true
schedule: "0 3 * * *"
target_state: Backlog
labels: [retro]
product_repository: digitaldrywood/detent
daily_issue_cap: 3
lookback_days: 7
min_occurrences: 2
single_occurrence_severity: critical
fallback_threshold: 3
receipt_baseline_multiple: 4Workflow findings stay on the project's board and include a governed proposed
WORKFLOW.md change when the evidence maps to a known setting. Product findings
such as completed-work re-dispatch or systemic capacity handling route to
product_repository. The configured retro label is always retained, and
target_state: Todo can be used when a project explicitly wants findings to
skip Backlog triage.
Detent files a finding after min_occurrences, or after one occurrence at or
above single_occurrence_severity. daily_issue_cap limits newly created
issues; recurrence updates to existing fingerprinted issues remain allowed.
Retrospection never edits workflow files, prompts, or runtime configuration.
Its issue body records the evidence, proposed change, and pending human outcome.
detent doctor reports the last run, finding count, and filed/updated issue
counts for every enabled project. ProjectV2 trackers must set
tracker.repository so Detent knows where to create project-level findings.
gate controls the validation contract the agent and operator flow follow.
Omitting it preserves the code default: kind: command with run: make check,
plus green CI, no P1 automated PR review findings, a quiet window, and a
current-head automated PR review before auto-promotion. Set
automated_review: optional to wait through
agent.auto_promote.gate_wait_timeout_seconds and then continue to Merging
when the remaining checks pass. Set automated_review: off to skip that wait.
The legacy require_automated_review boolean remains accepted and maps true
to required and false to off. The quiet period resets on observed
issue updates, Project status updates, automated PR review submission, and
linked PR activity such as a fresh push to the PR head. Failed or cancelled
current-head CI moves a Human Review item back to
Rework by default. Set ci_failure_action: skip only when red CI should park
in Human Review; pending CI stays parked. Use
kind: human_review with approval_label only when the workflow explicitly
requires a human approval label to promote.
Set required_status_checks to the exact branch-protection or ruleset check
names that are release-blocking for the project. Detent treats a configured
required check as non-green when it is missing, skipped, failed, cancelled,
neutral, or still running on the current PR head.
Set gate.validator.enabled: true to add a validator-agent review before
auto-promotion. The validator inspects the PR diff against the issue acceptance
criteria and returns a structured verdict, score, summary, and severity-tagged
findings. gate.validator.model optionally overrides the selected validator
route model, min_score below threshold routes to Rework, and any finding
severity listed in block_on routes to Rework regardless of score.
Validator production failures are retried with backoff up to max_attempts
(default 3). Each failure is logged and visible to detent doctor; an
exhausted validator routes the item to Rework with the failure cause.
For Codex-backed validators, start with the cheap-tier override
gate.validator.model: gpt-5.4-mini and watch rework-rate per validator model
once cache/model telemetry lands. gate.validator.max_inline_diff_bytes
defaults to 65536; validator prompts include the full diff only at or below
that size and otherwise seed stat-only context.
plan controls the optional plan-approval stop before implementation. It is
disabled by default, preserving the direct dispatch behavior. When enabled, the
first Todo dispatch runs in plan-only mode, posts a ## Detent Plan issue
comment, and moves the issue to the configured stop such as Plan Review.
review: human waits for approval_label (plan-approved by default),
review: automated waits for a ## Detent Plan Review issue comment or
current-head automated review state, and review: both accepts either path.
Blocking P1 plan findings route the issue to Rework with feedback.
For production, self-hosted, or multi-instance GitHub Projects, prefer GitHub
App installation authentication instead of a shared personal access token. App
installation tokens have a dedicated GraphQL budget per installation and scale
with larger installations, while a PAT shares one fixed user budget across
Detent, agents, and operator gh calls. Configure the tracker with
github_app_id, github_app_installation_id, and either
github_app_private_key or github_app_private_key_path; keep api_key for
small local setups or one-off evaluation.
Default workflows do not need worktree setup hooks. Detent creates and removes
Git worktrees natively, so a fresh Windows project can dispatch without bash.
Omit codex.shell and hooks.shell to use the per-OS defaults: sh on Unix
and cmd on Windows. For portable hooks, prefer no hook when Detent already
does the setup natively. When a hook is necessary, keep it to commands available
on every target or set hooks.shell: pwsh and write PowerShell that reads
Detent values from $env:WORKSPACE, $env:WORKSPACE_KEY, $env:BRANCH,
and $env:ISSUE_IDENTIFIER. The older DETENT_* hook variables remain
available as deprecated aliases for one release.
- Create the global config and add the project:
detent init
detent add-project \
--id <id> \
--workflow /absolute/path/to/project-checkout/WORKFLOW.md \
--workdir /absolute/path/to/project-checkoutFor first-time onboarding, leave --workflow-ref unset until this
WORKFLOW.md has been merged to the ref Detent should read from.
detent doctor validates the configured ref; setting
--workflow-ref origin/main before origin/main:WORKFLOW.md exists will fail
even when the file exists locally in the working tree. After the first workflow
merge, add workflow_ref: origin/main to the project entry when you want Detent
to load the workflow from the branch tip instead of the working-tree file.
Edit the resolved global.yaml and set the top-level runtime keys:
env: prod
log_level: info
github_token: gh
port: 4000
update:
auto_check_enabled: true
check_interval_hours: 24
auto_apply_enabled: false-
Verify the setup before dispatching:
detent doctor --allow-write-probes
detent doctor is a preflight check: config resolution, the SQLite database,
the codex binary, GitHub auth mode, GitHub tracker readiness, git, and
whether the server port is free. In ProjectV2 mode it checks project access,
Status options, board item reads, repository issue/PR access, and rate-limit
visibility. In issue-field mode it checks repository access, issue field
discovery, Status option discovery, issue reads by field value, and REST/GraphQL
rate-limit visibility. In label mode it checks repository access, status label
mappings, issue reads by configured status labels, and REST/GraphQL rate-limit
visibility. By default doctor is read-only: if a configured workflow would run
write probes, the report warns that they were skipped. Pass
--allow-write-probes only after the onboarding mutation gate has passed and
the operator has explicitly confirmed mutation. With that flag, ProjectV2 and
issue-field modes require tracker.write_probe_issue when integration needs
status-write proof, because their status mutations target a concrete project
item or issue field value. Label mode does not need a persistent status-labeled
scratch issue for the default permission proof: doctor sends intentionally
invalid repository-label and issue-create requests, expecting GitHub to reject
them with validation while proving the token has the repository Issues write
permission class. Configure tracker.write_probe_issue in label mode only for
legacy/deep issue-object proof, such as reapplying an existing status label on a
scratch issue. That proof is stronger for the chosen issue object, but the issue
must be kept off the board by removing Detent status labels or closing it after
migration. Before starting Detent, fix any FAIL (missing github_token: gh or
an unauthenticated codex are the usual culprits). If Detent is already running
on the configured port, the server-port check can fail because
the live service owns the port; use detent doctor --port 0 for the same
read-only config, toolchain, token, and database preflight without the port
collision, or detent doctor --port 0 --allow-write-probes after mutation
authorization to prove writes, then verify the live service with /health.
For Detent dogfood/self-tests that need a running server, start an isolated mock
runtime instead of stopping or reusing the live process on 127.0.0.1:4000:
detent dev-runtime --port 0The command prints Mode: isolated dev runtime, the selected dashboard URL,
temp home, DB mode, tracker mode, and fixture path. By default it uses a temp
config/workspace home, an in-memory SQLite database, a stateful fixture-backed
memory tracker, and a fake runner; it does not call GitHub or mutate a real
ProjectV2 board. It refuses the live dogfood port and live
~/.detent/detent.db unless explicitly overridden.
Use the built-in Kanban demo when you want to evaluate the operator board and mutation dialogs without a GitHub token, a real ProjectV2 board, or production database state:
detent dev-runtime --demo kanban --port 0Pass --demo-project to choose the generated project ID when you want generic
demo URLs and labels instead of the default dogfood-safe ID:
detent dev-runtime --demo kanban --demo-project demo-project --port 0Demo runtimes bind to 0.0.0.0 when --host is omitted so the selected
random port can be reached from trusted network interfaces. From another
machine on Tailscale, replace the local banner host with the Tailscale
hostname. With the override above, open http://prometheus:<port>/kanban for
the mixed-project board or
http://prometheus:<port>/projects/demo-project/kanban for the generated
project's interactive board. Pass --host 127.0.0.1 for a local-only demo run.
The Kanban demo keeps the runtime isolated on the memory tracker, seeds at
least four projects with one or two cards each, and mixes configured project
colors with deterministic automatic colors. The fleet /kanban board is
read-only and shows cards across those projects; project-specific pages such as
/projects/demo-project/kanban enable integration mode for the generated demo
workflow. The demo includes explicit server.kanban.allowed_transitions such
as Backlog -> Todo so sheet-based Move actions can be exercised without
weakening production defaults. Demo cards cover Backlog, Todo, In Progress, Blocked,
Human Review, Rework, Merging, Done, and Cancelled states, including
issue-only cards, linked PR cards, CI pass, pending, and failure states, Codex
review clean and finding states, labels, assignees, blockers, and wait
metadata. Issue and PR comments are captured by the memory connector with no
external side effects.
Use the screenshots demo when you need deterministic pages, HTMX fragments, API responses, reports, and SSE payloads for documentation screenshots, video recording, or visual e2e baselines:
detent dev-runtime --demo screenshots --port 0The screenshots demo uses the same isolation model and demo bind default as the
Kanban demo: memory
tracker, fake runner, isolated home, isolated database, isolated workspaces,
fake https://github.test/... URLs, no GitHub calls, no real ProjectV2
mutation, and no live dogfood port by default. It freezes demo time at
2026-06-15T12:00:00Z unless started with --demo-clock play, which advances
SSE ticks and visible running-work counters for video capture. The boot banner
prints the scenario manifest location. Screenshots mode intentionally keeps the
primary project fixed at dogfood so page routes and visual baselines remain
deterministic:
Scenario manifest: /api/v1/demo/scenarios
Select a scenario with X-Detent-Demo-Scenario; the visible URL stays on the
normal page route:
const scenarios = [
["fleet-healthy-parallel-work", "/"],
["fleet-kanban-multiproject", "/kanban"],
["kanban-full-integration", "/projects/dogfood/kanban"],
["reports-normal-window", "/reports"],
];
for (const [scenario, route] of scenarios) {
await page.setExtraHTTPHeaders({ "X-Detent-Demo-Scenario": scenario });
await page.goto(`${baseURL}${route}`);
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot(`${scenario}.png`);
}For visual comparisons, keep the screenshot environment stable: browser, viewport, fonts, OS rendering, device scale factor, and generated assets should match the baseline environment. The manifest includes each scenario ID, route, required header, recommended viewport, screenshot name, and wait selector. A quick JSON smoke check looks like this:
curl -H 'X-Detent-Demo-Scenario: fleet-healthy-parallel-work' "$DETENT_URL/api/v1/state"Use the capture harness when you need the canonical video-production artifact set from one command:
detent dev-runtime capture --out ./captureThe harness starts an isolated screenshots demo on an ephemeral local port,
loads the scenario manifest, captures the canonical still set, and writes a
deterministic terminal onboarding cast. It does not read or write the operator's
real ~/.config/detent/global.yaml. Stable output paths are:
capture/demo-capture-v1.json
capture/stills/v1/01-fleet-healthy-parallel-work.png
capture/stills/v1/02-fleet-kanban-multiproject.png
capture/stills/v1/03-kanban-full-integration.png
capture/stills/v1/04-project-active-overview.png
capture/stills/v1/05-reports-normal-window.png
capture/stills/v1/06-onboarding-project-selection.png
capture/terminal/v1/onboarding.cast
By default the browser viewport is 1920x1080 with
--device-scale-factor 2, producing 4K PNGs. Pass --scenario <id> one or
more times for a named subset, --all-scenarios for every browser-capturable
GET scenario in the manifest, --width, --height, and
--device-scale-factor for alternate framing, or --demo-clock play when a
motion capture needs advancing counters. The PNG capture uses a local
Chrome-family browser; pass --browser <path> or set DETENT_CAPTURE_BROWSER
when auto-detection cannot find one.
The CI browser visual gate runs Playwright when a PR changes UI-sensitive paths
such as .github/workflows/ci.yml, go.mod, go.sum, package.json,
static/**, internal/web/**, internal/cli/dev_runtime*.go,
internal/devruntime/**, Templ inputs, or screenshot/onboarding docs. It builds
the PR's Detent binary, starts isolated dev-runtime instances on port 0,
captures current evidence under tmp/playwright-evidence, and uploads
Playwright reports, traces, screenshots, and image diffs when assertions fail.
PRs without UI-sensitive changes keep the required Browser Visual check fast
by building the Detent binary and running a CLI smoke instead of starting
Playwright.
Run the layout gate locally after installing Playwright's Chromium browser:
npx playwright install chromium
make visual-e2eCommitted image baselines are authoritative for GitHub Actions Ubuntu
x64/Chromium. On non-Linux hosts, make visual-e2e still runs the browser
layout assertions and captures evidence, but skips pixel comparison unless
DETENT_VISUAL_STRICT=1 is set.
Update baselines only when the visual change is intentional. Run the update in
the same Ubuntu x64/Chromium environment as CI, then review and commit the
changed files under tests/visual/__screenshots__/chromium/:
make visual-e2e-updateDo not commit tmp/playwright-evidence, tmp/playwright-report, or
tmp/playwright-results; those are transient review and debugging artifacts.
Use the normal live runtime, detent with your global config, only when you
intend to operate on the configured tracker and ProjectV2 board. Use
detent dev-runtime --fixture <path> for focused fixture validation such as
autopromote behavior, --demo kanban for safe board exploration, and
--demo screenshots for deterministic page-addressable screenshots.
- Start Detent:
detentOpen the dashboard at http://localhost:4000. Use --host and --port to
override the address. Before exposing a remote URL such as
http://prometheus:4000/, choose the dashboard bind mode:
On shutdown, the first Ctrl-C stops new dispatches and drains running agent sessions while the terminal reports the blocker count and time remaining. A second Ctrl-C force-quits immediately, interrupts those sessions, and re-queues their issues. A new process using the same runtime database will fail with an actionable error until the prior process has released it; a listener conflict also fails before SQLite migrations begin.
127.0.0.1keeps the dashboard local to the host and is the safest default for SSH tunnel access.- A specific private or Tailscale IP exposes the dashboard only on that interface and is preferred for VPN-only access.
0.0.0.0exposes the dashboard on every interface, not just Tailscale. Use it only on trusted private networks with the expected host firewall rules.
When Detent is bound to 127.0.0.1, curl from the same host can work while
http://<host>:4000/ fails from another machine because loopback is not
reachable remotely. Set server.host in WORKFLOW.md for the default bind, or
set --host in the CLI command or service ExecStart:
detent --host 127.0.0.1 --port 4000
detent --host <tailscale-or-private-ip> --port 4000
detent --headless --host 0.0.0.0 --port 4000Verify the listener and the local or VPN URL you intend operators to use:
ss -ltnp | rg ':4000|detent'
curl -fsS http://127.0.0.1:4000/api/v1/state
curl -fsS http://<tailscale-or-private-ip>:4000/api/v1/stateA complete, ordered runbook to take a bare machine to a running Detent. Every
step has a verification command — do not proceed until it passes. An AI agent
can execute these steps top to bottom; replace each <...> placeholder. The
detent-orchestration
repo is a real, working instance of this setup to copy from.
-
Install Detent.
brew install digitaldrywood/tap/detent(macOS/Linux),go install github.com/digitaldrywood/detent/cmd/detent@latest, or a platform installer from Install. Verify:detent version. -
Install and authenticate the GitHub CLI. Install
gh, then choose scopes for the tracker mode:# ProjectV2-backed board mode. gh auth login --scopes "repo,read:org,read:project,project" # For existing auth: gh auth refresh -h github.com --scopes "repo,read:org,read:project,project" # Boardless issue-field mode. gh auth login --scopes "repo,read:org" # Boardless label mode. gh auth login --scopes "repo"
Verify the required classic PAT scopes independently. Boardless issue-field and label modes do not require
read:projectorproject; label mode also does not requireread:orgunless another workflow setting needs it.gh auth status 2>&1 | rg '\brepo\b' gh auth status 2>&1 | rg '\bread:org\b' gh auth status 2>&1 | rg '\bread:project\b' gh auth status 2>&1 | rg "(^|[[:space:],'\"])project([[:space:],'\"]|$)"
Use
github_token: ghinglobal.yamlso Detent resolves this token at startup. -
Install and sign in to the Codex CLI. Install the OpenAI Codex CLI and sign in. Detent dispatches every agent through
codex app-server. Verify:codex --version. -
Choose the GitHub status source. For the current/default compatibility path, choose the GitHub ProjectV2 board Detent will drive and get its node id (starts with
PVT_):gh project list --owner <org-or-user> --format json --limit 50
This verifies the token can read ProjectV2 boards. The write
projectscope is verified when Detent first performs an intentional board mutation. The board only needs to exist — Detent auto-provisions missingStatusandPriorityoptions on first run. The option names must match yourWORKFLOW.mdstates, and Detent keeps knownStatusoptions in canonical board order.For boardless issue-field mode, skip ProjectV2 board creation and instead confirm the repository's organization has a single-select issue field named
Status:gh api /orgs/<org>/issue-fields --jq '.[] | select(.name == "Status")'
For boardless label mode, skip ProjectV2 board creation and organization issue-field setup, then confirm the repository has status labels with the configured prefix:
gh api repos/<owner>/<repo>/labels --paginate --jq '.[].name' | rg '^detent:'
-
Clone the repository you want Detent to work on (its checkout becomes
workspace.source_root):git clone <repo-url> <source-root>
-
Author the project contract. Copy the mode-specific template as a starting point, then edit it. For from-zero board creation, interview questions, issue intake, and the first-dispatch smoke test, follow Project Onboarding:
GITHUB_MODE="${GITHUB_MODE:?set GITHUB_MODE to project_v2, issue_field, or label}" curl -fsSL "https://raw.githubusercontent.com/digitaldrywood/detent/main/docs/templates/WORKFLOW.${GITHUB_MODE}.md" \ -o <source-root>/WORKFLOW.md
The maintained templates are
WORKFLOW.project_v2.md,WORKFLOW.issue_field.md,WORKFLOW.label.md, and the CLI-only local-status templateWORKFLOW.github_local.md. Non-code artifact workflows can start fromWORKFLOW.non_code_artifact.md. A runnable local example with seeded work items, artifact metadata, API payloads, and artifact-gate transitions lives indocs/examples/non-code-artifact. They setserver.kanban.mode: integrationfor trusted project boards; change that toread_onlyonly for an observer or shared dashboard, explicit no-writes choice, or failed post-authorization write probes. They also include a## Required Execution FlowwithFor Todo,For In Progress,For Rework, andFor Mergingsections so merge workers have a terminal instruction: invoke$go-workflow:ship, merge and move the issue toDone, move it toReworkwith an actionable defect, or leave it inMergingwith a concrete external blocker recorded. Before dispatchingMerging, confirm the Detent host's Codex environment exposes$go-workflow:ship; otherwise install or enable that workflow, or replace theFor Mergingsection with equivalent project-local merge instructions.For ProjectV2 mode, set
tracker.project_slug(yourPVT_id). For boardless issue-field mode, settracker.github_status_source: issue_field,tracker.repository: <repo-owner>/<repo-name>, and optionallytracker.status_field. For boardless label mode, settracker.github_status_source: label,tracker.repository: <repo-owner>/<repo-name>, andtracker.status_label_prefix. For externally owned or open-source repositories where Detent must not pollute issues, labels, fields, Projects, or issue comments, usetracker.kind: github_localfromWORKFLOW.github_local.md; do not settracker.github_status_source, configuretracker.local_sqlite.path, and import explicit issue numbers withdetent github-local import <project-id> <issue-number> --state Todo. In every mode, setworkspace.source_root(<source-root>),workspace.root(a worktrees directory),write_probe_issuefor ProjectV2 or issue-field status-write proof, and the prompt body. In label mode, setwrite_probe_issueonly when using legacy/deep issue-object probes. If the repository already has aWORKFLOW.md, audit its prompt body for the same Required Execution Flow andCurrent Detent status: {{ issue.state }}line before dispatching Detent against it. Registered projects can usegithub_token: ghinglobal.yaml; leavetracker.api_keyout of the workflow unless you are intentionally using a workflow-local token. The full field reference is in Quick Start.Interactive alternative: when Detent starts without a resolved
global.yamland without aWORKFLOW.mdin the current directory, it serves the/onboardingweb wizard. Openhttp://localhost:<port>/onboardingto walk through tracker, credentials, project, agent, and write steps for generatingWORKFLOW.md. The first step asks for ProjectV2 board, organization issue field, or repository labels; label-mode users should choose Repository labels, then enter the repository and status label prefix such asdetent:. The wizard hides ProjectV2 fields for boardless modes. Then return to the runbook for board creation, global registration, issue intake, and the smoke test. -
Create global config and register the project:
detent init detent add-project --id <id> \ --workflow <source-root>/WORKFLOW.md \ --workdir <source-root>
Edit the resolved
global.yamland setgithub_token: ghwith any desiredenv,log_level, andportoverrides. -
Verify everything:
detent doctor --allow-write-probes
Every check must pass before starting Detent. If Detent is already running on the configured port, the server-port check may fail because the live service owns the port. In that case, validate the rest of the setup without the port collision, then verify the running service:
detent doctor --port 0 --allow-write-probes curl -fsS http://127.0.0.1:4000/health | jq -e '.status == "ok" and .mode == "running"'
-
Start Detent and confirm the dashboard:
detent --host 127.0.0.1 --port 4000 ss -ltnp | rg ':4000|detent' curl -fsS http://127.0.0.1:4000/api/v1/state
Keep
127.0.0.1for SSH tunnels. For VPN access, use the selected private or Tailscale IP instead and verify it from another machine:detent --host <tailscale-or-private-ip> --port 4000 curl -fsS http://<tailscale-or-private-ip>:4000/api/v1/state
Use
--host 0.0.0.0only when every host interface is trusted for dashboard access; it is not limited to Tailscale. -
Dispatch work. Move an issue to
Todothrough the configured status source: ProjectV2Status, issue-fieldStatus, thedetent:todostatus label, or the local SQLite state used bygithub_local. Detent claims it, creates an isolated worktree, dispatches an agent, and the issue appears under Running on the dashboard. Drive the rest through the configured status source (Todo→In Progress→Human Review→Merging→Done).
Detent isolates tracker integration behind a connector interface. The current
production connector is GitHub. It supports the current ProjectV2-backed board
mode, boardless issue-field mode, boardless label mode, and github_local
hybrid mode. A memory connector is available for local development, and the
connector boundary is where GitLab and Jira support will land later.
GitHub configuration lives in each project's WORKFLOW.md frontmatter. The
default github_status_source: project_v2 mode uses project_slug as the
GitHub ProjectV2 node id. Detent reads issue state, priority, labels, blockers,
and assignment from the board, then writes comments and state transitions back
through the connector. Boardless github_status_source: issue_field mode uses
repository: owner/name and an organization issue field such as
status_field: Status; Detent reads issues by issue-field value and updates
that field for state transitions. Boardless github_status_source: label mode
uses repository: owner/name and repository labels named by
status_label_prefix; Detent reads issues by configured status labels and
updates state by replacing the previous status label with the target one.
tracker.kind: github_local is a separate backend, not a fourth
tracker.github_status_source value. GitHub is read-only for issue, dependency,
parent/child, comment, PR, review, check, and rate-limit facts; Detent's SQLite
database is the source of truth for workflow state, claim fields, audit-trail
comments, priority, and local close decisions. In this mode Detent does not
create or mutate GitHub Projects, issue fields, repository labels, status
labels, GitHub issue comments, or GitHub issue close state. Pull request
lifecycle writes for Detent-owned PRs remain allowlisted.
Plain detent doctor is read-only and reports that write probes were skipped.
After mutation authorization, ProjectV2 and issue-field modes still need a
configured tracker.write_probe_issue to prove concrete status writes against
a project item or issue field value. Label mode can prove repository write
access without a scratch issue by sending intentionally invalid repository-label
and issue-create requests. GitHub should answer with validation errors, which
proves the Issues write permission class without creating board-visible work.
Set tracker.write_probe_issue in label mode only when you need legacy/deep
issue-object proof by replaying existing values on a scratch issue. That scratch
issue must already have one configured status label so doctor can reapply it;
after switching to the no-persistent probe path, remove any Detent status label
from old scratch issues or close them so they stop appearing as normal board
inventory.
GitHub issue fields apply to issues, not pull requests. In issue-field mode, boardless status comes from the linked issue. In label mode, Detent treats repository issues with configured status labels as work items. Detent still displays linked PR state and can comment on PRs through GitHub's shared issue-comment endpoints, but a PR-only card without a linked issue is not dispatchable through issue-field or label status.
The GitHub connector uses one pooled keep-alive HTTP client for GraphQL and
GitHub App REST token requests. Tune tracker.http_max_idle_conns,
tracker.http_max_idle_conns_per_host, and
tracker.http_idle_conn_timeout_ms when many Detent instances share one host.
Keep host-level agent concurrency within the machine's shared outbound
connection and ephemeral-port budget; the connector logs its live connection
count on GitHub requests to help spot pressure. For shared-board operation, see
Running Multiple Instances.
The recommended GitHub Project board states are:
| State | Meaning |
|---|---|
Backlog |
Not eligible for agents yet. |
Todo |
Ready for Detent to claim and dispatch. |
In Progress |
An agent is actively working or continuing work. |
Blocked |
Human-blocked work, or dependency-waiting work when auto-unblock is enabled. |
Human Review |
The PR is ready for review/soak until the workflow's promotion criteria pass. |
Rework |
Human or bot feedback needs another agent pass. |
Merging |
Final rebase, merge-gate check, CI watch, and merge. |
Done |
Complete. |
Cancelled |
Terminal state mapped to Done in the default release flow. |
Manual Cancelled means Detent should stop managing the work item, not that
GitHub issues or pull requests are automatically closed. On the next poll that
observes the cancelled terminal state, Detent cancels any running agent context,
releases the global dispatch slot, clears configured claim lease state, records
the run as completed with final state Cancelled, and asks the workspace
backend to remove the Detent worktree, prune git worktrees, delete the generated
detent/ branch when safe, and reap workspace processes.
Terminal cleanup is attempted on each poll for terminal states so a cancelled non-running issue can still clean up an existing Detent workspace without waiting for the idle cleanup sweep. The idle sweep interval still controls non-terminal observed workspace cleanup.
Detent emits cleanup diagnostics in /api/v1/state under events and in
published telemetry snapshots. A successful cleanup records
workspace_reap_succeeded with worktrees=, branches=, and processes=
counts. Cleanup failures record workspace_reap_failed, leave the workspace
eligible for a later retry, and keep the diagnostic visible in recent events.
If Detent completes a terminal run but no workspace reaper is configured, it
records workspace_reap_unverified.
Detent does not close the GitHub issue when an item is manually cancelled. The
configured tracker state is the source of truth; in label mode, the default
Cancelled: Done state map means the repository label is detent:done.
Operators remain responsible for closing the GitHub issue with not_planned or
another reason when that is the desired repository record.
Detent also leaves linked pull requests open. Operators should close, comment on, or reuse an open PR explicitly when cancelled work has one.
Human Review is the holding state before the merge train. Auto-promotion out
of that state is controlled by the workflow:
gate.kind: commandrequires a linked open PR, green CI, no P1 automated PR review findings, and the configured quiet period. By default it also requires a current-head automated GitHub PR review.gate.automated_review: optionalwaits for a current-head review until the gate deadline, honors any review that arrives, and then promotes without one when the remaining checks pass.offdoes not wait;requiredwaits and defaults to a Human Review timeout.gate.required_status_checksnames release-blocking check runs or commit status contexts that must be present, completed, and successful on the current PR head.gate.ci_failure_action: reworkroutes failed or cancelled current-head CI fromHuman Reviewback toReworkby default; setgate.ci_failure_action: skiponly when non-green CI should leave the item parked.gate.transient_ci_retry_limitcontrols how many times Detent reruns failed checks with transient runner or infrastructure signals such as timeouts, startup failures, OOM kills, orsignal: killedannotations before red CI is treated as a hard failure.gate.validator.enabled: trueruns a validator-agent review before auto-promotion; verdicts belowmin_scoreor with severities inblock_onroute the issue toRework.agent.auto_promote.rework_limitbounds repeatedHuman ReviewtoReworkloops using persisted lane events; the default is3, and0is an explicit opt-out that leaves the loop unlimited. Positive limits requireBlockedin a configured tracker state list and route the next over-limit rework decision there with repeated reasons summarized for handoff.agent.auto_promote.gate_wait_state: sourcekeeps zero-quiet completed active issues in their current lane while CI/check gates are still pending;reviewrestores the legacy behavior of parking those pending issues in the configured review source state.gate_wait_timeout_secondsbounds that active wait.gate_wait_timeout_action: merge | human_reviewmakes the terminal action explicit; it defaults tomergefor optional review andhuman_reviewfor required review.gate.kind: human_reviewrequires a linked open PR plus the configuredapproval_labelon the issue.
The quiet period resets on observed issue updates, Project status updates, automated PR review submission, and linked PR activity such as a fresh push to the PR head.
The quiet period is an intentional quality gate. Tune
agent.auto_promote.quiet_seconds when reviewer soak time is too conservative,
but keep the gate explicit so faster merges are a policy choice rather than an
accidental bypass.
A Codex coding session that created the PR is not the same signal as a
Codex/ChatGPT/Claude GitHub PR review. If automated PR review is required and
the PR head changes after a review, request or wait for a fresh automated review
before expecting auto-promotion. detent doctor warns when required or optional
review is configured but none of the sampled recent merged PRs has an automated
review, which indicates the review producer may be inactive.
You choose where GitHub status lives; Detent fills in the rest.
- ProjectV2 mode: create a GitHub Projects v2 board (org or user) and point
tracker.project_slugat its node id — thePVT_…id fromgh project list --owner <org-or-user> --format json. The board has a defaultStatusfield; add aPrioritysingle-select if you rank work. - Detent auto-provisions the missing options inside those fields on first
run — the
Todo/In Progress/Rework/Merging/Donecolumns above and theUrgent…Lowpriorities — so the option names always match yourWORKFLOW.md. It also reorders the knownStatusoptions to Detent's canonical column order:Backlog,Todo,In Progress,Blocked,Human Review,Rework,Merging, then terminal states. Extra custom status options are preserved after the configured Detent states. It provisions the options, not the board or the fields themselves, so create the board (and thePriorityfield if used) first. - Boardless issue-field mode: create or reuse an organization issue field,
normally a single-select
Statusfield, and make it available to the repository. Configuretracker.github_status_source: issue_field,tracker.repository: owner/name, and optionallytracker.status_fieldwhen the field is not namedStatus. Detent's Kanban/dashboard view becomes the board surface; no GitHub ProjectV2 board ortracker.project_slugis needed. - Boardless label mode: create or reuse repository labels for every
effective Detent state. Configure
tracker.github_status_source: label,tracker.repository: owner/name, and optionallytracker.status_label_prefixwhen the prefix is notdetent:. The label name is the prefix plus the slugified mapped state:In Progressbecomesdetent:in-progress. The issue should have exactly one configured status label at a time. Detent's Kanban/dashboard view becomes the board surface; no GitHub ProjectV2 board,tracker.project_slug, or organization issue field is needed. - GitHub local-status mode: configure
tracker.kind: github_local,tracker.repository: owner/name, andtracker.local_sqlite.path. Do not settracker.github_status_source. Import issues explicitly withdetent github-local import <project-id> 123,456 --state Todo; unimported issues do not appear on Detent boards. Detent updates local workflow state only, while GitHub remains the read-only source for issue text, labels, assignees, dependencies, linked PRs, reviews, checks, and rate-limit health. The board surfaces divergence such as an upstream-closed issue that is still locally active. - Blank
Statusvalues and missing status labels are notBacklog. In the current release, an issue with no configured issue-field value or status label is not dispatchable through the state machine. Put unready work in theBacklogoption ordetent:backloglabel explicitly. Detent's own GitHub issue templates default todetent:backlogso new dogfood issues are visible to operators but are not dispatchable until triaged todetent:todoor another active state. Remove that label only when an issue is intentionally outside Detent. - Label-mode drift is surfaced as cleanup work. In label mode,
detent doctorand the dashboard report open repository issues with zero configured status labels, plus open issues that still carry a configured terminal status label such asdetent:done. Add exactly one configured status label to untracked issues, and close or relabel stale-open terminal issues. - Detent reads status, priority, labels, blockers, assignees, and linked
pull requests from each issue, and writes back status transitions and a
## Codex Workpadcomment as the agent works, except ingithub_local, where those workflow writes are durable local records.
Boardless projects use Detent's own dashboard as the day-to-day board. The
fleet /kanban board stays read-only because it is a cross-project observer
surface. A trusted operator project board should default to integration
before mutation authorization, so operators can move cards and post comments
from /projects/<id>/kanban after the mutation gate and write probes pass.
Skipped pre-mutation write probes are not evidence for read_only. Keep
read_only for an observer or shared dashboard, an explicit no-writes choice,
or failed post-authorization write probes for ProjectV2 status write in
ProjectV2 mode, issue-field status write in issue-field mode, status-label
update in label mode, or issue/PR comment write for comment forms.
Existing users do not need to migrate. Leaving
tracker.github_status_source unset keeps ProjectV2 as the source of truth,
and existing tracker.project_slug workflows remain valid. This is the
compatibility path when the GitHub Project board is where humans already plan,
rank, and move work.
To switch a repository to boardless issue-field mode, create the organization
issue Status field and options, copy current issue statuses from the
ProjectV2 board manually or with a one-off script outside Detent, then change
the workflow to github_status_source: issue_field with repository: owner/name. Detent does not automatically migrate ProjectV2 items to issue
fields. After the switch, run detent doctor --port 0 --allow-write-probes and
fix field discovery, option discovery, write-probe, comment-write, and
rate-limit checks before dispatching.
To switch a repository to boardless label mode, create status labels matching
the effective workflow states, copy current issue statuses by applying exactly
one configured status label per issue, then change the workflow to
github_status_source: label with repository: owner/name and
status_label_prefix: "detent:". Detent does not automatically migrate
ProjectV2 items or issue-field values into labels. After the switch, run
detent doctor --port 0 --allow-write-probes and fix label mapping, issue reads
by label, write-probe, comment-write, and rate-limit checks before dispatching.
To switch a repository to local-only status mode, copy
docs/templates/WORKFLOW.github_local.md, set tracker.repository,
tracker.local_sqlite.path, workspace paths, and the prompt body, then import
only the issue numbers Detent should see:
detent github-local import <project-id> 123,456 --state Todo
detent doctor --project <project-id> --port 0This mode does not migrate or mutate GitHub status data. Existing ProjectV2 items, issue fields, labels, and GitHub issue comments stay untouched. If GitHub closes or transfers an imported issue while local state is still active, Detent keeps the local row and surfaces the divergence on the board instead of auto-resolving it.
Detent supports two dependency patterns. Use the one that matches how much of the wait should be visible on the board.
- Keep the issue in
Todo. Add a machine-readable dependency line such asDepends on: #123,Blocked by: owner/repo#123, orDepends on: https://github.com/owner/repo/issues/123. Detent keeps the issue out of dispatch while any referenced blocker is non-terminal, then dispatches it normally after blockers clear. This is the default behavior and needs no extra configuration. - Keep the issue in
Blocked. Enabletracker.dependency_auto_unblockwhen your team wants dependency-waiting issues to sit in a waiting column. Detent only moves issues that have explicitDepends on:orBlocked by:references. When all blockers are terminal, closed, or have a merged linked PR under the configuredreadinessrule, Detent updates the configured GitHub status source totarget_stateand posts an audit comment. Withouttracker.dependency_auto_unblock.enabled: true, aBlockedissue is observed for display but will not be moved back toTodo. Human blockers without explicit dependency references stay blocked. - Queue fixable blockers automatically. Enable
tracker.blocker_auto_promotealongside dependency auto-unblock when a dependency-waiting issue should pull same-repository blockers out of inactive states such asBacklog,Blocked, orHuman Review. Detent promotes only resolved, same-repository blockers to the configuredtarget_state, respects current local agent capacity, and posts an audit comment on the promoted blocker. Agents that move work toBlockedbecause of another tracked issue or pull request must ensure the issue body contains a machine-readableBlocked by:orDepends on:line; Workpad mentions alone are not a durable dependency contract.
Before you dispatch anything, run detent doctor --allow-write-probes after
mutation authorization — it checks config
resolution, the database, the codex binary, GitHub auth mode, configured
tracker access, repository issue/PR access, required write proofs, rate-limit
visibility, git, and the server port. A clean pre-start doctor clears
Detent's direct preflight.
When a running Detent process already owns the configured port,
detent doctor --port 0 --allow-write-probes validates the config, database,
tools, token, and write proofs without treating the live listener as a blocker;
pair it with /health on the actual service before dispatching more work. Do
not dispatch from a failed doctor run unless the only failure is that expected
live-port collision and /health is green. If Detent runs under a systemd user
service, also verify the
service PATH resolves every command used by project hooks and validation gates;
doctor checks Detent's direct dependencies, not repo-specific bootstrap tools.
The onboarding runbook includes the service-context check.
Keep systemd's default KillMode=control-group for the Detent service. Detent
starts each Codex and Claude worker in its own process group without leaving the
service cgroup, so its persisted worker registry can terminate stale process
groups on shutdown or startup while systemd remains the final cleanup backstop.
Do not wrap worker commands in launchers that double-fork or explicitly move
children into another cgroup.
Merging is intentionally serialized. Keep this in every production workflow:
agent:
max_concurrent_agents_by_state:
Merging: 1Do not cap Todo, In Progress, or Rework unless you have a specific
operational reason. Those states should share the global agent pool so workers
stay busy while merge candidates wait for CI or a clean base branch.
When GitHub reports HAS_MERGE_QUEUE, Detent delegates every eligible green
candidate to the repository's native merge queue instead of rebasing each PR
through the serialized worker. GitHub owns merge-group validation and batching;
Detent keeps the issues in Merging, observes their queue entries, and
reconciles them to Done after GitHub reports the PR merged. Repositories
without a native queue continue through the serialized rebase and current-head
CI path.
Inside the serialized Merging lane, avoid duplicating the full local release
gate when it does not buy new signal. If the PR already passed the pre-review
gate, the branch rebases cleanly onto current origin/main, and no source files
change during rebase, the merge agent should run a focused rebase/smoke gate
locally and rely on required current-head CI for full enforcement. If the merge
agent edits code, resolves conflicts, detects stale or unknown validation state,
or cannot prove the final rebase was source-clean, it must run the full
configured gate again.
CI waiting should poll current-head REST check runs with backoff, not loop on
GraphQL-heavy PR status commands. Required release checks must run on the PR
head before merge; post-merge-only failures cannot be part of the merge
decision. Merge handoff telemetry should record the
quiet-window wait, GitHub queue/start wait, local merge-gate duration,
current-head PR CI duration, active slow-check runtimes, and whether post-merge
main CI is still running. The quiet window, current-head required CI, and
conflict/full-gate fallback are quality gates; repeated full local validation
after a source-clean rebase, noisy status polling, uncached tool install, and
duplicated non-blocking post-merge work are optimization targets.
The repository CI caches the project-pinned golangci-lint binary and only builds
it with go install on cache miss. The official prebuilt action was evaluated,
but the prebuilt v2.1.6 binary targets an older Go toolchain than this repo and
newer prebuilt lint releases change the enforced lint set. GoReleaser Snapshot
now runs on pull requests, main, release tags, nightly schedule, and manual
dispatch so packaging signal is available before and after merge.
Detent separates host-level orchestration from per-project workflow:
- The resolved global config file lists projects and host-level scheduling settings.
- Each project has its own
WORKFLOW.mdwith tracker credentials, states, workspace rules, Codex settings, budgets, hooks, and agent instructions.
A minimal global config looks like this:
apiVersion: detent/v1
kind: GlobalConfig
env: prod
log_level: info
github_token: gh
api_token: detent_replace_with_random_secret
port: 4000
instance_name: buildbox
update:
auto_check_enabled: false
check_interval_hours: 24
auto_apply_enabled: false
global:
max_concurrent_agents: 8
scheduling: weighted
fair_share:
half_life: 1h
startup:
jitter_seconds: 10
max_spawn_per_second: 2
max_concurrent_starts: 4
projects:
- id: detent
workflow: /absolute/path/to/detent/WORKFLOW.md
workdir: /absolute/path/to/detent
color: "#1192e8"
weight: 2
priority: 1
- id: website
workflow: /absolute/path/to/website/WORKFLOW.md
workdir: /absolute/path/to/website
weight: 1
priority: 3
paused: trueProject weights are relative scheduling weights. Higher weights receive a
larger dispatch share in weighted and fair-share scheduling modes. Project
priority is a rank: 0 is highest and 4 is lowest.
Set optional projects[].color to an opaque CSS hex color in #RGB or
#RRGGBB form when a project needs a fixed visual marker. The sidebar,
project cards, and top-level multi-project Kanban board keep the project name
or ID visible and use color only as an additional compact marker. Projects
without a configured color receive a deterministic automatic color from a
curated categorical palette based on the project ID, so colors remain stable
across restarts and do not depend on project order. When there are more
projects than palette entries, Detent deterministically reuses palette colors;
labels and project IDs remain the primary identifiers.
Set projects[].workflow_ref only after the workflow file already exists at
that git ref, such as after the first WORKFLOW.md merge to origin/main.
When set, the workflow file is read from a git ref in the configured source
checkout instead of the checkout's working-tree copy. workflow may be an
absolute path under workdir or a repository relative path such as
WORKFLOW.md. When the ref advances, Detent reloads the workflow content from
that ref; when workflow_ref is omitted, Detent keeps reading the working-tree
file. If workflow_ref points at a ref that does not contain the workflow file,
detent doctor reports a load failure for <ref>:WORKFLOW.md.
Use the project administration commands to edit global.yaml:
detent add-project \
--id <id> \
--workflow <WORKFLOW.md> \
--workdir <dir> \
--weight 1 \
--priority 3
detent pause <id>
detent unpause <id>
detent promote <id> --priority 1
detent remove-project <id>These commands persist the global config. A running Detent process watches the
active global.yaml, including symlinked config targets, and reconciles
supported live-reload fields without a process restart. Invalid edits are
logged and ignored while the last valid config stays live.
Paused projects do not run workflow watchers or periodic workflow reconciliation.
detent unpause <id> synchronously reloads the project's current WORKFLOW.md
before dispatch resumes, so edits made while paused take effect on unpause. If
the current workflow cannot be loaded or prepared, unpause returns the error and
the project remains paused.
For projects whose workflow file is already present on the target branch, you
can include --workflow-ref origin/main during registration or add
workflow_ref: origin/main to the project entry later.
| Field | Reload behavior |
|---|---|
| Project list and project settings | Live reload |
Credentials: github_token and project credentials |
Live reload |
global.startup |
Live reload |
instance_name |
Live reload |
global.identity |
Live reload; project runtimes restart in-process and /api/v1/state.instance.name updates after the next telemetry snapshot |
global.max_concurrent_agents, global.scheduling, global.fair_share |
Live reload at the next dispatch decision; lowering capacity drains to the new limit without interrupting active workers |
log_level |
Live reload |
port, env, log_max_size_bytes, log_max_backups |
Restart required |
When a changed field requires restart, Detent logs
global config setting change requires restart with the field name.
Run more than one Detent instance when a single GitHub ProjectV2 board should
be split across independent workers. Each instance is a separate detent
process with its own global.yaml, process identity, authorization selector,
runtime database, listener address, and claim lease. The instances may point at
the same tracker.project_slug,
but their authorization selectors should be disjoint so each issue belongs to
one worker set before claiming begins.
Use global.identity for the process identity in multi-instance operation.
That identity is applied to every project in that global.yaml and overrides
workflow-level identity while the project is loaded from global config. A
workflow can still define top-level identity for single-project runs, but do
not put identity under a projects entry in global.yaml; project entries only
carry scheduling, paths, credentials, pause state, and authorization selectors.
apiVersion: detent/v1
kind: GlobalConfig
global:
max_concurrent_agents: 4
scheduling: weighted
identity:
name: detent-alpha
github_login: detent-alpha
ownership_mode: field
owner_field: Detent Owner
projects:
- id: detent-alpha
workflow: /absolute/path/to/detent/WORKFLOW.md
workdir: /absolute/path/to/detent
weight: 1
priority: 1
authorization:
labels:
include:
- scope:alphaA second instance can use the same workflow and board with a different identity and a non-overlapping selector:
apiVersion: detent/v1
kind: GlobalConfig
global:
max_concurrent_agents: 4
scheduling: weighted
identity:
name: detent-beta
github_login: detent-beta
ownership_mode: field
owner_field: Detent Owner
projects:
- id: detent-beta
workflow: /absolute/path/to/detent/WORKFLOW.md
workdir: /absolute/path/to/detent
weight: 1
priority: 1
authorization:
labels:
include:
- scope:betaThe selector schema is the same in projects[].authorization and
tracker.authorization: assignee_in, author_in, priority_in,
labels.include, labels.exclude, fields, and, and or.
projects[].authorization from global.yaml is combined with
tracker.authorization from WORKFLOW.md as an and, so both selectors must
match. Use @me inside assignee_in, author_in, or field selector values to
match the current instance identity (github_login and name). For example,
one common pattern is a global project selector for a broad lane label and a
workflow selector for a board field:
tracker:
authorization:
fields:
- name: Workstream
value: engineeringAuthorization only decides which issues an instance is allowed to consider. Claiming is the final concurrent-dispatch guard. Enable it in the shared workflow so all instances use the same lease field and TTL:
tracker:
claims:
enabled: true
lease_field: Detent Lease
ttl_seconds: 900
heartbeat_seconds: 120When claims are enabled, Detent writes ownership first, then writes
lease_field with a UTC RFC3339 timestamp, refetches the issue, and dispatches
only if the refreshed owner and lease still match the current instance. With
ownership_mode: assignee, ownership is the GitHub assignee and owner_field
must be omitted. With ownership_mode: field, ownership is written to
identity.owner_field, which must exist on the board. While another owner has
a fresh lease, the issue is skipped. When the lease timestamp is stale by
ttl_seconds or missing, another matching instance may reclaim it. Detent
refreshes running claim leases every heartbeat_seconds; that value must be
greater than zero and less than or equal to ttl_seconds.
Task-to-model routing also lives in WORKFLOW.md. If agents.backends is
omitted, routes can reference the legacy codex backend built from the top-level
codex block. Routes are evaluated in order, skipping defaults first; the first
non-default selector match wins, then the single default route is used. A
route can set a fixed model, read a model from a ProjectV2 field with
model_field, or fall back to an issue model override when neither is set.
Routes without role are code-agent routes. Runner.Run dispatches plan mode
with role: plan, Rework-state issues with role: rework, Merging-state
issues with role: merge, and all other implementation dispatches with
role: code. Set role: validator to give the validator-agent review its own
backend/model route when gate.validator.enabled is true. If a stage-specific
route does not match, Detent falls back to that role's default route and then to
the code default route, preserving the zero-config behavior.
If the validator runs through the Codex backend, prefer setting
gate.validator.model: gpt-5.4-mini as the cheap-tier override before adding a
separate validator route. Treat rework-rate per validator model as the quality
signal once cache/model telemetry lands; increase the validator tier only when
that rate worsens.
agents:
routes:
- name: plan-cheap
role: plan
backend: codex
model: gpt-5.4-mini
- name: rework-high-context
role: rework
backend: codex
model: gpt-5-codex-high
- name: merge-standard
role: merge
backend: codex
model: gpt-5-codex
- name: high-context
backend: codex
model: gpt-5-codex-high
selector:
labels:
include:
- model:high
- name: board-model
backend: codex
model_field: Model
- name: default
backend: codex
model: gpt-5-codex
default: trueFor explicit backend profiles, configure agents.backends and route to those
ids. Supported backend kinds are codex with protocol: app-server and
claude_code with protocol: headless. Codex backend options use the same
runtime fields as the top-level codex block, including shell,
approval_policy, thread_sandbox, turn_sandbox_policy, turn_timeout_ms,
read_timeout_ms, and stall_timeout_ms. Claude Code backend options
include permission_mode, allowed_tools, disallowed_tools,
include_partial_messages, turn_timeout_ms, stall_timeout_ms, shell, and
extra_args. When a Codex backend needs different configuration, launch
codex app-server with a dedicated CODEX_HOME or -c overrides. When a
Claude Code backend needs isolated state, launch claude with a dedicated
CLAUDE_CONFIG_DIR.
agents:
backends:
- id: codex-standard
kind: codex
protocol: app-server
command: codex app-server
- id: codex-high
kind: codex
protocol: app-server
command: env CODEX_HOME=/opt/detent/codex-high codex app-server
- id: claude-worker
kind: claude_code
protocol: headless
command: env CLAUDE_CONFIG_DIR=/var/lib/detent/claude/worker-1 claude
options:
permission_mode: bypassPermissions
allowed_tools:
- Bash
- Edit
disallowed_tools:
- WebFetch
extra_args:
- --no-session-persistence
routes:
- name: validator
role: validator
backend: claude-worker
model: fable
- name: high-label
backend: codex-high
model: gpt-5-codex-high
selector:
labels:
include:
- model:high
- name: default
backend: codex-standard
model: gpt-5-codex
default: trueClaude Code auth is ambient and the backend is auth-agnostic. A logged-in
claude CLI uses the operator's subscription login. Setting
ANTHROPIC_API_KEY in the Detent worker environment switches the same backend
to API billing, and that key takes precedence over the subscription login.
Detent stores no Anthropic keys, mirroring the way Codex credentials stay
outside Detent.
Claude Pro/Max subscription limits are opaque in headless claude -p mode.
The 5-hour windows and weekly caps do not expose an in-band "limit approaching"
signal; a cap hit appears only as an error result from the turn. Use
subscription auth for bounded or bursty personal operation. Use
ANTHROPIC_API_KEY for sustained or parallel fleet runs where predictable
billing and capacity matter.
For fleet isolation, set a distinct CLAUDE_CONFIG_DIR per worker process so
concurrent claude invocations do not race on config or session state. Add
--no-session-persistence through options.extra_args when workers do not
need Claude Code session continuity; otherwise sessions accumulate under
~/.claude/projects/.
The sandbox model differs by backend. Codex runs turns under an OS-level
workspace-write sandbox. Claude Code headless runs inside Detent's isolated
git worktree with permission_mode: bypassPermissions, but that is not an OS
sandbox: allowed shell tools can still access the host as the Detent worker
user. Treat the worktree as the checkout boundary, use container, VM, or OS
sandbox isolation when you need a hard blast-radius boundary, and tighten role
exposure with allowed_tools plus disallowed_tools. Choose backend routes
with that trade-off in mind, especially for roles that can execute shell
commands or edit files.
For local Anthropic-compatible inference, point ANTHROPIC_BASE_URL at a local
server such as Ollama, which has native Anthropic API compatibility as of
January 2026, and keep using the claude_code backend. See
Local Models With Codex And Ollama for the
model sizing and context-window checks that also apply when evaluating local
agent backends.
The dashboard and /api/v1/state surface each instance identity, authorization
scope, owner, lease renewal time, lease expiry, and selected model usage, which
lets operators verify that scoped instances are not contending for the same
work.
The web dashboard starts with the main detent command. In running mode it
shows live counts, running issues, retry queue, blocked work, completed
sessions, token totals, budget status, Codex rate-limit snapshots, and GitHub
REST and GraphQL rate-limit snapshots with per-cycle query cost contributors
when the GitHub connector reports them. The shared GitHub API health indicator
separates primary quota exhaustion from observed secondary REST backoff, so a
healthy primary budget can still show backoff when GitHub returned 429 for
endpoint families such as pull requests or check runs. Open the indicator to see
remaining primary quota, reset times, backed-off endpoint families, last status
codes, retry timing, and tracker refresh timing.
When an agent backend reports model_context_window, Detent also surfaces
context pressure for running and recent sessions. Context pressure is the
session's total_tokens divided by the model context window. The JSON snapshot
includes context_pressure.total_tokens, context_limit_tokens,
percent_used, and threshold_state; the web dashboard and TUI render the
same value as a compact percent. Thresholds are normal below 70%, watch at
70%, warning at 85%, and critical at 95%. Unknown context windows omit the
derived fields instead of reporting a misleading zero.
Context pressure is a model-window signal, not a Detent stop threshold.
agent.max_session_tokens is an absolute configured ceiling for a session.
total_tokens counts input, output, cache-created, and cache-read tokens,
accumulated across every turn of the session — cached context is re-counted on
each turn, so a healthy session accrues millions of tokens within minutes.
Size max_session_tokens as a runaway-session guard; a value near one turn's
worth of context terminates every session at its ceiling.
agent.max_session_context_multiplier derives a ceiling from the reported
context window when that window is known. A session can show high context
pressure before either ceiling is exceeded, and a low-pressure session can
still hit a lower absolute agent.max_session_tokens value. Token rows also
show cache-read efficiency when cached input is reported: cached input divided
by input tokens. Use that value with context pressure to evaluate whether
thread-resume behavior is preserving useful context without repeatedly filling
the window.
budget.billing_mode accepts metered or subscription. Metered mode
enforces configured USD caps and the spend-progress breaker. Subscription mode
keeps notional spend telemetry but never refuses dispatch or parks work based
on USD; Detent instead scales dispatch concurrency with the lowest reported
primary or secondary provider rate-window percentage. An omitted mode preserves
legacy metered enforcement when budget.enabled=true, and detent doctor warns
until the mode is declared.
agent.no_progress_spend_limit_usd defaults to a base limit of 3, below the
default per-issue budget backstop. The effective limit scales with the session's
reasoning effort: unknown/low 1x, medium 1.5x, high 3x, xhigh 6x, and
max/ultracode 8x. These multipliers assume one retry at the observed cost
profile should fit before the breaker fires; detent doctor warns when an
effort tier's effective limit is below its observed p50 per-session cost and
recommends a base limit with 1.5x retry-cost headroom.
Detent sums persisted session cost for each issue after its latest accepted
lane or PR advancement. PR creation, a new head commit, a dirty-to-clean
mergeability transition, and a failing-to-passing CI transition each reset the
spend window. Spend accumulates only while the PR fingerprint remains static.
In metered mode, when spend exceeds the effective limit, Detent parks the issue
in Blocked and identifies whether no PR evidence was produced or a linked PR
remained static. The latter points operators toward merge-train capacity and
serialization tuning; the former recommends narrowing or splitting the task.
The next worker must explain the missing progress signal in its first Workpad
update before using tools. Set the value to 0 to disable the breaker and its
history/spend lookups. In subscription mode, the same calculation is advisory
telemetry and never parks the issue.
agent.failure_breaker pauses new project dispatches when the same failure
class reaches same_class_limit attempts inside window_seconds. The default
is five matching failures in one hour, followed by a one-hour cooldown. After
the cooldown or a workflow reload, Detent permits exactly one canary attempt;
a success or different failure class closes the breaker, while the same class
starts a fresh cooldown. The board banner shows the active class, count, and
window.
Daily budget caps are scoped to the configured project. Session rows persist
the project ID; on upgrade, Detent backfills older rows first from work
attempts and then by matching each identifier's repository prefix against the
configured project registry. Any session that remains unattributed counts
toward every project's daily cap as a conservative fallback. detent doctor
warns while unattributed completed sessions exist for the current UTC day.
agent.resume_orphaned_sessions defaults to true. After an unclean Detent
restart, active sessions whose provider identity was journaled are preflighted
and resumed with a short continuation prompt. Missing provider state,
unsupported backends, and failed resume handshakes automatically fall back to
the full fresh continuation prompt. Set the field to false to retain fresh
redispatch behavior for every restart.
Completed issues persist an efficiency receipt built from session, attempt, usage, and workflow-lane rows. Receipts appear in the project Runs table and issue detail sheet; Reports shows per-merged-issue percentiles, cache share, first-attempt merge rate, dwell decomposition, anomalies, and a trailing-window baseline. The default anomaly threshold is 3x the project baseline and can be changed per workflow. OTLP lifecycle export is optional and disabled when no endpoint is configured:
observability:
efficiency:
anomaly_tokens_multiple: 3
anomaly_sessions_multiple: 3
anomaly_dwell_multiple: 3
otlp:
endpoint: http://127.0.0.1:4318
service_name: detent
timeout_ms: 5000The exporter posts OTLP HTTP/JSON traces to /v1/traces with linked
detent.dispatch, detent.session, detent.gate, and detent.merge spans.
Static collector headers may be supplied with observability.otlp.headers.
Useful endpoints:
| Route | Purpose |
|---|---|
/ |
Web dashboard. |
/kanban |
Read-only fleet Kanban board across all registered projects. The sidebar link appears only when more than one project is registered. |
/projects/<id> |
Project-scoped dashboard overview. |
/projects/<id>/kanban |
Project-scoped Kanban board; read-only or integration mode follows that project's workflow config. |
/projects/<id>/runs |
Project running, retry, blocked, and recent session details. |
/projects/<id>/configuration |
Project workflow and runtime configuration view. |
/projects/<id>/diagnostics |
Project health, board flow, and telemetry diagnostics. |
/settings |
Fleet settings and configuration summary. |
/reports |
Usage reports for spend, tokens, projects, issues, PRs, and models. |
/health |
Server health and configured dependency checks. |
/events |
Server-sent dashboard updates. Use ?view=kanban for the fleet board and ?project=<id>&view=kanban for a project board. |
/api/v1/state |
JSON telemetry snapshot. |
/api/v1/timeseries?window=10m&bucket=1m |
Fleet chart samples for running agents, tokens/sec, and completions. |
/api/v1/projects/<id>/state |
Project-scoped JSON telemetry snapshot. |
/api/v1/projects/<id>/timeseries?window=10m&bucket=1m |
Project chart samples for running agents, token spend, and board flow. |
/api/v1/projects/<id>/work-items |
Create a runtime work item with POST for local_sqlite and github_local trackers. |
/api/v1/refresh |
Request an orchestrator refresh with POST. |
/api/v1/webhooks/github |
Accept signed GitHub webhook deliveries with POST. |
/api/v1/<issue> |
JSON detail for a running, retrying, or blocked issue. |
Configure a top-level api_token in global.yaml, or set
DETENT_API_TOKEN to override it at runtime. Use a high-entropy value; the
recommended shape is a detent_ prefix followed by a random secret. Mutating
API routes require Authorization: Bearer <token> or X-API-Key: <token>.
When a token is configured, read-only GET /api/v1/* routes require it too.
GET /health stays unauthenticated, and the GitHub webhook keeps its HMAC
signature check.
If Detent binds a non-loopback host such as 0.0.0.0 without an api_token,
API routes fail closed and mutating routes return 403 until a token is
configured. With no token on loopback, read-only API routes remain open for
local development.
Create a runtime work item:
curl -fsS -X POST "http://127.0.0.1:4000/api/v1/projects/digitaldrywood-video/work-items" \
-H "Authorization: Bearer $DETENT_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"title": "Author beat visuals",
"description": "Full markdown brief.",
"state": "Todo",
"labels": ["video-assets"],
"fields": {"render_status": "queued"},
"priority": 2,
"deliverable": {
"kind": "artifact",
"review_url": "http://127.0.0.1:8090/v/example/g/assets"
}
}'The response is 201 with {"id":"...","identifier":"...","url":"..."}.
Duplicate submitted identifiers return 409, invalid states or missing title
and description return 422, unknown projects return 404, and trackers other
than local_sqlite or github_local return 501.
The CLI uses the same validation and writes through the configured tracker directly:
detent work-item add digitaldrywood-video \
--title "Author beat visuals" \
--body-file brief.md \
--label video-assets \
--field render_status=queued \
--priority 2 \
--deliverable-review-url "http://127.0.0.1:8090/v/example/g/assets" \
--format jsonThe terminal TUI renders the same telemetry snapshot model for terminal-first
operator surfaces. The default binary path starts the web dashboard; embedding
the TUI uses the internal/tui Bubble Tea model with a telemetry hub.
The standing Go-vs-Elixir parity checklist is maintained in docs/parity-audit.md.
Common development commands:
make setup
make dev
make check
make modernize-checkmake dev runs Air with ENV=dev and
LOG_LEVEL=debug, builds a dev-versioned ./tmp/detent with the current
commit SHA and build date, rotates
tmp/air-combined.log, and streams combined build and application output to
tmp/air-combined.log.
make check runs the local release gate: build, golangci-lint, go vet,
NilAway, race tests, and the 70 percent coverage check. Run make generate
before committing changes to Templ templates, sqlc queries, or Tailwind inputs.
make modernize-check runs the Go modernizer diff check with the repo's
selected safe analyzer set.
Packages that own transport, hub, watcher, orchestrator, and runner goroutines
also run go.uber.org/goleak from package-level tests, so go test ./...,
race tests, and make check fail on unexpected goroutines. Add goleak ignores
only in the package that needs them, and only after identifying the dependency
or intentionally shared test goroutine.
Nil safety is enforced by make check and can also be run directly while
iterating:
make nilaway-auditThe project uses the standalone NilAway command instead of golangci-lint
integration because the linter integration requires a custom module-plugin
binary. Go 1.26's experimental runtime/pprof goroutineleak profile remains a
runtime audit aid behind GOEXPERIMENT=goroutineleakprofile; the stable CI
coverage for now is the goleak-backed test gate.
See CONTRIBUTING.md for the full contributor workflow.
Detent logs with log/slog.
ENV=dev,development, orlocalenables tint text logs.ENV=prodor any other non-development value keeps JSON logs.- When no environment is configured, Detent defaults to
prod. LOG_LEVELacceptsdebug,info,warn,warning, anderror.--envand--log-leveloverride environment variables for one run.DETENT_ENVandDETENT_LOG_LEVELremain deprecated fallbacks for one release. The unprefixed names win when both are set.- Text logs are written to stdout; JSON logs are written to stderr.
- The terminal dashboard writes JSON logs to
detent.lognext to the runtime database.log_max_size_bytesdefaults to 52428800 andlog_max_backupsdefaults to 5. - Per-request GitHub REST request/response debug logs are off by default. Set
tracker.github_rest_debug_logging: trueonly while diagnosing REST traffic.
Detent command output is selected by --format pretty|json. The explicit flag
wins, then DETENT_FORMAT, then the stdout terminal check. Interactive
terminals default to pretty; pipes, redirects, and agent subprocesses default
to json. JSON is written to stdout. Progress and logs that would corrupt a
JSON stdout stream are written to stderr in JSON mode.
This changes piped output for scripts that parsed the old prose output. Use
--format pretty for a single command or DETENT_FORMAT=pretty for a process
environment that must keep the old text shape.
Structured command objects:
| Command | JSON object |
|---|---|
detent version |
{"version":"v0.1.0","commit":"abc1234","build_date":"2026-06-13T00:00:00Z","go_version":"go1.26.4","os":"linux","arch":"amd64"} |
detent update |
The update status object, including current_version, latest_version, latest_tag, update_available, install_source, action, message, and command when present. |
detent init |
{"status":"ok","path":"/path/global.yaml","rule":"--config"} |
detent add-project |
{"id":"api","workflow":"/repo/WORKFLOW.md","workdir":"/repo","weight":1,"priority":0,"paused":false,"credential_ref":"github"} |
detent pause api / detent unpause api |
{"status":"ok","project":"api","paused":true} |
detent promote api --priority 1 |
{"status":"ok","project":"api","priority":1} |
detent remove-project api |
{"status":"ok","project":"api","removed":true} |
detent work-item add api --title "..." --body "..." |
{"id":"wi-...","identifier":"wi-...","url":"/projects/api/kanban"} |
detent config path |
{"path":"/path/global.yaml","rule":"--config"} |
detent doctor |
{"checks":[{"name":"Config resolution","status":"OK","detail":"...","hint":"..."}],"summary":{"ok":8,"warn":0,"fail":0},"result":"PASS"} |
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.
If no global config is found, Detent keeps the single-project fallback and looks for WORKFLOW.md in the current working directory. 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 | |
| Web port | --port |
PORT |
port |
4000 |
| Instance name | instance_name |
short hostname | ||
| Automatic update checks | update.auto_check_enabled |
false |
||
| Update check interval | update.check_interval_hours |
24 |
||
| Automatic update apply | update.auto_apply_enabled |
false |
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.
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.
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. In
single-project fallback mode without global.yaml, workflow top-level
identity.name is used before the short hostname. Names are trimmed, must be a
single line, and are capped at 40 characters in the web UI.
Automatic update checks are host-specific and disabled by default. When
enabled, Detent checks on a jittered in-process schedule and reports the last
check, available or applied version, and next check on /health and the Health
dashboard. detent doctor reports whether the host is opted in and suggests
enabling checks when it is not. Automatic apply remains off unless explicitly
enabled and only replaces release-installer binaries; other install sources
remain notification-only. A successful automatic 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.
Detent began as an Elixir/OTP implementation of
OpenAI's Symphony — the open spec for
orchestrating Codex agents from a project board — adapted from Symphony's Linear
target to GitHub Projects v2. It is now a ground-up Go rewrite: one CGO-free
binary instead of a BEAM service, plus multi-project orchestration, the gated
merge train, a richer operator dashboard, detent doctor, Windows support, and
a GoReleaser pipeline. That earlier Elixir implementation is archived.
Detent is released under the MIT license. See LICENSE.