Agent-Executable Project Onboarding
This runbook takes a target repository from zero Detent setup to one dispatched
issue, or adds a new project to an existing Detent install. It starts with an
identity gate so a reference repository, example setup, current shell directory,
or existing Detent project cannot become the implicit target. Replace every
<...> placeholder before running a command.
detent.yaml is the schema-versioned Detent machine contract. WORKFLOW.md
contains portable agent instruction prose only. Their optional gitignored local
overlays are detent.local.yaml and WORKFLOW.local.md, respectively. All
structured Detent paths named in this runbook belong in detent.yaml.
Use these placeholders consistently:
| Placeholder | Meaning |
|---|---|
<customer-id> |
Customer or workstream id being onboarded. |
<repo-owner> |
GitHub owner of the target repository. |
<repo-name> |
GitHub repository name. |
<source-root> |
Local checkout of <repo-owner>/<repo-name>. |
<reference-repositories> |
Comma-separated owner/repo list used only for docs, examples, or tooling, or empty. |
<worktree-root> |
Directory where Detent will create issue worktrees. |
<github-mode> |
project_v2 for GitHub ProjectV2-backed status, issue_field for boardless repository issue-field status, or label for repository issue-label status. |
<project-owner> |
GitHub org or user that owns the ProjectV2 board. |
<project-number> |
ProjectV2 number shown by gh project list. |
<project-node-id> |
ProjectV2 node id, starting with PVT_. |
<status-field-name> |
Organization issue field Detent uses as status in boardless mode, usually Status. |
<status-label-prefix> |
Repository label prefix Detent uses for status labels in label mode, usually detent:. |
<write-probe-issue> |
Scratch issue reference such as <repo-owner>/<repo-name>#123 for legacy/deep doctor write probes. |
<detent-project-id> |
Local global.yaml project id, such as api. |
Source Freshness Gate
Before relying on this runbook, README.md, or detent command
recommendations, pin the Detent documentation source to a concrete canonical
commit from digitaldrywood/detent. A local Detent checkout is optional. Use
the remote GitHub 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 the operator explicitly
asks for a local source checkout.
Remote Detent Documentation Source
Use GitHub as the first-class documentation source and record the pinned commit in the initial evidence packet:
ONBOARDING_DIR="${ONBOARDING_DIR:-${TMPDIR:-/tmp}/detent-onboarding-<repo-owner>-<repo-name>}"
mkdir -p "$ONBOARDING_DIR"
DETENT_DOCS_ACCESS_METHOD=github_api
DETENT_DOCS_REPOSITORY=digitaldrywood/detent
DETENT_DOCS_REF=main
DETENT_DOCS_COMMIT="$(
gh api repos/digitaldrywood/detent/git/ref/heads/main --jq '.object.sha'
)"
DETENT_VERSION_JSON="$(detent --format json version 2>/dev/null || true)"
DETENT_BINARY_VERSION="$(printf '%s\n' "$DETENT_VERSION_JSON" | jq -r '.version // empty' 2>/dev/null || true)"
DETENT_BINARY_COMMIT="$(printf '%s\n' "$DETENT_VERSION_JSON" | jq -r '.commit // empty' 2>/dev/null || true)"
DETENT_BINARY_BUILD_DATE="$(printf '%s\n' "$DETENT_VERSION_JSON" | jq -r '.build_date // empty' 2>/dev/null || true)"
if test -n "$DETENT_BINARY_COMMIT" && test "$DETENT_BINARY_COMMIT" = "$DETENT_DOCS_COMMIT"; then
DETENT_BINARY_MATCHES_CANONICAL=true
else
DETENT_BINARY_MATCHES_CANONICAL=false
fi
{
printf 'DETENT_DOCS_ACCESS_METHOD=%s\n' "$DETENT_DOCS_ACCESS_METHOD"
printf 'DETENT_DOCS_REPOSITORY=%s\n' "$DETENT_DOCS_REPOSITORY"
printf 'DETENT_DOCS_REF=%s\n' "$DETENT_DOCS_REF"
printf 'DETENT_DOCS_COMMIT=%s\n' "$DETENT_DOCS_COMMIT"
printf 'DETENT_BINARY_VERSION=%s\n' "$DETENT_BINARY_VERSION"
printf 'DETENT_BINARY_COMMIT=%s\n' "$DETENT_BINARY_COMMIT"
printf 'DETENT_BINARY_BUILD_DATE=%s\n' "$DETENT_BINARY_BUILD_DATE"
printf 'DETENT_BINARY_MATCHES_CANONICAL=%s\n' "$DETENT_BINARY_MATCHES_CANONICAL"
printf 'DETENT_VERSION_JSON=%s\n' "$DETENT_VERSION_JSON"
} > "$ONBOARDING_DIR/detent-source-freshness.env"
test -n "$DETENT_DOCS_COMMIT"
Read required Detent files from GitHub at the pinned commit. Required file coverage is README.md AGENTS.md CLAUDE.md docs/ONBOARDING.md CONTRIBUTING.md, build and language manifests, .github/workflows docs/templates, install scripts, workflow examples, and any existing WORKFLOW.md or global.yaml examples.
for path in README.md AGENTS.md CLAUDE.md docs/ONBOARDING.md CONTRIBUTING.md Makefile go.mod; do
gh api "repos/$DETENT_DOCS_REPOSITORY/contents/$path?ref=$DETENT_DOCS_COMMIT" \
--jq '.content' 2>/dev/null | base64 --decode || true
done
gh api "repos/$DETENT_DOCS_REPOSITORY/contents/.github/workflows?ref=$DETENT_DOCS_COMMIT" \
--jq '.[].path' 2>/dev/null || true
gh api "repos/$DETENT_DOCS_REPOSITORY/contents/docs/templates?ref=$DETENT_DOCS_COMMIT" \
--jq '.[].path' 2>/dev/null || true
Local Detent Source Checkout
If a local Detent checkout is explicitly known and verified, you may use it as
a local documentation source. Keep it separate from the target repository, set
DETENT_SOURCE_ROOT only after verifying the checkout root, then compare it to
the canonical repository:
DETENT_SOURCE_ROOT="<absolute-local-detent-source-checkout>"
git -C "$DETENT_SOURCE_ROOT" fetch origin main:refs/remotes/origin/main
DETENT_SOURCE_HEAD="$(git -C "$DETENT_SOURCE_ROOT" rev-parse HEAD)"
DETENT_CANONICAL_MAIN="$(git -C "$DETENT_SOURCE_ROOT" rev-parse refs/remotes/origin/main)"
if test "$DETENT_SOURCE_HEAD" = "$DETENT_CANONICAL_MAIN"; then
DETENT_SOURCE_MATCHES_CANONICAL=true
else
DETENT_SOURCE_MATCHES_CANONICAL=false
fi
{
cat "$ONBOARDING_DIR/detent-source-freshness.env" 2>/dev/null || true
printf 'DETENT_SOURCE_ROOT=%s\n' "$DETENT_SOURCE_ROOT"
printf 'DETENT_SOURCE_HEAD=%s\n' "$DETENT_SOURCE_HEAD"
printf 'DETENT_CANONICAL_MAIN=%s\n' "$DETENT_CANONICAL_MAIN"
printf 'DETENT_SOURCE_MATCHES_CANONICAL=%s\n' "$DETENT_SOURCE_MATCHES_CANONICAL"
} > "$ONBOARDING_DIR/detent-source-freshness.next.env"
mv "$ONBOARDING_DIR/detent-source-freshness.next.env" "$ONBOARDING_DIR/detent-source-freshness.env"
If the local checkout is absent, stale, or cannot be proven current, do not read
local Detent docs from it. Continue from the remote GitHub documentation source
at DETENT_DOCS_COMMIT instead. Stop before Phase 2 recommendations only when
neither a pinned remote documentation source nor a verified local documentation
source is available, or when the installed binary must be present for a command
recommendation and cannot be verified. Include
$ONBOARDING_DIR/detent-source-freshness.env in the initial evidence packet so
the operator can see the documentation repository, ref, commit SHA, access
method, binary version/commit/build date when available, and whether the
binary/docs match canonical.
Start Here — Determine The Mode
Do not assume this is a fresh install, and do not silently choose the target
project from the current directory or a pasted repository URL. In this runbook,
do not assume means infer a candidate from identity-safe local evidence and
confirm it with the operator; it does not mean ask for raw answers.env fields
first. You may inspect Detent installation evidence and current-checkout
identity before the identity gate, but repository-specific discovery waits for
Phase 0.5.
Classify the work into one of these modes:
new-install: Detent is not installed or the human wants a fresh host. Follow Bootstrap On A New Machine through tool installation and authentication, then continue with this runbook.existing-install: Detent is already installed or a service/dashboard appears to be running. Verify the binary, config path, registered projects, service health, GitHub auth, Codex auth, and read-onlydetent doctor --port 0before proposing changes. Do not pass--allow-write-probesduring this identity-safe verification; defer doctor write probes until Phase 2.5 authorizes GitHub mutations.add-project: An existing Detent install is present and the target repository is not registered yet. Reuse the existingglobal.yaml, preserve current runtime settings unless the human chooses otherwise, then create or adopt a board, authorWORKFLOW.md, register the project, and smoke test.
Record only Detent install/mode and identity-safe current-checkout evidence before the identity interview:
ONBOARDING_DIR="${TMPDIR:-/tmp}/detent-onboarding-<repo-owner>-<repo-name>"
mkdir -p "$ONBOARDING_DIR"
{
pwd
git rev-parse --show-toplevel 2>/dev/null || true
git remote get-url origin 2>/dev/null || true
command -v detent || true
detent version 2>/dev/null || true
cat "$ONBOARDING_DIR/detent-source-freshness.env" 2>/dev/null || true
detent --format pretty config path 2>/dev/null || true
gh auth status 2>&1 || true
codex --version 2>/dev/null || true
} > "$ONBOARDING_DIR/mode-evidence.txt"
if command -v detent >/dev/null 2>&1; then
detent --format pretty config path \
> "$ONBOARDING_DIR/global-path.txt" 2>/dev/null || true
GLOBAL_CONFIG="$(
awk '/^path:/ {print $2}' "$ONBOARDING_DIR/global-path.txt" 2>/dev/null || true
)"
if test -n "$GLOBAL_CONFIG" && test -f "$GLOBAL_CONFIG"; then
sed -n '1,240p' "$GLOBAL_CONFIG" > "$ONBOARDING_DIR/global-config.before.txt"
awk '/^projects:/ {show=1} show {print}' "$GLOBAL_CONFIG" \
> "$ONBOARDING_DIR/global-projects.txt"
fi
fi
test -s "$ONBOARDING_DIR/mode-evidence.txt"
If Detent appears to be running, verify the live service before changing config.
Use the configured port when known, and use detent doctor --port 0 when the
live process already owns the dashboard port. This early doctor run is
read-only; do not pass --allow-write-probes before Phase 2.5:
detent doctor --port 0
curl -fsS http://127.0.0.1:<port>/health | jq -e '.status == "ok"'
curl -fsS http://127.0.0.1:<port>/api/v1/state
Before adding a project to an existing install, check whether it is already registered and decide whether this is a new registration or a repair/update:
rg -n 'id: <detent-project-id>|workflow: .*<repo-name>|workdir: .*<repo-name>' \
"$ONBOARDING_DIR/global-config.before.txt" 2>/dev/null || true
Treat existing registered projects as examples only. Do not reuse tracker mode, status namespace, validation gate, workspace root, dashboard bind, scheduling priority or weight, auto-promote policy, review policy, or mutation scope unless the operator explicitly accepts that setting for this customer/project.
Phase 0 — Preconditions
-
Confirm Detent is installed or intentionally new. For
new-install, follow Bootstrap On A New Machine steps 1-3 before project onboarding. Forexisting-installandadd-project, verify the detected binary and config path before changing anything. Verify:command -v detent detent version detent --format pretty config path -
Confirm GitHub CLI auth and scopes. Detent needs GitHub auth for the selected tracker mode. Before the identity gate, verify only the local
ghauth state and token scopes. Do not list target ProjectV2 boards, organization issue fields, repository labels, or target issues until Phase 0.5 identity is confirmed and Phase 2 recordsGITHUB_MODE.ProjectV2 mode needs repository, organization, and ProjectV2 read/write scopes. Boardless issue-field mode needs repository issue access plus organization issue-field read access; classic PATs use
repoandread:org. Label mode needs repository issue and label read/write access; classic PATs can userepo.For ProjectV2 mode, request:
gh auth login --scopes "repo,read:org,read:project,project"If any ProjectV2 scope check fails for existing auth, refresh the token:
gh auth refresh -h github.com --scopes "repo,read:org,read:project,project"For boardless issue-field mode with a classic PAT, request:
gh auth login --scopes "repo,read:org"For repository label mode with a classic PAT, request:
gh auth login --scopes "repo"In a remote or headless shell, avoid launching a remote GUI browser while preserving the terminal-side device flow:
BROWSER=/usr/bin/true gh auth refresh -h github.com --scopes "repo,read:org,read:project,project"Press Enter in the terminal so
ghstarts polling, openhttps://github.com/login/devicein the operator browser, enter the code, and wait for the terminal to print authentication completion. Verify each required scope independently:gh auth status gh auth status 2>&1 | rg '\brepo\b' gh auth status 2>&1 | rg '\bread:org\b' # ProjectV2 mode only: gh auth status 2>&1 | rg '\bread:project\b' gh auth status 2>&1 | rg "(^|[[:space:],'\"])project([[:space:],'\"]|$)"After Phase 0.5 identity confirmation and the explicit Phase 2
GITHUB_MODEanswer, run only the selected mode's target-specific read probe:gh project list --owner <project-owner> --format json --limit 1for ProjectV2,gh api /orgs/<repo-owner>/issue-fieldsfor issue-field mode, orgh api repos/<repo-owner>/<repo-name>/labels --paginatefor label mode. Defer writeprojectverification until the first intentional ProjectV2 mutation, such as creating or linking a board, creating fields, or editing the status of a real existing item. Fine-grained PATs and GitHub Apps should grant Issue Fields organization read for issue-field mode, repository label access for label mode, Issues repository read/write when status moves or comments are enabled, Pull requests read/checks read for PR gates, and selected repository access. -
Confirm Codex is installed and signed in. Detent dispatches agents through the Codex app-server. Verify:
codex --version -
Stop before target-specific discovery. Do not inspect target ProjectV2 boards, organization issue fields, repository labels, issues,
WORKFLOW.md, validation commands, deployment docs, or local target checkout contents until Phase 0.5 identity answers are explicit and confirmed.
Phase 0.5 — Identity Gate
Before Phase 1 repository-specific discovery, create and validate a
customer/project identity record in $ONBOARDING_DIR/answers.env. This is an
operator decision checkpoint, not a recommendation. Recommendations can cite
evidence later, but they must not become selected answers.
Draft the first candidate from identity-safe local evidence. Run this from the target checkout:
detent onboarding draft-answers --output pretty
detent onboarding draft-answers --answers "$ONBOARDING_DIR/answers.env" --write
If a verified local Detent source checkout is available and you want the draft
to include local source freshness evidence, add
--detent-source-root "$DETENT_SOURCE_ROOT". If you are currently in the Detent
source checkout, pass the target explicitly:
detent onboarding draft-answers --detent-source-root "$DETENT_SOURCE_ROOT" --target-source-root <absolute-local-checkout-path> --output pretty
detent onboarding draft-answers --detent-source-root "$DETENT_SOURCE_ROOT" --target-source-root <absolute-local-checkout-path> --answers "$ONBOARDING_DIR/answers.env" --write
The draft command may inspect only local identity evidence: current working
directory and git top-level, origin remote and parsed GitHub owner/name,
Detent documentation source evidence, Detent config path and registered
project ids, and local installed binary/config/service evidence needed to
recommend new-install, existing-install, or add-project. It must not
inspect target ProjectV2 boards, organization issue fields, repository labels,
issues, WORKFLOW.md, validation commands, deployment docs, or runtime docs.
The command should infer and restate an identity candidate from the current git
checkout before asking for raw answer fields. Its local evidence includes
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 its git top level is not the canonical Detent source checkout, it proposes
that checkout as the target candidate. It derives TARGET_REPOSITORY from the
origin remote and TARGET_SOURCE_ROOT from git rev-parse --show-toplevel,
then proposes DETENT_PROJECT_ID from the repo name. If a registered project
already uses that id, compare the registered repository, workdir, and workflow
path to the candidate checkout before accepting the draft. Reuse the existing
project id when it is the same target repository or source root; use a short
non-colliding variant only when the id belongs to a different target. The draft
explains the collision. It proposes CUSTOMER_ID from the owner when that is
the clearest stable workstream id, or from a repo-name/workstream heuristic when
the owner is too broad. CUSTOMER_ID is only a stable local grouping id, not a
billing account or GitHub organization requirement.
If the current working directory is the canonical Detent source checkout, do not
propose Detent as the target unless the operator explicitly says they are
onboarding Detent itself. Pass --target-source-root or ask for the target
checkout or clone destination in human language instead of asking for raw
environment variable names.
Common add-project case:
- Current directory is
/home/loganlanou/projects/digitaldrywood/creswoodcorners-phone. - Detent source checkout is
/home/loganlanou/projects/digitaldrywood/detent. - Current checkout origin is
git@github.com:digitaldrywood/creswoodcorners-phone.git. - Existing Detent config is present and does not register
creswoodcorners-phone. - Onboarding mode is
add-project.
Present the candidate in human-facing language first, then show the
answers.env representation:
I found a likely target checkout from the current shell:
Customer/workstream: `creswoodcorners`
Project id: `creswoodcorners-phone`
Target repository: `digitaldrywood/creswoodcorners-phone`
Source checkout: `/home/loganlanou/projects/digitaldrywood/creswoodcorners-phone`
Reference repositories: `digitaldrywood/detent`
Onboarding mode: `add-project`
customer_id_source=repo_prefix
customer_id_confidence=medium
detent_project_id_source=repo_name
confidence=medium
Customer/workstream alternatives: `digitaldrywood`
`CUSTOMER_ID` is only a stable local grouping id for this Detent install. I
will not inspect target labels, issues, boards, `WORKFLOW.md`, validation
commands, or runtime docs until you confirm this identity and the identity
validator passes. Is this the target you want to onboard?
Review and correct the candidate answers before confirmation. The identity record must contain these answers:
printf '%s\n' \
'CUSTOMER_ID=<customer-or-workstream-id>' \
'DETENT_PROJECT_ID=<local-detent-project-id>' \
'TARGET_REPOSITORY=<repo-owner>/<repo-name>' \
'TARGET_SOURCE_ROOT=<absolute-local-checkout-path>' \
'REFERENCE_REPOSITORIES=<comma-separated-owner/repo-list-or-empty>' \
'DETENT_ONBOARDING_MODE=<new-install|existing-install|add-project>' \
> "$ONBOARDING_DIR/answers.env"
When draft-answers --write creates or updates answers.env, it leaves
IDENTITY_CONFIRMED=false. Do not change that value until the operator confirms
the restatement.
Present the final interpretation back to the operator before inspecting target resources:
I will onboard project `<detent-project-id>` for customer/workstream
`<customer-id>`. The target repository is `<repo-owner>/<repo-name>` at
`<source-root>`. The following repositories are references only and will not be
registered as the target: `<reference-repositories>`. The onboarding mode is
`<new-install|existing-install|add-project>`. Is that correct?
Only after the operator confirms, append the identity confirmation:
printf '%s\n' \
'IDENTITY_CONFIRMED=true' \
>> "$ONBOARDING_DIR/answers.env"
Validate the identity gate:
test -f "$ONBOARDING_DIR/answers.env"
rg '^CUSTOMER_ID=[A-Za-z0-9_.-]+$' "$ONBOARDING_DIR/answers.env"
rg '^DETENT_PROJECT_ID=[A-Za-z0-9_.-]+$' "$ONBOARDING_DIR/answers.env"
rg '^TARGET_REPOSITORY=[^/]+/[^/]+$' "$ONBOARDING_DIR/answers.env"
rg '^TARGET_SOURCE_ROOT=/' "$ONBOARDING_DIR/answers.env"
rg '^REFERENCE_REPOSITORIES=' "$ONBOARDING_DIR/answers.env"
rg '^DETENT_ONBOARDING_MODE=(new-install|existing-install|add-project)$' "$ONBOARDING_DIR/answers.env"
rg '^IDENTITY_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase identity
The CLI validator rejects missing customer/workstream id, missing Detent project
id, malformed target repository, missing or relative target source root, a
target source root that is not a git checkout for the target repository, missing
reference repository separation, missing onboarding mode, missing
IDENTITY_CONFIRMED=true, and GITHUB_MODE answers recorded before identity is
valid.
If <source-root> does not exist, clone the confirmed target repository there
before validation:
git clone "https://github.com/<repo-owner>/<repo-name>.git" <source-root>
Confirm the target checkout only after identity is validated:
git -C <source-root> remote get-url origin
git -C <source-root> rev-parse --show-toplevel
Do not use a reference or tooling repository as the target. A wrong target repository failure example looks like this: the operator mentions
digitaldrywood/detent-orchestration as an example config and
customer/api as the actual project, but the agent runs issue, label, board,
or validation discovery against digitaldrywood/detent-orchestration. Stop,
rewrite TARGET_REPOSITORY=customer/api, verify TARGET_SOURCE_ROOT points to
that checkout, and rerun detent onboarding validate-answers --phase identity
before discovery resumes.
For repeated customer onboarding, a reviewable manifest may carry the same
answers in addition to answers.env:
apiVersion: detent.dev/onboarding/v1
kind: OnboardingAnswers
customer:
id: <customer-or-workstream-id>
display_name: <human-readable-name>
project:
id: <local-detent-project-id>
repository: <repo-owner>/<repo-name>
source_root: <absolute-local-checkout-path>
references:
repositories:
- <owner>/<repo-used-only-for-docs-or-examples>
detent:
mode: add-project
tracker:
github_status_source: <project_v2|issue_field|label>
mutation:
confirmed: false
Phase 0.6 — Status Source Decision
After identity is confirmed and before target-specific discovery, ask the GitHub status-source question. This is separate from identity confirmation, and it must still be an explicit operator answer.
If the operator already volunteered a status-source answer before identity
validation, do not ask the status-source question again. Keep the volunteered
answer pending outside answers.env until identity validation succeeds. After
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase identity
passes, append the pending GITHUB_MODE answer and run
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase decision
without re-asking. For an early "use label for this repo" answer, record
GITHUB_MODE=label immediately after the identity gate passes.
Ask: "Use ProjectV2 board mode, boardless issue-field mode, or repository label
mode?" Explain that this answer maps to tracker.github_status_source: project_v2, tracker.github_status_source: issue_field, or
tracker.github_status_source: label in detent.yaml. Do not choose label
mode for the operator, and do not infer ProjectV2, issue-field, or label mode
from existing registered projects.
Record and validate the explicit answer:
printf '%s\n' \
'GITHUB_MODE=<project_v2|issue_field|label>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase decision
Hard stop: do not inspect target ProjectV2 boards, organization issue fields,
repository labels, target issues, WORKFLOW.md, validation commands, or
deployment docs until this GITHUB_MODE answer is recorded and
detent onboarding validate-answers --phase decision passes.
GitHub GraphQL Rate-Budget Discipline
Treat every gh project ... command as GitHub GraphQL work. GitHub
ProjectV2 is GraphQL-backed, so gh project list, field-list, item-list,
item-add, and item-edit spend the same GraphQL primary rate-limit budget
as Detent's GitHub connector. When github_token: gh is configured, the
operator shell, Detent, and spawned Codex agents all use the same gh user
token and therefore the same user-token GraphQL bucket.
Boardless issue-field mode uses REST for field discovery and issue-field value
writes. Label mode uses REST for repository label discovery, issue reads by
label, and status-label writes. GitHub still reports REST and GraphQL budgets
separately, and detent doctor surfaces both so operators can see whether
boardless work is healthy without spending ProjectV2 GraphQL inventory budget.
GitHub's documented primary GraphQL limit for user-backed tokens is 5,000 points per hour. GitHub App installation tokens receive their own installation-scoped GraphQL budget and can scale for larger installations.
GitHub reports REST and GraphQL primary limits separately. Check the GraphQL bucket before ProjectV2 discovery, before bulk status cleanup, and before the first smoke dispatch:
gh api rate_limit --jq '.resources.graphql | {limit, used, remaining, reset}'
If the remaining budget is low for the planned board size, stop before dispatching an agent. Wait for reset, reduce ProjectV2 inventory work, or move Detent to GitHub App installation authentication so the orchestrator receives an installation-scoped GraphQL budget instead of sharing the operator's user budget.
Use one saved inventory artifact per step. Avoid loops that repeatedly run
gh project item-list --limit 1000 for board inventory, status cleanup, or
smoke verification. Prefer one paginated GraphQL inventory query or one
gh project item-list result written to $ONBOARDING_DIR, then run jq
against the local file.
Once an agent is running, stop operator polling against GitHub ProjectV2. The
agent still needs GraphQL budget for workpad, status, PR, and review activity.
Use the Detent dashboard or /api/v1/state for smoke verification and ongoing
operator checks.
Future Detent tooling should replace hand-rolled ProjectV2 inventory loops with a CLI command or onboarding wizard page that inventories a board once and prints status and priority counts from cached data.
Phase 1 — Discover And Recommend
Do not ask questions in this phase. First re-run the identity and decision validators, then inspect only the confirmed target setup and selected status source. Write one grounded recommendation per remaining Phase 2 question, then interview the human. For an existing install, include the mode evidence, current config path, registered project table, and service health in the recommendations before asking what to change.
-
Create an onboarding notes directory. Keep all discovery artifacts in one place so recommendations can cite evidence. Verify:
ONBOARDING_DIR="${TMPDIR:-/tmp}/detent-onboarding-<repo-owner>-<repo-name>" mkdir -p "$ONBOARDING_DIR" test -d "$ONBOARDING_DIR" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase identity detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase decision -
Record the initial GitHub GraphQL budget for ProjectV2 mode. Use the REST rate-limit endpoint before the first ProjectV2 discovery command. For
GITHUB_MODE=issue_fieldorGITHUB_MODE=label, record a skipped artifact and do not spend GraphQL budget on board discovery. If the remaining budget is low, record the warning in the recommendation and avoid GraphQL-heavy board inventory until reset or GitHub App auth is available. Verify:GITHUB_MODE="$(awk -F= '/^GITHUB_MODE=/ {value=$2} END {print value}' "$ONBOARDING_DIR/answers.env")" if test "$GITHUB_MODE" = "project_v2"; then gh api rate_limit --jq '.resources.graphql | {limit, used, remaining, reset}' \ > "$ONBOARDING_DIR/graphql-rate-limit.before-discovery.json" jq -r '"graphql remaining=\(.remaining) reset=\(.reset)"' \ "$ONBOARDING_DIR/graphql-rate-limit.before-discovery.json" jq -e '.remaining >= 1000' "$ONBOARDING_DIR/graphql-rate-limit.before-discovery.json" \ || printf 'WARNING: low GitHub GraphQL budget; avoid ProjectV2 inventory loops before reset\n' else printf '{"skipped":true,"reason":"GITHUB_MODE=%s"}\n' "$GITHUB_MODE" \ > "$ONBOARDING_DIR/graphql-rate-limit.before-discovery.json" fi -
Inspect the validation surface. Prefer a repo-local release gate over an invented command. Detent is stack-agnostic at the project boundary: identify the target repository's manifests, package managers, task runners, CI workflows, and existing release commands. Record every CI stage category the project requires, such as tests, lint, security scanning, packaging, or deployment validation, together with the project-specific local command and CI check name that satisfy it. These categories and mappings are project knowledge that will be written into the
WORKFLOW.mdprompt; do not infer them from Detent defaults or expectdetent doctorto discover them. Ifmake checkexists, recommendgate.kind: commandwithgate.run: make check. Otherwise recommend the closest local equivalent for the detected ecosystem, such asmix test,bundle exec rspec,npm test,pnpm test,pytest,cargo test,composer test,mvn test,gradle test,dotnet test, or a documented static-site/build check. If no command can be inferred, recommendgate.kind: human_reviewwith an approval label only when the workflow explicitly wants a human label to promote. Verify:cd <source-root> MANIFEST_PATTERN='^(Makefile|Justfile|justfile|Taskfile\.ya?ml|package\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|deno\.jsonc?|go\.mod|mix\.exs|Gemfile|Rakefile|pyproject\.toml|requirements\.txt|tox\.ini|noxfile\.py|Cargo\.toml|composer\.json|pom\.xml|build\.gradle(\.kts)?|settings\.gradle(\.kts)?|Package\.swift|pubspec\.yaml|build\.zig|\.tool-versions|mise\.toml)$' GATE_PATTERN='make check|make test|go test|npm (run )?test|npm test|pnpm (run )?test|yarn test|bun test|deno test|mix test|bundle exec rspec|bundle exec rake|rspec|rake test|pytest|python -m pytest|tox|nox|cargo test|composer test|phpunit|mvn test|gradle test|./gradlew test|dotnet test|swift test|flutter test|zig build test|just |task ' { printf 'Detected manifests:\n' fd -H -a -d 4 "$MANIFEST_PATTERN" . 2>/dev/null || true printf '\nCandidate validation commands:\n' test -f Makefile && awk -F: '/^[A-Za-z0-9][A-Za-z0-9_.-]*:/ {print "make " $1}' Makefile fd -a '' .github/workflows 2>/dev/null || true rg -n "$GATE_PATTERN" \ .github/workflows Makefile Justfile justfile Taskfile.yml Taskfile.yaml \ package.json deno.json deno.jsonc go.mod mix.exs Gemfile Rakefile \ pyproject.toml tox.ini noxfile.py Cargo.toml composer.json pom.xml \ build.gradle build.gradle.kts Package.swift pubspec.yaml build.zig \ 2>/dev/null || true } > "$ONBOARDING_DIR/gate.txt" test -f "$ONBOARDING_DIR/gate.txt" GATE_COMMAND="$(awk 'BEGIN {found=0} /^make check$/ {print; found=1; exit} /^make test$/ && candidate == "" {candidate=$0} END {if (!found && candidate != "") print candidate}' "$ONBOARDING_DIR/gate.txt")" if test -n "$GATE_COMMAND"; then detent --format json onboarding diagnose-gate \ --source-root "$PWD" \ --command "$GATE_COMMAND" \ > "$ONBOARDING_DIR/gate-diagnostic.json" else printf '{"status":"skipped","detail":"no candidate command"}\n' \ > "$ONBOARDING_DIR/gate-diagnostic.json" fi jq -e '.status == "pass" or .status == "env_polluted" or .status == "fail" or .status == "skipped"' \ "$ONBOARDING_DIR/gate-diagnostic.json"When one onboarding operation configures multiple projects, classify the resolved workflows after this inspection. If the set contains both local-heavy projects (local command/validator gates or CI triggers) and cloud-only projects, present agent pools as an optional profile choice: start with separate
codeandcloudpools, or keep one shared pool and decide later. Do not phrase this as a warning because onboarding has no contention telemetry. Say nothing for one project or when every project is in the same class. Existingagent_poolsare never repartitioned automatically.If the diagnostic reports
status: env_polluted, record the failing command,relevant_environment_keys, andpassing_sanitized_command. Userecommended_gate_commandas the gate recommendation because Detent proved it passes with the polluted local environment removed.The commands above are illustrative examples from multiple ecosystems, not Detent defaults or a required tool list. Use the target project's own documented commands and CI check names. A single command gate may cover several required categories, but the onboarding notes must state that mapping explicitly so an agent can verify every category independently.
-
Inspect existing global scheduling. Show the current project table before recommending
priorityandweight. Recommendweight: 1andpriority: 3when no stronger signal exists. Recommend a higher weight or lower priority number only when the existing table shows this repo should outrank peers. Verify:detent --format pretty config path > "$ONBOARDING_DIR/global-path.txt" GLOBAL_CONFIG="$(awk '/^path:/ {print $2}' "$ONBOARDING_DIR/global-path.txt")" if test -f "$GLOBAL_CONFIG"; then awk '/^projects:/ {show=1} show {print}' "$GLOBAL_CONFIG" > "$ONBOARDING_DIR/global-projects.txt" else printf 'No existing global.yaml at %s\n' "$GLOBAL_CONFIG" > "$ONBOARDING_DIR/global-projects.txt" fi test -s "$ONBOARDING_DIR/global-projects.txt" -
Inspect open issue distribution. Count candidate issues by label, assignee, author, and milestone before recommending authorization and intake filters. Verify:
gh issue list --repo <repo-owner>/<repo-name> --state open --limit 1000 \ --json number,title,labels,assignees,milestone,author,url \ > "$ONBOARDING_DIR/issues.json" jq '{ total: length, labels: ([.[] | .labels[].name] | sort | group_by(.) | map({name: .[0], count: length})), assignees: ([.[] | if (.assignees | length) == 0 then "Unassigned" else .assignees[].login end] | sort | group_by(.) | map({name: .[0], count: length})), authors: ([.[] | .author.login] | sort | group_by(.) | map({name: .[0], count: length})), milestones: ([.[] | .milestone.title // "No milestone"] | sort | group_by(.) | map({name: .[0], count: length})) }' "$ONBOARDING_DIR/issues.json" > "$ONBOARDING_DIR/issue-counts.json" jq -e '.total >= 0' "$ONBOARDING_DIR/issue-counts.json" -
Inspect existing ProjectV2 boards only for ProjectV2 mode. Recommend reuse when a board clearly belongs to this repo or workstream; otherwise recommend creating a new board named after the repo or product. Skip this step for
GITHUB_MODE=issue_fieldandGITHUB_MODE=label. This is the ProjectV2 read verification. Verify:GITHUB_MODE="$(awk -F= '/^GITHUB_MODE=/ {value=$2} END {print value}' "$ONBOARDING_DIR/answers.env")" if test "$GITHUB_MODE" = "project_v2"; then gh project list --owner <project-owner> --format json --limit 50 \ > "$ONBOARDING_DIR/projects.json" else printf '{"projects":[],"skipped":true,"reason":"GITHUB_MODE=%s"}\n' "$GITHUB_MODE" \ > "$ONBOARDING_DIR/projects.json" fi jq -e '.projects | length >= 0' "$ONBOARDING_DIR/projects.json" -
Inspect priority counts for ProjectV2 reuse candidates.
priority_independs on the ProjectV2Priorityfield, so gather counts from the strongest reuse candidate with one paginated inventory pass saved to a local artifact. Do not repeatedly callgh project item-list --limit 1000. For a new board or non-ProjectV2 mode, record an empty count table and recommend nopriority_infilter until issues have been added and ranked. Verify:GITHUB_MODE="$(awk -F= '/^GITHUB_MODE=/ {value=$2} END {print value}' "$ONBOARDING_DIR/answers.env")" REUSE_PROJECT_NODE_ID="<reuse-candidate-project-node-id-or-empty>" if test "$GITHUB_MODE" = "project_v2" && test -n "$REUSE_PROJECT_NODE_ID"; then PRIORITY_QUERY=' query($project: ID!, $after: String) { node(id: $project) { ... on ProjectV2 { items(first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { content { ... on Issue { state repository { nameWithOwner } } } priorityValue: fieldValueByName(name: "Priority") { ... on ProjectV2ItemFieldSingleSelectValue { name } } } } } } }' : > "$ONBOARDING_DIR/priority-items.jsonl" AFTER="" while :; do if test -n "$AFTER"; then gh api graphql \ -f project="$REUSE_PROJECT_NODE_ID" \ -f after="$AFTER" \ -f query="$PRIORITY_QUERY" \ > "$ONBOARDING_DIR/priority-page.json" else gh api graphql \ -f project="$REUSE_PROJECT_NODE_ID" \ -f query="$PRIORITY_QUERY" \ > "$ONBOARDING_DIR/priority-page.json" fi jq -c '.data.node.items.nodes[]' "$ONBOARDING_DIR/priority-page.json" \ >> "$ONBOARDING_DIR/priority-items.jsonl" jq -e '.data.node.items.pageInfo.hasNextPage' "$ONBOARDING_DIR/priority-page.json" \ >/dev/null || break AFTER="$(jq -r '.data.node.items.pageInfo.endCursor' "$ONBOARDING_DIR/priority-page.json")" done jq -s --arg repo '<repo-owner>/<repo-name>' \ '[.[] | select(.content.repository.nameWithOwner == $repo and .content.state == "OPEN") | (.priorityValue.name // "No priority")] | sort | group_by(.) | map({name: .[0], count: length})' \ "$ONBOARDING_DIR/priority-items.jsonl" > "$ONBOARDING_DIR/priority-counts.json" else printf '[]\n' > "$ONBOARDING_DIR/priority-counts.json" fi jq -e 'type == "array"' "$ONBOARDING_DIR/priority-counts.json" -
Record recommendations before the interview. The recommendation must cite the discovery artifact that produced it. Verify:
printf '%s\n' \ 'mode: <new-install|existing-install|add-project, from mode evidence>' \ 'board: <reuse-or-create recommendation, from projects.json>' \ 'rate_budget: <GraphQL remaining/reset and low-budget warning>' \ 'scheduling: <priority/weight recommendation, from global-projects.txt>' \ 'authorization: <filter recommendation, from issue-counts.json and priority-counts.json>' \ 'dashboard_bind: <localhost/private-or-tailscale/all-interfaces recommendation>' \ 'gate: <gate recommendation, from gate-diagnostic.json and gate.txt>' \ 'concurrency: <max agents and Merging cap recommendation>' \ 'review_policy: <hard stop or auto-promote recommendation>' \ 'prompt: <template or repo-specific recommendation, from repo docs>' \ 'intake: <bulk-add filter and initial Status recommendation>' \ > "$ONBOARDING_DIR/recommendations.md" rg -n '^(mode|board|rate_budget|scheduling|authorization|dashboard_bind):' \ "$ONBOARDING_DIR/recommendations.md" rg -n '^(gate|concurrency|review_policy|prompt|intake):' \ "$ONBOARDING_DIR/recommendations.md"
Phase 2 — Interview The Human
Ask only these eligible decision questions. Present each as question, grounded
recommendation, and default-if-silent. Defaults are recommendations only; they
do not authorize GitHub, issue, label, WORKFLOW.md, or global.yaml
mutations. Record explicit answers in $ONBOARDING_DIR/answers.env.
Present the operator's plain-English operating model first, then show the
canonical answers.env fields. Do not lead with raw environment keys when a
human intent profile can explain the operating effect. After a named delivery
profile supplies a field, skip the duplicate low-level question unless the
operator asks for an advanced override.
Ask or select the delivery profile before emitting low-level KANBAN_MODE
defaults. For GITHUB_MODE=label add-project onboarding on an operator-owned
local or private Detent instance, recommend KANBAN_MODE=integration even when
the pre-mutation detent doctor --port 0 skipped write probes. Skipped
pre-mutation write probes must not become a read_only recommendation;
mutation still requires Phase 2.5 authorization and post-authorization write
probes.
-
Mode.
DETENT_ONBOARDING_MODEwas selected in Phase 0.5. If discovery shows the chosen mode is wrong, stop, update the identity answers, present the full interpretation again, and rerundetent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase identity. Do not infer mode from an existing registered project or carry it forward as a default. -
GitHub status source.
GITHUB_MODEwas selected in Phase 0.6. If discovery shows the chosen status source is wrong, stop, update the explicit answer, rerundetent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase decision, and repeat Phase 1 for the selected mode. Do not inspect your recommendation as if it selected the mode, and do not continue to Phase 3, issue-field, label,WORKFLOW.md, orglobal.yamlmutation without this explicitGITHUB_MODEanswer. -
ProjectV2 board. Ask this only when
GITHUB_MODE=project_v2: "Reuse an existing ProjectV2 board or create a new one?" List the boards from$ONBOARDING_DIR/projects.json. Recommendation source: matching board title, owner, item count, and whether the board already has repo work. Default if silent: reuse the strongest matching board; if none match, create<repo-name>. Verify:printf '%s\n' \ 'BOARD_MODE=<reuse|create>' \ 'PROJECT_OWNER=<project-owner>' \ 'PROJECT_NUMBER=<project-number-if-reuse>' \ 'PROJECT_TITLE=<project-title-if-create>' \ >> "$ONBOARDING_DIR/answers.env" rg '^BOARD_MODE=' "$ONBOARDING_DIR/answers.env" -
Boardless issue field. Ask this only when
GITHUB_MODE=issue_field: "Which organization issue field should Detent use for status?" RecommendStatusunless inspection found a different single-select workflow field. Confirm that GitHub issue fields are issue-only: linked PR cards derive status from the linked issue, and PR-only cards cannot be moved by issue-field status writes. Verify:printf '%s\n' \ 'STATUS_FIELD_NAME=<status-field-name>' \ >> "$ONBOARDING_DIR/answers.env" gh api /orgs/<repo-owner>/issue-fields \ --jq '.[] | select(.name == "<status-field-name>") | {name,data_type,options}' rg '^STATUS_FIELD_NAME=' "$ONBOARDING_DIR/answers.env" -
Repository status labels. Ask this only when
GITHUB_MODE=label: "What repository label prefix should Detent use for status?" Recommenddetent:unless the repository already has an intentional Detent status-label namespace. Do not choose label mode for the operator; ask even if label mode appears easiest for the repository. Explain that Detent maps each configured workflow state throughtracker.state_map, slugifies the resulting state name, and prefixes it:Todomaps todetent:todo,In Progressmaps todetent:in-progress, and with the default release flowCancelled: Donestate map,Cancelledmaps todetent:done. These labels are the status source of truth; they are distinct fromtracker.authorization.labels.*filters,projects[].authorization, andagent.dispatch_priority_by_label, which select or rank work but do not define state. Verify:printf '%s\n' \ 'STATUS_LABEL_PREFIX=<status-label-prefix>' \ >> "$ONBOARDING_DIR/answers.env" rg '^STATUS_LABEL_PREFIX=' "$ONBOARDING_DIR/answers.env" -
Automation policy. Ask the customer-facing policy questions before recommending review, auto-promotion, dependency unblock, Kanban mutation, or Merging settings. Do not present raw
answers.envkeys as the primary interface.Ask first:
How should Detent handle completed work after the validation gate passes? 1. Full autopilot: keep completed work in its active lane and automatically promote it into Merging when gates pass. 2. Review gate: stop in Human Review until I approve, then continue merging. 3. Conservative/manual: stop before mutations or merging unless I explicitly approve each step.Then ask:
When blocked work becomes unblocked, should Detent automatically resume it when guardrails pass, or wait for human approval?And ask:
Should Detent automatically move eligible issues forward when the configured tests/checks pass?Present the operator's plain-English operating model first, then show the canonical
answers.envfields as a secondary implementation mapping:- Full autopilot: keep completed work in its active lane and promote it to
Mergingwhen linked PRs, gates, CI, mergeability, dependency checks, and guardrails pass; also auto-unblock dependency-waiting work when declared blockers clear. - Review gate: stop at
Human Reviewuntil a human explicitly approves promotion toMerging; GitHub/config writes still require the separate mutation confirmation. - Conservative/manual: require explicit approval before promotion or mutation; keep project Kanban mutations read-only unless the operator later switches to a custom advanced policy.
- Custom/advanced: expose the underlying fields for teams that need a mixed policy.
When the operator says "no human review, no wait state, unblock aggressively, only stop on real blockers," asks for maximum automation, or says to auto-promote and auto-unblock as much as guardrails allow, recommend
full_autopilot. Explain that full autopilot still requires linked PRs, green CI, clear gates, mergeability, satisfied dependencies, and no blocking P1 automated findings; it does not bypass validation. Do not recommendAUTO_PROMOTE_ENABLED=falseunless the operator selected review gate or conservative/manual. Do not recommend stopping atHuman Reviewunless the operator selected review gate or conservative/manual. Do not label an unselected setup as a safety-default review posture.Asking for automation policy does not authorize mutation. Phase 2.5 still must approve the exact answers before creating labels, editing
global.yaml, writingWORKFLOW.md, or dispatching agents. Onboarding mutation should use live reload or a project-scoped refresh and verify running agents were not interrupted rather than restarting Detent.After selecting a named profile, do not ask the Kanban interaction, validation-gate automated-review, Merging concurrency, review policy, or dependency waiting policy questions again unless the operator asks for an advanced override. Advanced override means the operator switches to Custom/advanced after seeing the expansion; remove or omit
DELIVERY_PROFILEbefore recording a profile-supplied key with a different value. Ask the remaining Phase 2 questions that are not supplied by the selected profile.For full autopilot, review gate, or conservative/manual, write
DELIVERY_PROFILEfirst:printf '%s\n' \ 'DELIVERY_PROFILE=<full_autopilot|review_gate|conservative_manual>' \ >> "$ONBOARDING_DIR/answers.env" rg '^DELIVERY_PROFILE=' "$ONBOARDING_DIR/answers.env"Full autopilot expands to:
printf '%s\n' \ 'KANBAN_MODE=integration' \ 'AUTO_PROMOTE_ENABLED=true' \ 'AUTO_PROMOTE_QUIET_SECONDS=0' \ 'AUTO_PROMOTE_GATE_WAIT_STATE=source' \ 'GATE_REQUIRE_AUTOMATED_REVIEW=false' \ 'AUTO_PROMOTE_REQUIRE_AUTOMATED_REVIEW=false' \ 'DEPENDENCY_AUTO_UNBLOCK_ENABLED=true' \ 'MERGING_CONCURRENCY=1' \ >> "$ONBOARDING_DIR/answers.env"Review gate expands to:
printf '%s\n' \ 'KANBAN_MODE=integration' \ 'AUTO_PROMOTE_ENABLED=false' \ 'AUTO_PROMOTE_QUIET_SECONDS=600' \ 'AUTO_PROMOTE_GATE_WAIT_STATE=review' \ 'GATE_REQUIRE_AUTOMATED_REVIEW=false' \ 'AUTO_PROMOTE_REQUIRE_AUTOMATED_REVIEW=false' \ 'DEPENDENCY_AUTO_UNBLOCK_ENABLED=false' \ 'MERGING_CONCURRENCY=1' \ >> "$ONBOARDING_DIR/answers.env"Conservative/manual expands to:
printf '%s\n' \ 'KANBAN_MODE=read_only' \ 'AUTO_PROMOTE_ENABLED=false' \ 'AUTO_PROMOTE_QUIET_SECONDS=600' \ 'AUTO_PROMOTE_GATE_WAIT_STATE=review' \ 'GATE_REQUIRE_AUTOMATED_REVIEW=true' \ 'AUTO_PROMOTE_REQUIRE_AUTOMATED_REVIEW=true' \ 'DEPENDENCY_AUTO_UNBLOCK_ENABLED=false' \ 'MERGING_CONCURRENCY=1' \ >> "$ONBOARDING_DIR/answers.env"For Custom/advanced, do not write
DELIVERY_PROFILE; continue through the lower-level fields below and record the operator's explicit mixed policy. - Full autopilot: keep completed work in its active lane and promote it to
-
Kanban interaction. Ask this only for Custom/advanced. If the operator wants to override a selected profile's
KANBAN_MODE, switch to Custom/advanced and remove or omitDELIVERY_PROFILEbefore recording the mixed policy: "Should Detent's project Kanban be read-only or allow GitHub mutations from the dashboard?" Keep fleet/kanbanread-only. For a project-scoped board on an operator-owned local or private Detent instance, recommendintegrationby default before mutation authorization. This recommendation does not authorize writes; Phase 2.5 must approve the exact answers before any GitHub or config mutation. After authorization,detent doctor --allow-write-probesmust prove ProjectV2 status write, issue-field status write, or status-label update for the selected status source, plus issue/PR comment write. For a shared observer dashboard or an explicit no-writes choice, recommendread_only. If failed post-authorization write probes prevent integration, stop and repair permissions or ask whether to downgrade toread_only. Do not use skipped pre-mutation write probes as evidence forread_only. Verify:printf '%s\n' \ 'KANBAN_MODE=<read_only|integration>' \ >> "$ONBOARDING_DIR/answers.env" rg '^KANBAN_MODE=' "$ONBOARDING_DIR/answers.env" -
Scheduling. Ask: "What
global.yamlpriorityfrom 0-4 andweightshould this project receive?" Show$ONBOARDING_DIR/global-projects.txt. Disambiguate this from the boardPriorityfield:global.yamlpriorityranks projects on the host; the boardPriorityfield ranks issues inside one project. Recommendation source: existing project weights, priority ranks, and whether this repo is release-critical relative to peers. Default if silent:priority: 3,weight: 1. Verify:printf '%s\n' \ 'GLOBAL_PRIORITY=<1-4>' \ 'GLOBAL_WEIGHT=<positive-integer>' \ >> "$ONBOARDING_DIR/answers.env" rg '^GLOBAL_(PRIORITY|WEIGHT)=' "$ONBOARDING_DIR/answers.env" -
Project color. Ask: "Should this project have a fixed dashboard color, or should Detent assign one automatically?" Show
$ONBOARDING_DIR/global-projects.txtso existing colors are discoverable. Explain thatprojects[].coloris optional, accepts opaque CSS hex values such as#1192e8, and missing colors are deterministic from the project ID. Colors appear in the sidebar and the top-level multi-project/kanbanboard, but project names and IDs remain visible. Verify:printf '%s\n' \ 'PROJECT_COLOR=<#RRGGBB-or-empty>' \ >> "$ONBOARDING_DIR/answers.env" rg '^PROJECT_COLOR=' "$ONBOARDING_DIR/answers.env" -
Dispatch label ordering. Ask: "When two issues have the same configured
Priority, should labels break the tie before age?" Show the label counts from$ONBOARDING_DIR/issue-counts.jsonand recommend an ordered list from labels that represent work type or risk, such asbug,regression, thenenhancementwhen those labels are common. Explain thatPrioritystill wins first when available, unlisted labels rank last, and an empty answer means no label ordering. In label status-source mode, do not useagent.dispatch_priority_by_labelfordetent:*status labels unless the team intentionally wants status labels to also affect tie-breaking. Verify:printf '%s\n' \ 'DISPATCH_PRIORITY_BY_LABEL=<comma-separated-labels-or-empty>' \ >> "$ONBOARDING_DIR/answers.env" rg '^DISPATCH_PRIORITY_BY_LABEL=' "$ONBOARDING_DIR/answers.env" -
Instance name. Ask: "What optional instance name should appear in Detent browser tabs and the navbar?" Recommendation source: the short hostname, existing
global.identity.name, and any operator naming convention for this host. Default if silent: the short hostname. Verify:
INSTANCE_NAME="$(hostname -s 2>/dev/null || hostname)"
printf '%s\n' \
"INSTANCE_NAME=${INSTANCE_NAME}" \
>> "$ONBOARDING_DIR/answers.env"
rg '^INSTANCE_NAME=' "$ONBOARDING_DIR/answers.env"
- Authorization filters. Ask: "Should Detent consider all board items or
only items matching a filter?" Offer
none,labels.include,labels.exclude,assignee_in,author_in, andpriority_in. Recommendation source: live counts in$ONBOARDING_DIR/issue-counts.jsonand$ONBOARDING_DIR/priority-counts.json, plus any repo/workstream labels already in use. Show the total count fornone, counts for each label, assignee, author, and priority option, and the remaining count for any proposedlabels.exclude. When the author counts include open issues authored by logins other than the operator's login, call that out as a shared repo / multiple humans signal and recommendauthor_in: [<operator login>]by default. Show the per-author counts before asking so the operator can decide whether to keep that guard, expand it, or choose a different selector. Default if silent: for a shared board with foreign authors, setauthor_in: [<operator login>]; use no filter for a dedicated repo board with only operator-authored open issues; otherwise use the narrowest label or assignee filter that matches the intended workstream.
Shared repo / multiple humans callout: Detent's claim and lease system
prevents duplicate dispatch among Detent instances that can see the same
issue, but manual work outside Detent writes no Detent claim or lease, so
authorization filters are the guardrail that keeps a new instance from
dispatching another person's manual work. Frame the answer as selector-level
tracker.authorization; it matches the neutral connector.Issue data
Detent already fetched and applies the same way across current and future
tracker backends.
In label status-source mode, keep authorization filters focused on
workstream labels such as documentation or backend; do not use the
detent:* status labels as authorization filters unless you are
deliberately narrowing the state machine. Verify:
printf '%s\n' \
'AUTHORIZATION_KIND=<none|labels.include|labels.exclude|assignee_in|author_in|priority_in>' \
'AUTHORIZATION_VALUE=<value-or-empty>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^AUTHORIZATION_' "$ONBOARDING_DIR/answers.env"
- Dashboard bind. Ask: "How should the Detent dashboard bind:
localhost-only, a private/Tailscale IP, or all interfaces?" Recommendation
source: the operator's access path, whether SSH tunnels or VPN/Tailscale are
expected, the host firewall, and any known private interface addresses.
Default if silent:
127.0.0.1for localhost-only access through SSH tunnels. Use a specific private or Tailscale IP for VPN-only exposure. Use0.0.0.0only on trusted private networks because it exposes the dashboard on every interface, not just Tailscale. Verify:
printf '%s\n' \
'DASHBOARD_HOST=<127.0.0.1|tailscale-or-private-ip|0.0.0.0>' \
'DASHBOARD_REMOTE_HOST=<tailscale-or-private-ip-or-empty>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^DASHBOARD_' "$ONBOARDING_DIR/answers.env"
- Validation gate. Ask for the gate kind and command: "Use the detected
command, a custom command, or a human review label gate?" Ask the automated
review subquestion only for Custom/advanced. If the operator wants to
override a selected profile's
GATE_REQUIRE_AUTOMATED_REVIEW, switch to Custom/advanced and remove or omitDELIVERY_PROFILEbefore recording the mixed policy: "If this is a command gate, should auto-promotion require an automated GitHub PR review from a bot?" Recommendation source:$ONBOARDING_DIR/gate-diagnostic.json,$ONBOARDING_DIR/gate.txt, detected manifests, task runners, CI workflow commands, and the repo's review policy. Default if silent:recommended_gate_commandfrom$ONBOARDING_DIR/gate-diagnostic.jsonwhen it reportspassorenv_polluted; otherwise detectedmake checkwhen present withrequire_automated_review: true; otherwise use the strongest ecosystem-specific command from the repo evidence, orkind: human_reviewwithapproval_label: human-approvedwhen no reliable local command exists. Verify:
printf '%s\n' \
'GATE_KIND=<command|human_review>' \
'GATE_RUN=<command-if-command>' \
'GATE_REQUIRE_AUTOMATED_REVIEW=<true|false-if-command>' \
'GATE_APPROVAL_LABEL=<label-if-human-review>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^GATE_' "$ONBOARDING_DIR/answers.env"
When the validator is enabled, also ask whether it should inherit its
route/provider default or deliberately pin a model with VALIDATOR_MODEL.
Recommend inheritance by default. A validator pin may provide reproducible
behavior or cost control, but it must be reviewed and updated before the
provider retires that model generation.
- Worker model. Ask: "Should Codex workers follow the provider default, or pin a specific model for this project?" Recommend provider default. It follows generation upgrades automatically, survives model retirements, and still produces accurate model telemetry from the Codex session. A pin is a first-class option for reproducibility or cost control, but it can break dispatch when the provider retires the model and must be reviewed each generation. Record the explicit choice and require the model only for a pin:
For the recommended provider default, omit WORKER_MODEL:
printf '%s\n' \
'WORKER_MODEL_MODE=provider_default' \
>> "$ONBOARDING_DIR/answers.env"
rg '^WORKER_MODEL' "$ONBOARDING_DIR/answers.env"
For an explicit pin, record both fields:
printf '%s\n' \
'WORKER_MODEL_MODE=pinned' \
'WORKER_MODEL=<model-if-pinned>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^WORKER_MODEL' "$ONBOARDING_DIR/answers.env"
Leave model_reasoning_effort unset by default because not every model
accepts it. Treat it as an optional per-project Codex config override only
after confirming the selected model supports the requested effort.
- Concurrency. Ask: "How many agents may this project run at once?"
Recommendation source: host capacity, existing
global.yamlprojects, and the repo's gate cost. Default if silent:agent.max_concurrent_agents: 5for an active code repo, lower for expensive gates. State thatMerging: 1is required. If the selected profile already suppliedMERGING_CONCURRENCY=1, do not ask Merging concurrency again; record onlyMAX_CONCURRENT_AGENTS. If the operator wants to override a selected profile's merging concurrency, switch to Custom/advanced and remove or omitDELIVERY_PROFILEbefore recording the mixed policy. Verify:
printf '%s\n' \
'MAX_CONCURRENT_AGENTS=<positive-integer>' \
'MERGING_CONCURRENCY=1' \
>> "$ONBOARDING_DIR/answers.env"
rg '^MERGING_CONCURRENCY=1$' "$ONBOARDING_DIR/answers.env"
agent.max_concurrent_agents_by_state is per-project configuration: it
lives only in each detent.yaml (internal/config/config.go), is enforced
per project orchestrator (internal/orchestrator/dispatch_planner.go
stateSlotsAvailable) and per project scheduler
(internal/project/project.go CapacityByState). The global settings
struct (internal/config/global/config.go) has no by-state field. The global
dispatch gate (internal/scheduler/global_gate.go) still arbitrates total
pool slots and weighted/fair-share project selection, but it also receives
each project's current dispatch-state priority so waiting Merging work can
reserve the next released global slot ahead of lower-priority lanes.
Merging: 1 serializes merges within one project only; multiple
projects merge concurrently because each merge train targets its own
repository. For multiple instances sharing one board/repo, serialization
comes from tracker.claims, not the per-state cap.
Use MAX_TURNS, MAX_SESSION_DURATION_MS, and NO_PROGRESS_TIMEOUT_MS
as the independent per-session catastrophe bounds. Their defaults allow 20
turns, two hours of wall-clock time, and 90 minutes without a workspace or
Workpad change. Keep MAX_SESSION_TOKENS as an additional token-consumption
backstop. Codex re-counts cached context on every turn, so do not emit
MAX_SESSION_CONTEXT_MULTIPLIER by default and do not recommend small
values such as 4. The multiplier is a coarse, advanced opt-in that caps
roughly how many full-context turns fit; record it only when the operator
explicitly requests that additional ceiling.
- Review policy. Ask this only for Custom/advanced. If the operator wants
to override a selected profile's
AUTO_PROMOTE_*, switch to Custom/advanced and remove or omitDELIVERY_PROFILEbefore recording the mixed policy: "Should Detent hard-stop atHuman Review, or may it auto-promote toMergingafter the human-defined criteria are true?" Recommendation source: repo risk, issue labels, review requirements, and how much trust the human wants to delegate. Default if silent: no review-policy answer is recorded; ask again in plain English. Do not writeAUTO_PROMOTE_ENABLED=falseunless the operator chose review gate, conservative/manual, or explicitly chose a custom hard stop. Both modes are fully supported, and this is the human's call.
For criteria-based auto-promote, use agent.auto_promote.enabled,
quiet_seconds, optout_label, allowed_issue_labels, gate_wait_state,
gate_wait_timeout_seconds, gate_wait_timeout_action, and the top-level
command gate's automated_review setting. quiet_seconds is the quiet period after
observed issue/status/review activity and linked PR activity such as a fresh
push to the PR head, optout_label is the per-issue escape hatch,
allowed_issue_labels is an allowlist such as documentation for low-risk
issue classes, and gate_wait_state: source keeps zero-quiet completed work
in its active lane while checks are pending until
gate_wait_timeout_seconds expires. automated_review: optional defaults
the timeout action to merge; required defaults it to human_review.
The legacy require_automated_review boolean maps to required or off.
When automated review is required, a
Codex/ChatGPT/Claude review on an older commit does not clear this gate.
Verify:
printf '%s\n' \
'AUTO_PROMOTE_ENABLED=<true|false>' \
'AUTO_PROMOTE_QUIET_SECONDS=<seconds>' \
'AUTO_PROMOTE_GATE_WAIT_STATE=<source|review>' \
'AUTO_PROMOTE_REQUIRE_AUTOMATED_REVIEW=<true|false-if-command>' \
'AUTO_PROMOTE_OPTOUT_LABEL=<label>' \
'AUTO_PROMOTE_ALLOWED_LABELS=<comma-separated-labels-or-empty>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^AUTO_PROMOTE_' "$ONBOARDING_DIR/answers.env"
- Dependency waiting policy. Ask this only for Custom/advanced. If the
operator wants to override a selected profile's
DEPENDENCY_AUTO_UNBLOCK_ENABLED, switch to Custom/advanced and remove or omitDELIVERY_PROFILEbefore recording the mixed policy: "Should dependency-waiting issues stay inTodoand be gated by Detent, or should they sit inBlockedand be auto-unblocked when dependencies clear?" Default if silent:tracker.dependency_auto_unblock.enabled: false. Use theBlockedauto-unblock mode only when the team declares dependency blockers through native GitHubblocked_byrelations or the structureddetent-statusWorkpad block. During the deprecation window, existing machine-readable issue-bodyDepends on:orBlocked by:lines remain a fallback, but prose-only dependency blockers should be migrated. Detent will not clear unrelated human blockers. If dependency-waiting issues are placed inBlockedwhile this setting stays disabled, Detent will only display them as blocked and will not move them back toTodo. Verify:
printf '%s\n' \
'DEPENDENCY_AUTO_UNBLOCK_ENABLED=<true|false>' \
'DEPENDENCY_AUTO_UNBLOCK_SOURCE_STATES=Blocked' \
'DEPENDENCY_AUTO_UNBLOCK_TARGET_STATE=Todo' \
'DEPENDENCY_AUTO_UNBLOCK_READINESS=terminal_or_merged' \
>> "$ONBOARDING_DIR/answers.env"
rg '^DEPENDENCY_AUTO_UNBLOCK_' "$ONBOARDING_DIR/answers.env"
- Prompt body. Ask: "Use the template prompt or add repo-specific
instructions?" Recommendation source:
CLAUDE.md,AGENTS.md,CONTRIBUTING.md, README development commands, manifests, and CI workflows in<source-root>. Default if silent: template prompt plus any repo authority files found. Verify:
AUTHORITY_PATTERN='^(CLAUDE|AGENTS|CONTRIBUTING|README)\.md$'
MANIFEST_PATTERN='^(Makefile|Justfile|justfile|Taskfile\.ya?ml|package\.json|deno\.jsonc?|go\.mod|mix\.exs|Gemfile|Rakefile|pyproject\.toml|Cargo\.toml|composer\.json|pom\.xml|build\.gradle(\.kts)?|Package\.swift|pubspec\.yaml|build\.zig|\.tool-versions|mise\.toml)$'
GATE_PATTERN='make check|make test|go test|npm (run )?test|npm test|pnpm (run )?test|yarn test|bun test|deno test|mix test|bundle exec rspec|rspec|rake test|pytest|python -m pytest|cargo test|composer test|phpunit|mvn test|gradle test|./gradlew test|dotnet test|swift test|flutter test|zig build test'
{
fd -H -a -d 4 "$AUTHORITY_PATTERN" <source-root> 2>/dev/null || true
fd -H -a -d 4 "$MANIFEST_PATTERN" <source-root> 2>/dev/null || true
rg -n "$GATE_PATTERN" \
<source-root>/README.md <source-root>/CONTRIBUTING.md \
<source-root>/.github/workflows <source-root>/package.json \
<source-root>/mix.exs <source-root>/Gemfile <source-root>/Rakefile \
<source-root>/pyproject.toml <source-root>/Cargo.toml \
<source-root>/composer.json <source-root>/pom.xml \
<source-root>/build.gradle <source-root>/build.gradle.kts \
2>/dev/null || true
} > "$ONBOARDING_DIR/prompt-evidence.txt"
printf 'PROMPT_MODE=<template|repo-specific>\n' >> "$ONBOARDING_DIR/answers.env"
rg '^PROMPT_MODE=' "$ONBOARDING_DIR/answers.env"
- Issue intake. Ask: "Which issue filter should be bulk-added, should the
initial
StatusbeBacklogorTodo, and should the human enable the auto-add workflow?" Recommendation source:$ONBOARDING_DIR/issue-counts.jsonand the authorization answer. Default if silent: bulk-add the narrowest safe filter toBacklog, then move one known issue toTodofor the smoke test. Verify:
printf '%s\n' \
'INTAKE_GH_FLAGS=<gh-issue-list-flags>' \
'INITIAL_STATUS=<Backlog|Todo>' \
'ENABLE_AUTO_ADD=<true|false>' \
>> "$ONBOARDING_DIR/answers.env"
rg '^(INTAKE_GH_FLAGS|INITIAL_STATUS|ENABLE_AUTO_ADD)=' "$ONBOARDING_DIR/answers.env"
Phase 2.5 — Mutation Authorization
Stop here before Phase 3 or any other command that can create, link, mutate, or
delete GitHub Projects, issue fields, labels, issues, PRs, WORKFLOW.md, or
global.yaml. Canonicalize profile-derived answers before the final operator
prompt so $ONBOARDING_DIR/answers.env visibly contains the exact low-level
values mutation steps will read:
detent onboarding normalize-answers --answers "$ONBOARDING_DIR/answers.env" --write
Show the operator $ONBOARDING_DIR/recommendations.md, the plain-English
behavior summary, and the complete $ONBOARDING_DIR/answers.env, then ask:
"May I execute the selected mutation steps using these exact answers?" Defaults
from Phase 2 are still only recommendations; an unanswered default must not
authorize any external or local config mutation.
detent --format pretty onboarding explain-answers --answers "$ONBOARDING_DIR/answers.env" --phase decision
Show this summary before the canonical answers.env keys so the operator
approves the effective behavior, not only raw fields.
Record the explicit confirmation only after the operator says yes. This removes stale confirmations first and appends the new confirmation as the final nonblank line. The second normalization call below is an idempotence check. If it reports missing profile expansion after confirmation, remove the stale confirmation, rerun normalization, show the updated answer file to the operator, and record a fresh confirmation. If any Phase 2 answer changes later, rerun Phase 2.5 and record a fresh confirmation.
CONFIRMATION_FILE="$(mktemp)"
rg -v '^MUTATION_CONFIRMED=' "$ONBOARDING_DIR/answers.env" > "$CONFIRMATION_FILE" || true
mv "$CONFIRMATION_FILE" "$ONBOARDING_DIR/answers.env"
printf '%s\n' \
'MUTATION_CONFIRMED=true' \
>> "$ONBOARDING_DIR/answers.env"
detent onboarding normalize-answers --answers "$ONBOARDING_DIR/answers.env" --write
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
Run this gate before every mutating phase and before every one-off mutating command:
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
test -f "$ONBOARDING_DIR/answers.env"
rg '^DETENT_ONBOARDING_MODE=' "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
GITHUB_MODE="$(
awk -F= '/^GITHUB_MODE=/ {value=$2} END {print value}' "$ONBOARDING_DIR/answers.env"
)"
case "$GITHUB_MODE" in
project_v2)
rg '^BOARD_MODE=(reuse|create)$' "$ONBOARDING_DIR/answers.env"
rg '^PROJECT_OWNER=' "$ONBOARDING_DIR/answers.env"
BOARD_MODE="$(
awk -F= '/^BOARD_MODE=/ {value=$2} END {print value}' "$ONBOARDING_DIR/answers.env"
)"
case "$BOARD_MODE" in
reuse) rg '^PROJECT_NUMBER=' "$ONBOARDING_DIR/answers.env" ;;
create) rg '^PROJECT_TITLE=' "$ONBOARDING_DIR/answers.env" ;;
esac
;;
issue_field)
rg '^STATUS_FIELD_NAME=' "$ONBOARDING_DIR/answers.env"
;;
label)
rg '^STATUS_LABEL_PREFIX=' "$ONBOARDING_DIR/answers.env"
;;
*)
printf 'Unsupported GITHUB_MODE=%s\n' "$GITHUB_MODE" >&2
exit 1
;;
esac
Only after this gate passes and the operator has confirmed mutation may doctor
run configured write probes. Use --port 0 when an existing service already
owns the dashboard port:
detent doctor --port 0 --allow-write-probes
Phase 3 — Create Or Adopt The Status Source
Run the ProjectV2 steps only when GITHUB_MODE=project_v2. When
GITHUB_MODE=issue_field, skip board creation/linking and run the boardless
issue-field verification step instead. When GITHUB_MODE=label, skip both
ProjectV2 and issue-field setup and verify repository status labels.
Before any command in this phase that can mutate GitHub ProjectV2, issue-field, or label resources, rerun the Phase 2.5 gate:
test -f "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
Before any ProjectV2 board mutation such as gh project create, gh project link, gh project field-create, or gh project item-edit, also run:
rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env"
rg '^BOARD_MODE=(reuse|create)$' "$ONBOARDING_DIR/answers.env"
rg '^PROJECT_OWNER=' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
-
Adopt an existing board when the answer says reuse. Record the number and node id for later commands. Run only when
GITHUB_MODE=project_v2. Verify:gh project view <project-number> --owner <project-owner> --format json \ > "$ONBOARDING_DIR/project.json" jq -e '.id | startswith("PVT_")' "$ONBOARDING_DIR/project.json" -
Create a board when the answer says create. Run only when
GITHUB_MODE=project_v2. GitHub creates the defaultStatusfield; Detent still needs you to create the board itself. Verify:rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" rg '^BOARD_MODE=create$' "$ONBOARDING_DIR/answers.env" rg '^PROJECT_OWNER=' "$ONBOARDING_DIR/answers.env" rg '^PROJECT_TITLE=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" gh project create --owner <project-owner> --title "<project-title>" --format json \ > "$ONBOARDING_DIR/project.json" jq -e '.number and (.id | startswith("PVT_"))' "$ONBOARDING_DIR/project.json" -
Ensure the
Priorityfield exists. Run only whenGITHUB_MODE=project_v2. Detent can add missing options inside an existing field, but it never creates the field. Reused boards can already have this field, so check before creating it. IfPriorityexists but is not a single-select field, stop and ask the human to rename the conflicting field or choose another board. Verify:rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" PROJECT_NUMBER="$(jq -r '.number' "$ONBOARDING_DIR/project.json")" gh project field-list "$PROJECT_NUMBER" --owner <project-owner> --format json \ > "$ONBOARDING_DIR/fields.before.json" if jq -e '.fields[] | select(.name == "Priority" and (.options | type == "array"))' \ "$ONBOARDING_DIR/fields.before.json" >/dev/null; then echo "Priority field already exists; reusing it" elif jq -e '.fields[] | select(.name == "Priority")' \ "$ONBOARDING_DIR/fields.before.json" >/dev/null; then echo "Priority exists but is not a single-select field" >&2 exit 1 else gh project field-create "$PROJECT_NUMBER" --owner <project-owner> \ --name Priority \ --data-type SINGLE_SELECT \ --single-select-options "Urgent,High,Medium,Low" fi gh project field-list "$PROJECT_NUMBER" --owner <project-owner> --format json \ | jq -e '.fields[] | select(.name == "Priority" and (.options | type == "array"))' -
Link the repository to the board. Run only when
GITHUB_MODE=project_v2. This keeps the project discoverable from the repo. Verify:rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" gh project link "$PROJECT_NUMBER" --owner <project-owner> --repo <repo-name> gh project view "$PROJECT_NUMBER" --owner <project-owner> --format json --jq '.url' -
Confirm required ProjectV2 fields. Run only when
GITHUB_MODE=project_v2. Detent auto-provisions missingStatusandPriorityoptions on first run, but never the board or fields themselves. Verify:gh project field-list "$PROJECT_NUMBER" --owner <project-owner> --format json \ | jq -e '[.fields[].name] as $names | all(["Status","Priority"][]; . as $want | $names | index($want))' -
Clean up inherited ProjectV2 statuses before first Detent dispatch. Run only when
GITHUB_MODE=project_v2. Reused boards often carry status options and stale item placements from a predecessor orchestrator. Inventory the board before the first Detent start so the operator can distinguish intentional custom lanes from old active or terminal columns. Verify:PROJECT_NUMBER="$(jq -r '.number' "$ONBOARDING_DIR/project.json")" PROJECT_NODE_ID="$(jq -r '.id' "$ONBOARDING_DIR/project.json")" gh project field-list "$PROJECT_NUMBER" --owner <project-owner> --format json \ > "$ONBOARDING_DIR/fields.before-detent.json" gh project item-list "$PROJECT_NUMBER" --owner <project-owner> --format json --limit 1000 \ > "$ONBOARDING_DIR/project-items.before-detent.json" gh project item-list "$PROJECT_NUMBER" --owner <project-owner> \ --query 'repo:<repo-owner>/<repo-name> is:issue is:open' \ --format json --limit 1000 \ > "$ONBOARDING_DIR/repo-open-project-items.before-detent.json" jq -r '.fields[] | select(.name == "Status") | .options[].name' \ "$ONBOARDING_DIR/fields.before-detent.json" \ > "$ONBOARDING_DIR/status-options.before-detent.txt" cat "$ONBOARDING_DIR/status-options.before-detent.txt"Detent will add missing canonical
Statusoptions fromdetent.yamlon first run and reorder known options into Detent's configured order. It preserves extra custom options after the configured Detent states; do not delete a custom option unless the operator confirms it is a predecessor leftover.Print status counts for every board item and for open issues from the target repository:
jq '[.items[] | (.status // "No status")] | sort | group_by(.) | map({status: .[0], count: length})' \ "$ONBOARDING_DIR/project-items.before-detent.json" jq '[.items[] | (.status // "No status")] | sort | group_by(.) | map({status: .[0], count: length})' \ "$ONBOARDING_DIR/repo-open-project-items.before-detent.json"Compare option names against the configured Detent states, including case-only differences such as
In progressversusIn Progress. If this project uses custom state names indetent.yaml, replace the default list below with those configured states. Verify:printf '%s\n' \ Backlog Todo 'In Progress' Blocked 'Human Review' Rework Merging Done Cancelled \ > "$ONBOARDING_DIR/detent-status-options.txt" awk ' NR == FNR { canonical[$0] = 1 canonical_lower[tolower($0)] = $0 next } !($0 in canonical) { if (tolower($0) in canonical_lower) { printf "case-different\t%s\tcanonical:%s\n", $0, canonical_lower[tolower($0)] } else { printf "custom-or-legacy\t%s\n", $0 } } ' "$ONBOARDING_DIR/detent-status-options.txt" \ "$ONBOARDING_DIR/status-options.before-detent.txt"Treat old active or terminal names such as
Ready,In progress,In review, orDoneas migration questions, not automatic truths. Ask the operator: "Should open issues currently in inherited active or terminal statuses stay where they are, be closed, or move back toBacklogbefore Detent starts?"Also ask what to do with empty non-Detent
Statusoptions that do not map to the configured workflow. The operator should choose one action for each option: remove it from the board, keep it as an intentional custom column, or map it through the workflow state configuration. The default recommendation is to remove empty non-mapping predecessor options during setup after status counts have been reported.The default recommendation is to move open issues from predecessor active or terminal statuses back to
Backlog, unless the operator confirms a status is intentional or the issue should be closed. IfBacklogdoes not exist yet, create it manually or start Detent once with no dispatchable items so it can provision canonical options, then repeat this cleanup before moving any issue toTodo. Verify the selected cleanup set before editing items:LEGACY_STATUS_REGEX='^(Ready|In progress|In review|Done)$' jq -r --arg re "$LEGACY_STATUS_REGEX" \ '.items[] | select((.status // "No status") | test($re)) | [.id, .status, .content.url] | @tsv' \ "$ONBOARDING_DIR/repo-open-project-items.before-detent.json"After the operator confirms the cleanup set, move those open items to
Backlog:rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" STATUS_FIELD_ID="$(gh project field-list "$PROJECT_NUMBER" --owner <project-owner> --format json --jq '.fields[] | select(.name == "Status") | .id')" BACKLOG_OPTION_ID="$(gh project field-list "$PROJECT_NUMBER" --owner <project-owner> --format json --jq '.fields[] | select(.name == "Status") | .options[] | select(.name == "Backlog") | .id')" jq -r --arg re "$LEGACY_STATUS_REGEX" \ '.items[] | select((.status // "No status") | test($re)) | .id' \ "$ONBOARDING_DIR/repo-open-project-items.before-detent.json" | while IFS= read -r item_id; do gh project item-edit \ --id "$item_id" \ --project-id "$PROJECT_NODE_ID" \ --field-id "$STATUS_FIELD_ID" \ --single-select-option-id "$BACKLOG_OPTION_ID" >/dev/null done -
Verify the boardless issue field when selected. Run this only when
GITHUB_MODE=issue_field. Detent needs an organization-level single-select issue field and options matching the configured workflow states. Issue fields are issue-only, so linked PR cards will use their linked issue's status. Verify:STATUS_FIELD_NAME="<status-field-name>" gh api /orgs/<repo-owner>/issue-fields > "$ONBOARDING_DIR/issue-fields.json" jq --arg name "$STATUS_FIELD_NAME" -e '.[] | select(.name == $name)' \ "$ONBOARDING_DIR/issue-fields.json" > "$ONBOARDING_DIR/issue-field.json" jq -e '.data_type == "single_select"' "$ONBOARDING_DIR/issue-field.json" jq -r '.options[].name' "$ONBOARDING_DIR/issue-field.json" \ > "$ONBOARDING_DIR/issue-field-status-options.txt" printf '%s\n' \ Backlog Todo 'In Progress' Blocked 'Human Review' Rework Merging Done Cancelled \ > "$ONBOARDING_DIR/detent-status-options.txt" sort "$ONBOARDING_DIR/detent-status-options.txt" > "$ONBOARDING_DIR/detent-status-options.sorted.txt" sort "$ONBOARDING_DIR/issue-field-status-options.txt" > "$ONBOARDING_DIR/issue-field-status-options.sorted.txt" comm -23 "$ONBOARDING_DIR/detent-status-options.sorted.txt" "$ONBOARDING_DIR/issue-field-status-options.sorted.txt"If the
commoutput is not empty, add the missing issue-field options in GitHub before starting Detent, or change the workflow states to match the existing options. Before any issue-field creation or option update, rerun:rg '^GITHUB_MODE=issue_field$' "$ONBOARDING_DIR/answers.env" rg '^STATUS_FIELD_NAME=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"Do not create or link a GitHub ProjectV2 board for this mode.
-
Verify repository status labels when selected. Run this only when
GITHUB_MODE=label. Detent needs one repository label for each effective configured workflow state. Applytracker.state_mapfirst, then slugify the result and prependtracker.status_label_prefix. With the default prefix and release-flowCancelled: Donemapping, the required labels are:detent:backlog detent:todo detent:in-progress detent:blocked detent:human-review detent:rework detent:merging detent:doneCreate or verify the labels before the first
detent doctorrun so doctor can prove readiness instead of reporting missing label mappings. Detent's defaulttracker.auto_provision: truecreates the same missing labels on project start, so the first start can mutate repository labels. Before creating labels manually or starting Detent with auto-provision enabled, rerun:rg '^GITHUB_MODE=label$' "$ONBOARDING_DIR/answers.env" rg '^STATUS_LABEL_PREFIX=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"Verify:
STATUS_LABEL_PREFIX="<status-label-prefix>" printf '%s\n' \ "${STATUS_LABEL_PREFIX}backlog" "${STATUS_LABEL_PREFIX}todo" \ "${STATUS_LABEL_PREFIX}in-progress" "${STATUS_LABEL_PREFIX}blocked" \ "${STATUS_LABEL_PREFIX}human-review" "${STATUS_LABEL_PREFIX}rework" \ "${STATUS_LABEL_PREFIX}merging" "${STATUS_LABEL_PREFIX}done" \ > "$ONBOARDING_DIR/detent-status-labels.required.txt" gh api repos/<repo-owner>/<repo-name>/labels --paginate --jq '.[].name' \ > "$ONBOARDING_DIR/repo-labels.txt" sort "$ONBOARDING_DIR/detent-status-labels.required.txt" \ > "$ONBOARDING_DIR/detent-status-labels.required.sorted.txt" sort "$ONBOARDING_DIR/repo-labels.txt" \ > "$ONBOARDING_DIR/repo-labels.sorted.txt" comm -23 "$ONBOARDING_DIR/detent-status-labels.required.sorted.txt" \ "$ONBOARDING_DIR/repo-labels.sorted.txt"If
commprints missing labels and you are using the default prefix, create them before starting Detent:gh label create detent:backlog --repo <repo-owner>/<repo-name> \ --color cfd3d7 --description "Not ready for Detent dispatch." gh label create detent:todo --repo <repo-owner>/<repo-name> \ --color cfd3d7 --description "Ready for Detent dispatch." gh label create detent:in-progress --repo <repo-owner>/<repo-name> \ --color fbca04 --description "Work is currently active." gh label create detent:blocked --repo <repo-owner>/<repo-name> \ --color d73a4a --description "Cannot continue without human input." gh label create detent:human-review --repo <repo-owner>/<repo-name> \ --color 6f42c1 --description "Waiting for human review." gh label create detent:rework --repo <repo-owner>/<repo-name> \ --color d93f0b --description "Changes are requested before review can continue." gh label create detent:merging --repo <repo-owner>/<repo-name> \ --color 6f42c1 --description "Approved work is being integrated." gh label create detent:done --repo <repo-owner>/<repo-name> \ --color 0e8a16 --description "Work is complete."For a custom
STATUS_LABEL_PREFIXor custom workflow states, generate the required label list from the actualWORKFLOW.mdstate names instead of copying the defaults above. Do not create or link a GitHub ProjectV2 board or organization issue field for this mode.
Phase 4 — Author detent.yaml And WORKFLOW.md
Before writing, overwriting, or editing <source-root>/detent.yaml or
<source-root>/WORKFLOW.md, rerun:
test -f "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
-
Fetch the selected mode template pair. Read both existing files first if either is present; this runbook is for from-zero repositories, so do not overwrite a human-authored contract without explicit approval. The maintained templates are paired
docs/templates/detent.project_v2.yamlanddocs/templates/WORKFLOW.project_v2.mdfiles, with equivalentdocs/templates/detent.issue_field.yamlwithdocs/templates/WORKFLOW.issue_field.md, anddocs/templates/detent.label.yamlwithdocs/templates/WORKFLOW.label.md. The CLI-only local-status pair usesdetent.github_local.yamlandWORKFLOW.github_local.md; use it when the target repository must remain read-only and Detent status should live only in local SQLite. Non-code artifact projects can start fromdocs/templates/detent.non_code_artifact.yamlanddocs/templates/WORKFLOW.non_code_artifact.md; the rest of this onboarding flow remains GitHub-focused. The/onboardingweb wizard can write the same tracker blocks interactively: choose Repository labels forGITHUB_MODE=label, enter the repository and status label prefix such asdetent:, and it will generatetracker.github_status_source: labelindetent.yamlwithout ProjectV2 fields. Verify:test ! -f <source-root>/WORKFLOW.md test ! -f <source-root>/detent.yaml GITHUB_MODE="$(sed -n 's/^GITHUB_MODE=//p' "$ONBOARDING_DIR/answers.env" | tail -n 1)" case "$GITHUB_MODE" in project_v2|issue_field|label) ;; *) printf 'invalid GITHUB_MODE: %s\n' "$GITHUB_MODE" >&2; exit 1 ;; esac curl -fsSL "https://raw.githubusercontent.com/digitaldrywood/detent/main/docs/templates/WORKFLOW.${GITHUB_MODE}.md" \ -o <source-root>/WORKFLOW.md curl -fsSL "https://raw.githubusercontent.com/digitaldrywood/detent/main/docs/templates/detent.${GITHUB_MODE}.yaml" \ -o <source-root>/detent.yaml rg -n "github_status_source: ${GITHUB_MODE}|^tracker:|^workspace:|^agent:" <source-root>/detent.yaml test -s <source-root>/WORKFLOW.mdWhen adding Detent to a repository that already has a
WORKFLOW.md, audit the existing prompt body instead of replacing it blindly. Add or tighten the## Codex Workpadcontract so every update includes a fenceddetent-statusblock, blockers are declared in that block, and narrative Workpad sentences are treated as prose only. Dependency blockers should prefer GitHub's nativeblocked_byrelation, then the structured Workpad block, with issue-bodyBlocked by:orDepends on:lines kept only as a labeled legacy fallback during the deprecation window. Add or tighten a## Required Execution Flowsection when it is missing, with explicitFor Todo,For In Progress,For Rework, andFor Merginginstructions. TheFor Mergingsection should invoke$go-workflow:ship, should not callgh pr mergedirectly outside ship, and should require exactly one terminal outcome: pull request merged and issue moved toDone, issue moved toReworkwith an actionable defect, or issue remaining inMergingwith a concrete external blocker recorded in thedetent-statusblock. 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.Add a
## Project CI Quality Gatessection to every code-oriented workflow prompt. List each required stage category and the project-specific local command and CI check name that satisfy it. Replace the template placeholders with evidence from discovery; add or remove categories to match the project. Instruct agents to verify that every declared stage exists and passes on the current pull request head whenever they touch CI configuration or perform a review. Keep the declaration stack-neutral: concrete tools are project data, not Detent behavior, anddetent doctordoes not validate or parse CI configuration.Keep the templates' rebase-survival rule in every merging or delivery flow. Conflict resolution during a rebase can silently drop branch changes, so the agent must capture the effective pre-rebase diff, verify the same files and hunks afterward, and stop in the project's blocked or exception state when loss is unexplained instead of pushing. The commands named in the template rule are illustrative; equivalent comparisons are valid.
Treat the prompt body as a thin, always-loaded core. Keep universal safety, state-transition, validation, and handoff contracts in
WORKFLOW.md; move domain-specific runbooks and conditionally relevant tool guidance into reviewed lazy skills under.detent/skills.detent doctorestimates the loaded prompt body at four characters per token and warns above 4,000 tokens by default. Use--workflow-token-threshold <tokens>to choose a different positive threshold. The warning names non-overlapping Markdown sections and candidate skill files, but never rewritesWORKFLOW.md; use the existing skill-creation review flow described below to perform any extraction. -
Substitute the tracker and workspace answers in
detent.yaml. In ProjectV2 mode, use the ProjectV2 node id astracker.project_slug. In boardless issue-field mode, set the repository and issue field. In label mode, set the repository and status-label prefix. Setwrite_probe_issuefor ProjectV2 or issue-field status-write proof; in label mode, set it only when using legacy/deep issue-object probes. Use absolute paths forworkspace.source_rootandworkspace.root, and leavetracker.api_keyout unless this workflow intentionally carries a workflow-local token instead of usinggithub_token: ghfromglobal.yaml. Verify the selected tracker block:ProjectV2 tracker snippet:
tracker: kind: github github_status_source: project_v2 project_slug: <project-node-id> write_probe_issue: <write-probe-issue>Boardless issue-field tracker snippet:
tracker: kind: github github_status_source: issue_field repository: <repo-owner>/<repo-name> status_field: <status-field-name> write_probe_issue: <write-probe-issue>Repository label tracker snippet:
tracker: kind: github github_status_source: label repository: <repo-owner>/<repo-name> status_label_prefix: "<status-label-prefix>"GitHub local-status tracker snippet:
tracker: kind: github_local repository: <repo-owner>/<repo-name> local_sqlite: path: .detent/github-local-work-items.dbDo not add
tracker.github_status_sourcetogithub_local; it is a composite backend, not a fourth GitHub status source. Import issues explicitly after the project is registered:detent github-local import <local-detent-project-id> <issue-number>[,<issue-number>...] --state Todo# ProjectV2 mode: PROJECT_NODE_ID="$(jq -r '.id' "$ONBOARDING_DIR/project.json")" rg -n 'github_status_source: project_v2|project_slug: <project-node-id>|write_probe_issue:' <source-root>/detent.yaml # Boardless issue-field mode: rg -n 'github_status_source: issue_field|repository: <repo-owner>/<repo-name>|status_field: <status-field-name>|write_probe_issue:' <source-root>/detent.yaml # Label mode: rg -n 'github_status_source: label|repository: <repo-owner>/<repo-name>|status_label_prefix: "<status-label-prefix>"' <source-root>/detent.yaml # GitHub local-status mode: rg -n 'kind: github_local|repository: <repo-owner>/<repo-name>|local_sqlite:|path: .detent/github-local-work-items.db' <source-root>/detent.yaml # All modes: perl -0pi -e 's#(?m)^ source_root: .*$# source_root: <source-root>#' <source-root>/detent.yaml perl -0pi -e 's#(?m)^ root: .*$# root: <worktree-root>#' <source-root>/detent.yaml rg -n 'source_root: <source-root>|root: <worktree-root>' <source-root>/detent.yaml -
Set Kanban interaction mode when supported. Do not add this block to current releases unless the Detent binary supports Kanban interaction configuration. Maintained templates set project boards to
integrationfor a trusted operator-owned local or private Detent instance. Keep fleet/kanbanread-only; this setting only controls/projects/<id>/kanban. For a shared observer dashboard or explicit no-writes choice, setread_only. If post-authorization write probes fail, stop and repair permissions or ask whether to downgrade toread_only; skipped pre-mutation write probes are not evidence for that downgrade. Verify: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. # Optional allowed_transitions expose broader manual status editing. # allowed_transitions: # In Progress: [Blocked, Cancelled] # Rework: [Blocked, Cancelled] # Merging: [Blocked, Cancelled]Apply the recorded Phase 2 answer before running doctor:
KANBAN_MODE="${KANBAN_MODE:?set KANBAN_MODE to read_only or integration from answers.env}" perl -0pi -e "s#(?m)^ mode: (read_only|integration)$# mode: ${KANBAN_MODE}#" <source-root>/detent.yaml rg -n "kanban:|mode: ${KANBAN_MODE}|show_blocked_alerts|allowed_transitions" <source-root>/detent.yaml -
Set the dashboard bind from the interview. This writes the default
server.hostused when Detent starts without an explicit--host. Service managers can still override it inExecStartwith the same selected host. Verify:perl -0pi -e 's#(?m)^ host: .*$# host: <dashboard-host>#' <source-root>/detent.yaml rg -n '^server:|host: <dashboard-host>|port:' <source-root>/detent.yaml -
Set the worker model from the interview. Render the explicit answer in
codex.commandand keep the trade-off next to the command. Do not inherit a stale pin from the template:# WORKER_MODEL_MODE=provider_default codex: # Optional model_reasoning_effort is unset because not every model accepts it. command: codex app-server # Provider default: upgrades automatically and avoids retirement breakage.# WORKER_MODEL_MODE=pinned and WORKER_MODEL=<model> codex: # Optional model_reasoning_effort is unset because not every model accepts it. command: codex app-server --config 'model="<model>"' # Pinned for reproducibility or cost control; update before retirement.Add a
model_reasoning_effortconfig override only when the operator explicitly requests it and the selected model accepts that setting. -
Set the gate from the interview. For command gates, include the command, whether an automated GitHub PR review is required for auto-promotion, whether failed current-head CI parks in
Human Reviewor routes toRework, and whether the validator-agent review is enabled. For human gates, include the approval label. Verify:rg -n '^gate:|kind: <command|human_review>|run: <gate-command>|require_automated_review: <true|false>|ci_failure_action: <skip|rework>|validator:|enabled: <true|false>|min_score: <score>|block_on:|approval_label: <label>' \ <source-root>/detent.yamlCommand gate shape:
gate: kind: command run: <gate-command> require_automated_review: <true|false> ci_failure_action: <skip|rework> validator: enabled: <true|false> # Optional deliberate pin; leave empty to inherit the route/provider default. # Pinned models require manual updates before provider retirement. model: "" min_score: 0.8 max_inline_diff_bytes: 65536 block_on: - p1Human review gate shape:
gate: kind: human_review approval_label: <approval-label>The generated web-onboarding workflow leaves Codex as the default backend. To route a specific role to Claude Code, add an explicit
agents:block that defines every referenced backend and keeps a Codex default route:agents: backends: - id: codex-default kind: codex protocol: app-server command: codex app-server options: approval_policy: never thread_sandbox: workspace-write turn_sandbox_policy: type: workspaceWrite networkAccess: true - 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-claude role: validator backend: claude-worker model: fable - name: default backend: codex-default model: gpt-5-codex default: trueClaude Code auth is ambient. A logged-in
claudeCLI uses subscription limits;ANTHROPIC_API_KEYuses API billing and takes precedence when both are present. Detent stores no Anthropic keys. Subscription limits in headlessclaude -pruns are opaque, with no in-band warning before 5-hour window or weekly caps; a cap hit only appears as an error result. Use subscription auth for bounded or bursty personal operation andANTHROPIC_API_KEYfor sustained or parallel fleet runs. For fleet isolation, set a distinctCLAUDE_CONFIG_DIRper worker and use--no-session-persistencethroughextra_argswhen session continuity is not needed. Codex uses an OS-levelworkspace-writesandbox; Claude Code runs withpermission_mode: bypassPermissionsinside the Detent git worktree, which is a checkout boundary, not an OS sandbox. Allowed shell tools can reach host files as the Detent worker user, so use container, VM, or OS sandbox isolation for a hard blast-radius boundary andallowed_tools/disallowed_toolsto tighten the role. For local Anthropic-compatible inference, pointANTHROPIC_BASE_URLat a local server such as Ollama and use the sameclaude_codebackend; see Local Models With Codex And Ollama for model and context-window guidance. -
Set dispatch ordering, review policy, dependency waiting policy, and concurrency. Keep
Merging: 1. Use the dispatch label ordering, review policy, and dependency policy selected by the human. Verify:rg -n 'max_concurrent_agents: <max>|Merging: 1|dispatch_priority_by_label:|auto_promote:|dependency_auto_unblock:|blocked_recovery:|enabled: <true|false>|quiet_seconds: <seconds>|optout_label: <label>|allowed_issue_labels:|gate_wait_state:|gate_wait_timeout_seconds:|rework_limit:|source_states:|target_state:|reason_codes:|readiness:' \ <source-root>/detent.yamlLabel tie-breaker shape:
agent: dispatch_priority_by_label: - <highest-ranked-label> - <next-ranked-label>Empty list shape:
agent: dispatch_priority_by_label: []Hard-stop review policy:
agent: auto_promote: enabled: false quiet_seconds: 600 optout_label: requires-human-review allowed_issue_labels: [] gate_wait_state: source gate_wait_timeout_seconds: 3600 rework_limit: 3Criteria-based auto-promote example:
agent: auto_promote: enabled: true quiet_seconds: <seconds> optout_label: <optout-label> allowed_issue_labels: - <allowed-label> gate_wait_state: <source-or-review> gate_wait_timeout_seconds: <positive-seconds> gate_wait_timeout_action: <merge-or-human_review> rework_limit: <0-to-disable-or-max-rework-laps>For a command gate, auto-promote requires a linked open PR, green CI, no P1 automated PR review findings, and the configured quiet period.
automated_review: requiredrequires a current-head automated GitHub PR review.optionalwaits until the gate deadline and then promotes if every other check passes;offdoes not wait. Any observed P1 bot findings still route the item toRework.gate.required_status_checksshould list every release-blocking branch-protection or ruleset check name; Detent treats missing, skipped, failed, cancelled, neutral, or still-running required checks as non-green on the current PR head. Failed or cancelled current-head CI also routes the item toReworkby default; setci_failure_action: skiponly when red CI should stay parked inHuman Review.agent.auto_promote.rework_limitdefaults to3; set it to0only when repeated rework should remain unlimited. A positive value requiresBlockedin a configured tracker state list and blocks the next lap after that many persisted Rework entries. Pending CI stays parked. 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. Withgate.validator.enabled: true, a validator-agent reviews the PR diff against issue acceptance criteria before auto-promotion; scores belowmin_scoreor findings whose severity appears inblock_onroute the item toRework.detent doctor --port 0reports sampledHuman Reviewcandidates and reasons such asautomated_review_missingwhen that gate is not met, and warns when recent merged PRs show no automated reviews for a project configured to expect them.Keep
agent.max_session_context_multiplierabsent unless the operator explicitly requested the coarse ceiling. The primary catastrophe bounds areagent.max_turns,agent.max_session_duration_ms, andagent.no_progress_timeout_ms; keepagent.max_session_tokensas an additional token-consumption backstop because cached context is counted again on every Codex turn. A small multiplier can terminate otherwise healthy sessions after only a few full-context turns. Keepagent.no_progress_token_limitpositive as the cross-session brake, especially whenbudget.billing_mode: subscriptionmakes USD controls inert.Dependency auto-unblock default:
tracker: dependency_auto_unblock: enabled: false source_states: - Blocked target_state: Todo readiness: terminal_or_mergedEnable it only for projects that use
Blockedas a dependency-waiting state with explicit machine-readable dependency references. Without this enabled,Blockedis an observed/display state and dependency completion will not move issues back toTodo.Blocked recovery default:
tracker: blocked_recovery: enabled: false source_states: - Blocked target_state: Rework reason_codes: - merge_conflict - stale_base - missing_current_head_ciEnable it only when structured Blocked-entry reason codes should authorize PR repair. An intentional recovery park records
reason_code: merge_conflict,stale_base, ormissing_current_head_ciin its blockeddetent-statusblock. A matching current PR condition and a new diff/base fingerprint are also required; issue descriptions and manual parking events never authorize recovery. -
Write the prompt body. Keep the
## Codex Workpadinstruction, include thedetent-statusschema examples, include the nativeblocked_bydependency command with the labeled legacy fallback note, include repo authority files discovered in Phase 2, state the validation gate, and keep the maintained template's## Required Execution Flowunless the human explicitly chooses stronger project-specific instructions. The flow should tell agents what to do inTodo,In Progress,Rework, andMerging, including theFor Mergingrequirement to use$go-workflow:ship, record external blockers in the structured status block, and move the issue toDoneonly after the pull request is merged. Include the current state withCurrent Detent status: {{ issue.state }}so resumed agents choose the right section. Verify:rg 'Current Detent status|Codex Workpad|detent-status|dependencies/blocked_by|Required Execution Flow|For Todo|For In Progress|For Rework|For Merging|go-workflow:ship|CLAUDE.md|AGENTS.md|CONTRIBUTING.md|<gate-command>|<repo-owner>/<repo-name>' \ <source-root>/WORKFLOW.md -
Check the workflow contract before registration. This is a structural check;
detent doctor --allow-write-probesin Phase 5 is the full preflight. Verify:rg -n '^schema:|^tracker:|project_slug:|^workspace:|source_root:|^agent:|max_concurrent_agents_by_state:|^gate:' \ <source-root>/detent.yaml if rg -q '^---$' <source-root>/WORKFLOW.md; then printf 'WORKFLOW.md must be prose-only for new projects\n' >&2 exit 1 fi
Out-of-Scope Follow-ups
Detent adds an ## Out-of-scope discoveries block to pull-request agent
prompts by default. It tells agents to keep the current issue scoped and file
meaningful unrelated problems or improvements as separate tracker issues in
the project's Backlog state. Each self-filed issue includes a fenced
detent-agent block with schema: 1 and the agent's best-guess effort from
the project's rubric. Projects should define that rubric in their
agent-facing documentation.
The agent uses the tracker's configured status source when placing the new issue. If that source cannot be updated from the session, the agent files the issue without a state and reports the limitation in its final handoff. Plan-only and non-pull-request runs do not receive this prompt block.
The prompt guidance is controlled by agent.followups.enabled, which defaults
to true:
agent:
followups:
enabled: true
Set agent.followups.enabled: false to remove the generated block. Maintained
workflow templates also include the filing rule in their prompt body, so the
guidance remains available when the generated block is disabled. detent doctor warns when follow-ups are disabled and the loaded WORKFLOW.md body has
no equivalent out-of-scope filing guidance.
Skills And Skill Creation
Detent skills are repository-owned Markdown instructions that give agents
repeatable guidance for project-specific tasks. By default, Detent loads up to
50 top-level *.md files from <source-root>/.detent/skills in filename order.
Each file starts with YAML front matter containing the required string fields
name, description, and when_to_use; the Markdown body contains the
implementation guidance:
---
name: release-check
description: Verify a release candidate before publishing.
when_to_use: The issue asks to prepare or validate a release.
---
Run the repository's release validation command and record the resulting
artifact checksums in the workpad.
Names must be unique. detent doctor --project <detent-project-id> --port 0
reports the effective skills configuration, loaded count, and every dropped
file. It warns when front matter is invalid, a name is duplicated, or the
configured prompt limit drops otherwise valid files. A missing skills
directory is healthy and reports zero loaded and zero dropped.
Skill creation is a review loop, not an automatic write to the main branch. When creation is enabled, the agent may draft at most the configured number of candidate skill files in its issue worktree. Those drafts arrive in the normal pull request with the implementation. Reviewers approve a skill by merging the pull request; only merged skills become repository guidance for future runs. Agents should not draft skills for one-off facts, routine edits, or secrets.
The agent.skills keys and defaults are:
| Key | Default | Purpose |
|---|---|---|
enabled |
true |
Load repository skills and enable the creation loop when creation is also enabled. |
path |
.detent/skills |
Workspace-relative directory containing skill Markdown files. |
max_skills_in_prompt |
50 |
Maximum valid skills included in an agent prompt. |
creation.enabled |
true |
Allow agents to propose skill drafts. |
creation.max_drafts_per_run |
1 |
Maximum candidate skill files an agent may draft in one run. |
Use this complete default block in detent.yaml:
agent:
skills:
enabled: true
path: .detent/skills
max_skills_in_prompt: 50
creation:
enabled: true
max_drafts_per_run: 1
Set agent.skills.creation.enabled: false to keep loading reviewed skills but
stop new drafts. Set agent.skills.enabled: false to disable both skill loading
and skill creation for the project.
Phase 5 — Register The Project
Before running detent init, detent add-project, mutating global.yaml, or
running detent doctor --allow-write-probes with configured write probes,
rerun:
test -f "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
-
Create global config only when needed. Use the resolved path Detent reports. For
add-project, read and preserve the existing config; do not reinitialize or overwrite runtime keys unless the human selected that change. Verify:detent --format pretty config path GLOBAL_CONFIG="$( detent --format pretty config path | awk '/^path:/ {print $2}' )" if test -f "$GLOBAL_CONFIG"; then sed -n '1,240p' "$GLOBAL_CONFIG" \ > "$ONBOARDING_DIR/global-config.before-register.txt" else detent init fi detent --format pretty config path -
Register the project. Skip this if the project is already registered and the human chose to repair or update the existing entry.
priorityandweightare the scheduling answers from Phase 2. Verify:detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" GLOBAL_CONFIG="$( detent --format pretty config path | awk '/^path:/ {print $2}' )" PROJECT_ENTRY_PATTERN='id: <detent-project-id>|workflow: <source-root>/WORKFLOW.md|workdir: <source-root>' if rg -n "$PROJECT_ENTRY_PATTERN" "$GLOBAL_CONFIG"; then printf 'Project already registered; confirm before editing\n' else detent add-project \ --id <detent-project-id> \ --workflow <source-root>/WORKFLOW.md \ --workdir <source-root> \ --weight <global-weight> \ --priority <global-priority> fi GLOBAL_CONFIG="$( detent --format pretty config path | awk '/^path:/ {print $2}' )" PROJECT_ENTRY_PATTERN='id: <detent-project-id>|workflow: <source-root>/WORKFLOW.md|workdir: <source-root>|weight: <global-weight>|priority: <global-priority>' rg -n "$PROJECT_ENTRY_PATTERN" "$GLOBAL_CONFIG" -
Set or preserve runtime keys in
global.yaml. For local onboarding, prefergithub_token: ghso Detent resolves the token fromgh auth tokenat startup. In ProjectV2 mode this shares the operator's GraphQL budget with Detent and spawned agents; in issue-field mode it still shares REST issue-field and comment write limits; in label mode it shares REST label, issue, and comment write limits. For production or high-volume projects, prefer GitHub App installation authentication indetent.yaml. Setinstance_nameto the interview answer or the short hostname on new installs. For existing installs, preserveenv,log_level,github_token,port, andinstance_nameunless the human selected different values. Use a non-4000 port if another Detent instance is already running; keep the dashboard host indetent.yamlserver.hostor pass it with--host. Verify:GLOBAL_CONFIG="$( detent --format pretty config path | awk '/^path:/ {print $2}' )" sed -n '1,240p' "$GLOBAL_CONFIG" # Run this edit only for new installs or confirmed runtime-key changes. GLOBAL_RUNTIME_INSERT=' if (!/^github_token:/m) { my $runtime = "env: prod\nlog_level: info\ngithub_token: gh\n"; $runtime .= "port: <port>\ninstance_name: <instance-name>\n"; s/^(global:)/${runtime}$1/m; } ' perl -0pi -e "$GLOBAL_RUNTIME_INSERT" "$GLOBAL_CONFIG" rg -n '^(env|log_level|github_token|port|instance_name):' "$GLOBAL_CONFIG"Required shape:
env: prod log_level: info github_token: gh port: <port> instance_name: <instance-name>GitHub App installation auth is configured in
detent.yamlwithgithub_app_id,github_app_installation_id, and eithergithub_app_private_keyorgithub_app_private_key_path. -
Run full preflight until every check passes. Fix every
FAIL; do not dispatch work from a failed doctor run. In ProjectV2 mode, confirm doctor reports project access, Status option discovery, repository issue/PR access, configured issue-object status-write probes, and rate-limit visibility. In issue-field mode, confirm repository access, issue field discovery, option discovery, issue reads by field value, configured issue-field write probe, issue/PR comment write when integration-capable features are configured, and rate-limit visibility. In label mode, confirm repository access, status label mappings, issue reads by configured status labels, no-persistent repository write probes, issue-write permission-class proof, and rate-limit visibility. Label mode should not create or require a status-labeled scratch issue by default. Settracker.write_probe_issuein label mode only for legacy/deep issue-object proof; after switching away from an old scratch issue, remove its Detent status label or close it so it stops appearing as board work. Verify:detent doctor --allow-write-probes -
Verify the systemd service PATH when Detent runs as a user service. User services do not inherit the interactive shell PATH.
detent doctorverifies Detent's direct dependencies, but a first dispatch can still fail if repo hooks or validation gates call tools from project-local, language manager, or user binary directories that are missing from the service environment. Copy the exactEnvironment=PATH=...value fromdetent.service, then verify Detent tools and every command used byhooks.*and the selected gate from that same service context. Replace the placeholder tools below with the actual binaries required by the target repo, such asmix,bundle,ruby,node,pnpm,python,cargo,composer,mvn,gradle,dotnet, or a static-site generator:systemd-run --user --wait --collect --pipe \ --property=Environment=PATH=<same-path-as-detent.service> \ /usr/bin/bash -lc ' for tool in gh codex git detent <gate-tool> <hook-tool>; do command -v "$tool" done 'Add every missing tool directory to the service PATH before dispatching. Use the directories required by the target repo's language manager and selected validation gate. For example:
Environment=PATH=/home/<user>/.local/bin:/home/<user>/.asdf/shims:/home/<user>/.cargo/bin:/usr/local/bin:/usr/bin:/binKeep the service's default
KillMode=control-group. Detent gives each worker a dedicated process group but leaves it inside the service cgroup; persisted process identity lets Detent reap stale workers, and systemd then remains the final backstop. Reject worker launch wrappers that double-fork or move the worker into another cgroup.When the project defines
hooks.after_createor other bootstrap hooks, dry-run that hook or the equivalent repo bootstrap script from an isolated throwaway worktree with the same service PATH before moving an issue toTodo.
Phase 6 — Issue Intake
Before adding issues to a ProjectV2 board, setting issue-field values, changing status labels, or enabling ProjectV2 auto-add, rerun:
test -f "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
rg '^INTAKE_GH_FLAGS=' "$ONBOARDING_DIR/answers.env"
rg '^INITIAL_STATUS=' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
-
Confirm the selected initial status option exists. In ProjectV2 mode, if this fails on a fresh board, start Detent once with no dispatchable items so it can auto-provision missing options, then repeat this verification. In label mode, verify the initial status label exists before assigning it. Verify:
# ProjectV2 mode: gh project field-list <project-number> --owner <project-owner> --format json \ --jq '.fields[] | select(.name == "Status") | .options[].name' | rg -x '<initial-status>' # Boardless issue-field mode: jq -r '.options[].name' "$ONBOARDING_DIR/issue-field.json" | rg -x '<initial-status>' # Label mode: rg -x '<status-label-prefix><initial-status-slug>' \ "$ONBOARDING_DIR/repo-labels.txt" -
ProjectV2 intake: bulk-add issues by the selected filter and set initial
Status. Run only whenGITHUB_MODE=project_v2. Use the exactgh issue listflags from the intake answer. UseBacklogfor broad intake andTodoonly for work that should dispatch immediately. This verifies the writeprojectscope if no earlier board creation, linking, field creation, or item edit has already done so.gh project item-addandgh project item-editare GraphQL mutations, so do not start a broad intake when the budget warning from Phase 1 is still unresolved. Verify with one cached inventory after the mutations finish:rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" PROJECT_NODE_ID="$(gh project view <project-number> --owner <project-owner> --format json --jq '.id')" gh project field-list <project-number> --owner <project-owner> --format json \ > "$ONBOARDING_DIR/project-fields.intake.json" STATUS_FIELD_ID="$(jq -r '.fields[] | select(.name == "Status") | .id' "$ONBOARDING_DIR/project-fields.intake.json")" STATUS_OPTION_ID="$(jq -r '.fields[] | select(.name == "Status") | .options[] | select(.name == "<initial-status>") | .id' "$ONBOARDING_DIR/project-fields.intake.json")" gh issue list --repo <repo-owner>/<repo-name> --state open <chosen-gh-issue-list-flags> \ --limit 1000 --json url --jq '.[].url' | while IFS= read -r issue_url; do item_id="$(gh project item-add <project-number> --owner <project-owner> --url "$issue_url" --format json --jq '.id')" gh project item-edit \ --id "$item_id" \ --project-id "$PROJECT_NODE_ID" \ --field-id "$STATUS_FIELD_ID" \ --single-select-option-id "$STATUS_OPTION_ID" >/dev/null done gh project item-list <project-number> --owner <project-owner> --format json --limit 1000 \ > "$ONBOARDING_DIR/project-items.after-intake.json" jq '[.items[] | select(.content.repository == "<repo-owner>/<repo-name>" and .status == "<initial-status>")] | length' \ "$ONBOARDING_DIR/project-items.after-intake.json" -
Issue-field intake: set initial issue-field Status on selected issues. Run only when
GITHUB_MODE=issue_field. UseBacklogfor broad intake andTodoonly for work that should dispatch immediately. Issue-field writes can trigger notifications and secondary rate limits, so keep broad edits deliberate and usedetent doctor --allow-write-probesto prove the write probe first. Verify:rg '^GITHUB_MODE=issue_field$' "$ONBOARDING_DIR/answers.env" rg '^STATUS_FIELD_NAME=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" STATUS_FIELD_ID="$(jq -r '.id' "$ONBOARDING_DIR/issue-field.json")" gh issue list --repo <repo-owner>/<repo-name> --state open <chosen-gh-issue-list-flags> \ --limit 1000 --json number --jq '.[].number' | while IFS= read -r issue_number; do jq -n --argjson field_id "$STATUS_FIELD_ID" --arg value "<initial-status>" \ '{issue_field_values: [{field_id: $field_id, value: $value}]}' | gh api --method POST \ "/repos/<repo-owner>/<repo-name>/issues/${issue_number}/issue-field-values" \ --input - >/dev/null done gh issue list --repo <repo-owner>/<repo-name> --state open <chosen-gh-issue-list-flags> \ --limit 1000 --json number,url > "$ONBOARDING_DIR/issues.after-intake.json" jq '. | length' "$ONBOARDING_DIR/issues.after-intake.json" -
Label intake: set the initial status label on selected issues. Run only when
GITHUB_MODE=label. UseBacklogfor broad intake andTodoonly for work that should dispatch immediately. Each issue should have exactly one configured status label with the selected prefix. Preserve ordinary labels such asdocumentation,bug, orenhancement; remove only labels that start withSTATUS_LABEL_PREFIX. Verify:rg '^GITHUB_MODE=label$' "$ONBOARDING_DIR/answers.env" rg '^STATUS_LABEL_PREFIX=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" STATUS_LABEL_PREFIX="<status-label-prefix>" STATUS_LABEL="${STATUS_LABEL_PREFIX}<initial-status-slug>" gh issue list --repo <repo-owner>/<repo-name> --state open <chosen-gh-issue-list-flags> \ --limit 1000 --json number --jq '.[].number' | while IFS= read -r issue_number; do gh api "repos/<repo-owner>/<repo-name>/issues/${issue_number}/labels" \ --jq '.[].name' | while IFS= read -r label; do case "$label" in "$STATUS_LABEL_PREFIX"*) encoded_label="$(jq -rn --arg value "$label" '$value | @uri')" gh api --method DELETE \ "/repos/<repo-owner>/<repo-name>/issues/${issue_number}/labels/${encoded_label}" \ --silent ;; esac done gh api --method POST \ "/repos/<repo-owner>/<repo-name>/issues/${issue_number}/labels" \ -f "labels[]=$STATUS_LABEL" \ --silent done gh issue list --repo <repo-owner>/<repo-name> --state open \ --label "$STATUS_LABEL" <chosen-gh-issue-list-flags> \ --limit 1000 --json number,url > "$ONBOARDING_DIR/issues.after-intake.json" jq '. | length' "$ONBOARDING_DIR/issues.after-intake.json" -
Optionally enable ProjectV2 auto-add. Run only when
GITHUB_MODE=project_v2. This is a human UI step because GitHub's built-in ProjectV2 auto-add workflows are not configurable through the API. Click: Project -> ... -> Workflows -> Auto-add to project. Configure the same repo and filter chosen in the interview. Verify:rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" gh project item-list <project-number> --owner <project-owner> \ --query 'repo:<repo-owner>/<repo-name> is:issue is:open' \ --format json > "$ONBOARDING_DIR/project-items.auto-add.json" jq '.totalCount' "$ONBOARDING_DIR/project-items.auto-add.json"
Phase 7 — Smoke Test
Before starting Detent, restarting a service, hot-reloading a running process,
or moving a smoke-test issue to Todo, rerun:
test -f "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
-
Start Detent or hot-reload the running process. Use the configured port, not
4000when another Detent instance owns that port. Use the dashboard host chosen in Phase 2:127.0.0.1for SSH tunnels, a private/Tailscale IP for VPN-only exposure, or0.0.0.0only on trusted private networks because it exposes every interface. Verify:detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" detent --host <dashboard-host> --port <port>For a user-level systemd service, use the same bind choice in
ExecStart, then restart the user service:ExecStart=/home/<user>/.local/bin/detent --headless --host <dashboard-host> --port <port>In another shell, verify the listener and the API URL that should work from that shell. Use
127.0.0.1for localhost-only binds, the selected private/Tailscale IP for VPN-only binds, or127.0.0.1for same-host checks when binding0.0.0.0:ss -ltnp | rg ':<port>|detent' curl -fsS http://<dashboard-check-host>:<port>/health | jq -e '.status == "ok" and .mode == "running"' curl -fsS http://<dashboard-check-host>:<port>/api/v1/stateIf the chosen host is a private/Tailscale IP or
0.0.0.0, verify the remote URL from another machine on that network:curl -fsS http://<tailscale-or-private-ip>:<port>/api/v1/state -
Check rate-limit budget before smoke dispatch. In ProjectV2 mode, this is the last stop before Detent and the spawned agent start spending GraphQL budget for polling, workpad, status, PR, and review work. In boardless issue-field and label modes, review REST core and GraphQL visibility from read-only
detent doctor --port 0; issue-field polling, status-label polling, and status writes are REST-backed, while PR relationship checks can still use GraphQL. Verify:# ProjectV2 mode: gh api rate_limit --jq '.resources.graphql | {limit, used, remaining, reset}' \ > "$ONBOARDING_DIR/graphql-rate-limit.before-smoke.json" jq -r '"graphql remaining=\(.remaining) reset=\(.reset)"' \ "$ONBOARDING_DIR/graphql-rate-limit.before-smoke.json" jq -e '.remaining >= 500' "$ONBOARDING_DIR/graphql-rate-limit.before-smoke.json" \ || printf 'WARNING: low GitHub GraphQL budget; defer smoke dispatch or use GitHub App auth\n' # Boardless issue-field mode: detent doctor --port 0 | rg 'GitHub API rate limit|GitHub issue field' # Label mode: detent doctor --port 0 | rg 'GitHub API rate limit|GitHub status label' -
Move one real issue to
Todo. Use a real issue that matches the authorization filters. Do not verify this by polling ProjectV2 after the edit; switch to the local Detent API in the next step. Verify:# ProjectV2 mode: rg '^GITHUB_MODE=project_v2$' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" gh project field-list <project-number> --owner <project-owner> --format json \ > "$ONBOARDING_DIR/project-fields.smoke.json" TODO_OPTION_ID="$(jq -r '.fields[] | select(.name == "Status") | .options[] | select(.name == "Todo") | .id' "$ONBOARDING_DIR/project-fields.smoke.json")" STATUS_FIELD_ID="$(jq -r '.fields[] | select(.name == "Status") | .id' "$ONBOARDING_DIR/project-fields.smoke.json")" PROJECT_NODE_ID="$(gh project view <project-number> --owner <project-owner> --format json --jq '.id')" ITEMS_JSON="$ONBOARDING_DIR/project-items.after-intake.json" if ! test -f "$ITEMS_JSON"; then gh project item-list <project-number> --owner <project-owner> --format json --limit 1000 \ > "$ONBOARDING_DIR/project-items.smoke.json" ITEMS_JSON="$ONBOARDING_DIR/project-items.smoke.json" fi ITEM_ID="$(jq -er '[.items[] | select(.content.url == "https://github.com/<repo-owner>/<repo-name>/issues/<issue-number>") | .id][0]' "$ITEMS_JSON")" gh project item-edit \ --id "$ITEM_ID" \ --project-id "$PROJECT_NODE_ID" \ --field-id "$STATUS_FIELD_ID" \ --single-select-option-id "$TODO_OPTION_ID" # Boardless issue-field mode: rg '^GITHUB_MODE=issue_field$' "$ONBOARDING_DIR/answers.env" rg '^STATUS_FIELD_NAME=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" STATUS_FIELD_ID="$(jq -r '.id' "$ONBOARDING_DIR/issue-field.json")" jq -n --argjson field_id "$STATUS_FIELD_ID" --arg value "Todo" \ '{issue_field_values: [{field_id: $field_id, value: $value}]}' | gh api --method POST \ "/repos/<repo-owner>/<repo-name>/issues/<issue-number>/issue-field-values" \ --input - # Label mode: rg '^GITHUB_MODE=label$' "$ONBOARDING_DIR/answers.env" rg '^STATUS_LABEL_PREFIX=' "$ONBOARDING_DIR/answers.env" detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env" awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env" STATUS_LABEL_PREFIX="<status-label-prefix>" TODO_STATUS_LABEL="${STATUS_LABEL_PREFIX}todo" gh api repos/<repo-owner>/<repo-name>/issues/<issue-number>/labels \ --jq '.[].name' | while IFS= read -r label; do case "$label" in "$STATUS_LABEL_PREFIX"*) encoded_label="$(jq -rn --arg value "$label" '$value | @uri')" gh api --method DELETE \ "/repos/<repo-owner>/<repo-name>/issues/<issue-number>/labels/${encoded_label}" \ --silent ;; esac done gh api --method POST \ "/repos/<repo-owner>/<repo-name>/issues/<issue-number>/labels" \ -f "labels[]=$TODO_STATUS_LABEL" -
Verify the issue dispatches locally. Onboarding is not complete until the issue appears under Running on the dashboard. Once it does, stop operator ProjectV2, issue-field, or label polling; the spawned agent owns GitHub work from here. Verify:
curl -fsS http://127.0.0.1:<port>/api/v1/state \ | jq -e '.running[] | select(.identifier == "<repo-owner>/<repo-name>#<issue-number>")' -
Verify Detent posted the workpad. The issue must have a persistent
## Codex Workpadcomment with adetent-statusfenced block. Verify:gh api repos/<repo-owner>/<repo-name>/issues/<issue-number>/comments --paginate \ --jq '.[] | select(.body | startswith("## Codex Workpad") and (.body | contains("detent-status"))) | .html_url' | rg . -
Verify cancellation cleanup when testing terminal moves. Moving an item to
Cancelledis a Detent workflow transition; Detent does not close the GitHub issue and does not close or comment on linked pull requests. Operators own those repository-record decisions. Detent stops any running agent, releases the dispatch slot and claim lease, records the run as terminal, and reaps Detent-owned workspace resources on the next poll that observes the cancelled terminal state. Confirm the cleanup diagnostic is visible:curl -fsS http://127.0.0.1:<port>/api/v1/state \ | jq -e '.events[] | select(.event == "workspace_reap_succeeded" and (.message | contains("reason=cancelled")) and (.message | contains("worktrees=")) and (.message | contains("branches=")) and (.message | contains("processes=")))'If cleanup fails, Detent records
workspace_reap_failed, leaves the workspace eligible for a later retry, and includes the cleanup error in the event message. If no workspace reaper is configured for a terminal run, Detent recordsworkspace_reap_unverified.
Reconfiguration Closeout
Use this checklist after editing an existing Detent setup, especially after
changing global.yaml or WORKFLOW.md.
Before any closeout command that edits config, restarts a service, reruns write-probe preflight, or moves issues, rerun:
test -f "$ONBOARDING_DIR/answers.env"
rg '^GITHUB_MODE=(project_v2|issue_field|label)$' "$ONBOARDING_DIR/answers.env"
detent onboarding validate-answers --answers "$ONBOARDING_DIR/answers.env" --phase mutation
rg '^MUTATION_CONFIRMED=true$' "$ONBOARDING_DIR/answers.env"
awk 'NF {last=$0} END {exit last == "MUTATION_CONFIRMED=true" ? 0 : 1}' "$ONBOARDING_DIR/answers.env"
-
Verify the binary you are about to run. After pulling, rebuilding, or installing updates, confirm the expected version, commit, and build date:
detent version -
Run the preflight again.
detent doctor --allow-write-probesshould pass before starting a stopped instance because Phase 5 expects the configured server port to be free:detent doctor --allow-write-probesIf Detent is already running on the configured port, the server-port check can fail because the live service owns that port. In that case, rerun the config, toolchain, token, and database preflight with an ephemeral port, then verify the live service separately:
detent doctor --port 0 --allow-write-probes curl -fsS http://<dashboard-check-host>:<port>/health | jq -e '.status == "ok" and .mode == "running"' -
Reload the runtime that needs the change. Detent watches the active
global.yaml, including symlinked config targets, and applies this reload matrix:Field Reload behavior Project list and project settings Live reload Credentials: github_token,trust_loopback_peer_read, and project credentialsLive reload global.startupLive reload instance_nameLive reload global.identityLive reload; project runtimes restart in-process and /api/v1/state.instance.nameupdates after the next telemetry snapshotglobal.max_concurrent_agents,global.scheduling,global.fair_shareLive reload at the next dispatch decision; lowering capacity drains to the new limit without interrupting active workers log_levelLive reload port,env,log_max_size_bytes,log_max_backupsRestart required When a changed field requires restart, Detent logs
global config setting change requires restartwith the field name. -
Hold dispatch on failed preflight. Do not move new work to
Todofrom a faileddetent doctor --allow-write-probesrun unless the only failure is the expected live-port collision,detent doctor --port 0 --allow-write-probespasses, and/healthis green for the running service.
Appendix
ProjectV2 And Boardless Migration Notes
Existing ProjectV2 workflows remain valid. Leaving
tracker.github_status_source unset keeps project_v2 as the default
compatibility path, and tracker.project_slug remains required only for that
mode. Choose this path when humans still use the GitHub Project board as the
source of truth for planning, ranking, and status changes.
Switch to boardless issue-field mode only after the repository has an
organization issue Status field with options matching the Detent workflow.
Change WORKFLOW.md to:
tracker:
kind: github
github_status_source: issue_field
repository: <repo-owner>/<repo-name>
status_field: <status-field-name>
Detent does not automatically migrate ProjectV2 item statuses into issue
fields. Copy existing statuses manually or with a one-off script outside
Detent, then run detent doctor --port 0 --allow-write-probes and fix
repository access, issue field discovery, option discovery, write-probe,
comment-write, and rate-limit checks before dispatching. GitHub issue fields
apply to issues only, so linked PR cards derive status from the linked issue.
Switch to repository label mode only after the repository has status labels
matching the effective workflow states. Change WORKFLOW.md to:
tracker:
kind: github
github_status_source: label
repository: <repo-owner>/<repo-name>
status_label_prefix: "<status-label-prefix>"
Detent does not automatically migrate ProjectV2 item statuses or issue-field
values into labels. With tracker.auto_provision enabled, Detent creates
missing prefixed status labels on startup, but it does not assign those labels
to existing issues. Copy existing statuses by applying exactly one configured
status label per issue, then run detent doctor --port 0 --allow-write-probes
and fix repository access, status label mappings, issue reads by label,
write-probe, comment-write, and rate-limit checks before dispatching. GitHub
status labels apply to issues only, so linked PR cards derive status from the
linked issue.
Use local-only GitHub status mode when GitHub issues and PRs should remain
read-only inputs. Change WORKFLOW.md to:
tracker:
kind: github_local
repository: <repo-owner>/<repo-name>
local_sqlite:
path: .detent/github-local-work-items.db
Do not set tracker.github_status_source for this mode. Detent reads issue
bodies, labels, assignees, dependencies, linked pull requests, reviews, checks,
and rate-limit health from GitHub, but stores Detent workflow state, priority,
claim fields, audit comments, and local close decisions in SQLite. It does not
create or mutate GitHub Projects, issue fields, repository labels, status
labels, GitHub issue comments, or GitHub issue close state. Import explicit
issues only:
detent github-local import <local-detent-project-id> 123,456 --state Todo
detent doctor --project <local-detent-project-id> --port 0
Unimported issues stay invisible on Detent boards. If an imported issue is closed upstream while still locally active, Detent surfaces that divergence on the card instead of auto-resolving it.
Interview Answers To Config Keys
| Interview answer | Config or system target |
|---|---|
| GitHub status source | tracker.github_status_source: project_v2, issue_field, or label in detent.yaml. |
| GitHub local status | tracker.kind: github_local, tracker.repository, and tracker.local_sqlite.path; no tracker.github_status_source. |
| Board node id | tracker.project_slug in detent.yaml for ProjectV2 mode only. |
| Boardless repository | tracker.repository in detent.yaml for issue-field and label modes. |
| Boardless issue field | tracker.status_field in detent.yaml; defaults to Status when omitted. |
| Status label prefix | tracker.status_label_prefix in detent.yaml; defaults to detent: for label mode. |
| Kanban interaction | Fleet and observer boards stay read-only; trusted operator-owned project boards default to integration, with writes authorized and proven by post-authorization detent doctor --allow-write-probes. |
| Project scheduling priority | projects[].priority in global.yaml. |
| Project scheduling weight | projects[].weight in global.yaml. |
| Project color | Optional projects[].color in global.yaml; missing colors are deterministic and appear in the sidebar and multi-project Kanban. |
| Dispatch label ordering | agent.dispatch_priority_by_label in detent.yaml. |
| Authorization filters | tracker.authorization in detent.yaml; optionally projects[].authorization in global.yaml for host-level scoping. |
| Dashboard bind | server.host in detent.yaml, or --host in the startup command or service ExecStart. |
| Worker model provider default | codex.command: codex app-server; follows provider upgrades and avoids retirement breakage. |
| Worker model pin | codex.command with --config 'model="<model>"'; provides reproducibility or cost control but requires generation maintenance. |
| Worker reasoning effort | Optional model_reasoning_effort Codex config override; unset by default because not every model accepts it. |
| Validation command | gate.kind: command and gate.run in detent.yaml. |
| Automated PR review requirement | gate.kind: command and gate.require_automated_review in detent.yaml. |
| Release-blocking CI names | gate.required_status_checks in detent.yaml; keep it aligned with branch protection or rulesets. |
| Failed CI recovery | gate.kind: command and gate.ci_failure_action in detent.yaml. |
| Validator-agent review | gate.validator.enabled, gate.validator.min_score, and gate.validator.block_on in detent.yaml. |
| Human validation label | gate.kind: human_review and gate.approval_label in detent.yaml. |
| Per-project concurrency | agent.max_concurrent_agents in detent.yaml. |
| Per-role reasoning effort | Optional agent.effort.code, agent.effort.rework, and agent.effort.merge defaults in detent.yaml. |
| Pull request merge strategy | deliverable.merge_method: squash, merge, or rebase; defaults to squash. |
| Merge serialization | agent.max_concurrent_agents_by_state.Merging: 1 in detent.yaml. |
| Session catastrophe bounds | agent.max_turns, agent.max_session_duration_ms, and agent.no_progress_timeout_ms; keep agent.max_session_tokens as an additional token-consumption backstop, while agent.max_session_context_multiplier remains absent unless explicitly requested as a coarse ceiling. |
| Hard-stop review policy | agent.auto_promote.enabled: false in detent.yaml. |
| Criteria-based auto-promote | agent.auto_promote.enabled, quiet_seconds, optout_label, and allowed_issue_labels in detent.yaml. |
| Prompt body | The complete Markdown content of prose-only WORKFLOW.md. |
| Intake filter | gh issue list flags and optional GitHub Project auto-add workflow. |
| Initial issue status | ProjectV2 or issue-field Status value, or one status label in label mode, usually Backlog or Todo. |
What A Good Detent Issue Looks Like
Use issues as executable contracts. A good issue has enough specificity that an agent can finish without inventing product intent.
Per-Issue Agent Overrides
An issue body can override the model, reasoning effort, or both for that
issue's worker sessions with a fenced detent-agent YAML block:
schema: 1
effort: xhigh
merge:
effort: high
The schema: 1 field is required. model, effort, and the optional code,
rework, and merge role maps are accepted; each role map accepts only an
effort key. Omit model to inherit the configured route or provider default.
When an issue has multiple complete detent-agent blocks, the last block wins.
Detent accepts a single YAML document. Invalid YAML, unknown fields, unsupported
schemas, unavailable models, and unsupported effort values are rejected.
Rejected fields fall back to the next applicable default, and Detent posts a
rejection comment identifying what it ignored. Because parsing uses strict
known-field validation, older Detent binaries reject role maps they do not yet
recognize; single-binary fleets should upgrade before adding this syntax.
Effort precedence is the issue's role effort, the issue-wide effort, the
project's role default, then the selected backend or provider configuration.
Configure project defaults under agent.effort:
agent:
effort:
code: xhigh
merge: high
Unset roles retain the backend/config behavior. An unset rework role inherits
the code role value at both the issue and project levels; set rework.effort or
agent.effort.rework to choose a distinct value. Role efforts are validated
against the effective model for that role at dispatch and by detent doctor.
Define a project-specific effort rubric in the repository's agent-facing docs,
such as AGENTS.md or CLAUDE.md. Issue authors should select the least effort
appropriate for the work so routine issues do not inherit a more expensive
fleet baseline than they need.
## Problem
What is broken, missing, or valuable, and who cares?
## Scope
- Files, packages, screens, commands, or docs expected to change.
- Explicit non-goals so the agent does not expand the work.
## Acceptance Criteria
- [ ] Observable behavior or documentation outcome.
- [ ] Edge case or failure mode covered.
- [ ] README, docs, or examples updated when user-facing behavior changes.
## Validation
- Exact command the agent must run, such as `make check`, `mix test`,
`bundle exec rspec`, `npm test`, or another repo-specific gate.
- Any focused tests or manual checks expected before the full gate.
## Dependencies
Depends on: #<issue-number>
State whether the dependency must be merged into `origin/main` before this
issue starts. If there is no dependency, omit the line.
Keep dependency order explicit. If issue B relies on issue A, prefer GitHub's
native blocked_by relation for issue B, then declare the same dependency in
the Workpad detent-status block when the issue is actively blocked. During the
deprecation window, issue-body Depends on: and Blocked by: lines remain a
fallback for projects that have not migrated; same-repo #A, cross-repo
owner/repo#A, and full https://github.com/owner/repo/issues/A issue URLs are
supported there.
If the project has opted into tracker.dependency_auto_unblock.enabled, issue B
can sit in a configured waiting state such as Blocked with native dependency
metadata or the legacy fallback line. Detent will move it to the configured
ready state after every blocker is terminal, closed, or merged according to the
workflow readiness rule. Do not use that mode for free-form human blockers
without explicit dependency references. If auto-unblock is disabled, a
dependency-waiting issue in Blocked will remain there even after the
dependency clears.