Skip to content
Docs Get started Mirrored upstream
ONBOARDING.md
Docs source v0.57.0 154392918736 View this file at the pinned commit ↗

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-only detent doctor --port 0 before proposing changes. Do not pass --allow-write-probes during 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 existing global.yaml, preserve current runtime settings unless the human chooses otherwise, then create or adopt a board, author WORKFLOW.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

  1. Confirm Detent is installed or intentionally new. For new-install, follow Bootstrap On A New Machine steps 1-3 before project onboarding. For existing-install and add-project, verify the detected binary and config path before changing anything. Verify:

    command -v detent
    detent version
    detent --format pretty config path
    
  2. Confirm GitHub CLI auth and scopes. Detent needs GitHub auth for the selected tracker mode. Before the identity gate, verify only the local gh auth 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 records GITHUB_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 repo and read:org. Label mode needs repository issue and label read/write access; classic PATs can use repo.

    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 gh starts polling, open https://github.com/login/device in 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_MODE answer, run only the selected mode's target-specific read probe: gh project list --owner <project-owner> --format json --limit 1 for ProjectV2, gh api /orgs/<repo-owner>/issue-fields for issue-field mode, or gh api repos/<repo-owner>/<repo-name>/labels --paginate for label mode. Defer write project verification 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.

  3. Confirm Codex is installed and signed in. Detent dispatches agents through the Codex app-server. Verify:

    codex --version
    
  4. 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.

  1. 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
    
  2. 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_field or GITHUB_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
    
  3. 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.md prompt; do not infer them from Detent defaults or expect detent doctor to discover them. If make check exists, recommend gate.kind: command with gate.run: make check. Otherwise recommend the closest local equivalent for the detected ecosystem, such as mix 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, recommend gate.kind: human_review with 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 code and cloud pools, 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. Existing agent_pools are never repartitioned automatically.

    If the diagnostic reports status: env_polluted, record the failing command, relevant_environment_keys, and passing_sanitized_command. Use recommended_gate_command as 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.

  4. Inspect existing global scheduling. Show the current project table before recommending priority and weight. Recommend weight: 1 and priority: 3 when 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"
    
  5. 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"
    
  6. 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_field and GITHUB_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"
    
  7. Inspect priority counts for ProjectV2 reuse candidates. priority_in depends on the ProjectV2 Priority field, so gather counts from the strongest reuse candidate with one paginated inventory pass saved to a local artifact. Do not repeatedly call gh project item-list --limit 1000. For a new board or non-ProjectV2 mode, record an empty count table and recommend no priority_in filter 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"
    
  8. 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.

  1. Mode. DETENT_ONBOARDING_MODE was selected in Phase 0.5. If discovery shows the chosen mode is wrong, stop, update the identity answers, present the full interpretation again, and rerun detent 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.

  2. GitHub status source. GITHUB_MODE was selected in Phase 0.6. If discovery shows the chosen status source is wrong, stop, update the explicit answer, rerun detent 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, or global.yaml mutation without this explicit GITHUB_MODE answer.

  3. 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"
    
  4. Boardless issue field. Ask this only when GITHUB_MODE=issue_field: "Which organization issue field should Detent use for status?" Recommend Status unless 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"
    
  5. Repository status labels. Ask this only when GITHUB_MODE=label: "What repository label prefix should Detent use for status?" Recommend detent: 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 through tracker.state_map, slugifies the resulting state name, and prefixes it: Todo maps to detent:todo, In Progress maps to detent:in-progress, and with the default release flow Cancelled: Done state map, Cancelled maps to detent:done. These labels are the status source of truth; they are distinct from tracker.authorization.labels.* filters, projects[].authorization, and agent.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"
    
  6. 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.env keys 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.env fields as a secondary implementation mapping:

    • Full autopilot: keep completed work in its active lane and promote it to Merging when 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 Review until a human explicitly approves promotion to Merging; 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 recommend AUTO_PROMOTE_ENABLED=false unless the operator selected review gate or conservative/manual. Do not recommend stopping at Human Review unless 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, writing WORKFLOW.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_PROFILE before 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_PROFILE first:

    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.

  7. 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 omit DELIVERY_PROFILE before recording the mixed policy: "Should Detent's project Kanban be read-only or allow GitHub mutations from the dashboard?" Keep fleet /kanban read-only. For a project-scoped board on an operator-owned local or private Detent instance, recommend integration by 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-probes must 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, recommend read_only. If failed post-authorization write probes prevent integration, stop and repair permissions or ask whether to downgrade to read_only. Do not use skipped pre-mutation write probes as evidence for read_only. Verify:

    printf '%s\n' \
      'KANBAN_MODE=<read_only|integration>' \
      >> "$ONBOARDING_DIR/answers.env"
    rg '^KANBAN_MODE=' "$ONBOARDING_DIR/answers.env"
    
  8. Scheduling. Ask: "What global.yaml priority from 0-4 and weight should this project receive?" Show $ONBOARDING_DIR/global-projects.txt. Disambiguate this from the board Priority field: global.yaml priority ranks projects on the host; the board Priority field 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"
    
  9. Project color. Ask: "Should this project have a fixed dashboard color, or should Detent assign one automatically?" Show $ONBOARDING_DIR/global-projects.txt so existing colors are discoverable. Explain that projects[].color is 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 /kanban board, 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"
    
  10. 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.json and recommend an ordered list from labels that represent work type or risk, such as bug, regression, then enhancement when those labels are common. Explain that Priority still wins first when available, unlisted labels rank last, and an empty answer means no label ordering. In label status-source mode, do not use agent.dispatch_priority_by_label for detent:* 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"
    
  11. 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"
  1. 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, and priority_in. Recommendation source: live counts in $ONBOARDING_DIR/issue-counts.json and $ONBOARDING_DIR/priority-counts.json, plus any repo/workstream labels already in use. Show the total count for none, counts for each label, assignee, author, and priority option, and the remaining count for any proposed labels.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 recommend author_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, set author_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"
  1. 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.1 for localhost-only access through SSH tunnels. Use a specific private or Tailscale IP for VPN-only exposure. Use 0.0.0.0 only 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"
  1. 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 omit DELIVERY_PROFILE before 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_command from $ONBOARDING_DIR/gate-diagnostic.json when it reports pass or env_polluted; otherwise detected make check when present with require_automated_review: true; otherwise use the strongest ecosystem-specific command from the repo evidence, or kind: human_review with approval_label: human-approved when 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.

  1. 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.

  1. Concurrency. Ask: "How many agents may this project run at once?" Recommendation source: host capacity, existing global.yaml projects, and the repo's gate cost. Default if silent: agent.max_concurrent_agents: 5 for an active code repo, lower for expensive gates. State that Merging: 1 is required. If the selected profile already supplied MERGING_CONCURRENCY=1, do not ask Merging concurrency again; record only MAX_CONCURRENT_AGENTS. If the operator wants to override a selected profile's merging concurrency, switch to Custom/advanced and remove or omit DELIVERY_PROFILE before 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.

  1. 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 omit DELIVERY_PROFILE before recording the mixed policy: "Should Detent hard-stop at Human Review, or may it auto-promote to Merging after 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 write AUTO_PROMOTE_ENABLED=false unless 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"
  1. 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 omit DELIVERY_PROFILE before recording the mixed policy: "Should dependency-waiting issues stay in Todo and be gated by Detent, or should they sit in Blocked and be auto-unblocked when dependencies clear?" Default if silent: tracker.dependency_auto_unblock.enabled: false. Use the Blocked auto-unblock mode only when the team declares dependency blockers through native GitHub blocked_by relations or the structured detent-status Workpad block. During the deprecation window, existing machine-readable issue-body Depends on: or Blocked 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 in Blocked while this setting stays disabled, Detent will only display them as blocked and will not move them back to Todo. 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"
  1. 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"
  1. Issue intake. Ask: "Which issue filter should be bulk-added, should the initial Status be Backlog or Todo, and should the human enable the auto-add workflow?" Recommendation source: $ONBOARDING_DIR/issue-counts.json and the authorization answer. Default if silent: bulk-add the narrowest safe filter to Backlog, then move one known issue to Todo for 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"
  1. 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"
    
  2. Create a board when the answer says create. Run only when GITHUB_MODE=project_v2. GitHub creates the default Status field; 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"
    
  3. Ensure the Priority field exists. Run only when GITHUB_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. If Priority exists 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"))'
    
  4. 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'
    
  5. Confirm required ProjectV2 fields. Run only when GITHUB_MODE=project_v2. Detent auto-provisions missing Status and Priority options 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))'
    
  6. 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 Status options from detent.yaml on 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 progress versus In Progress. If this project uses custom state names in detent.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, or Done as 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 to Backlog before Detent starts?"

    Also ask what to do with empty non-Detent Status options 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. If Backlog does 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 to Todo. 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
    
  7. 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 comm output 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.

  8. Verify repository status labels when selected. Run this only when GITHUB_MODE=label. Detent needs one repository label for each effective configured workflow state. Apply tracker.state_map first, then slugify the result and prepend tracker.status_label_prefix. With the default prefix and release-flow Cancelled: Done mapping, the required labels are:

    detent:backlog
    detent:todo
    detent:in-progress
    detent:blocked
    detent:human-review
    detent:rework
    detent:merging
    detent:done
    

    Create or verify the labels before the first detent doctor run so doctor can prove readiness instead of reporting missing label mappings. Detent's default tracker.auto_provision: true creates 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 comm prints 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_PREFIX or custom workflow states, generate the required label list from the actual WORKFLOW.md state 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"
  1. 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.yaml and docs/templates/WORKFLOW.project_v2.md files, with equivalent docs/templates/detent.issue_field.yaml with docs/templates/WORKFLOW.issue_field.md, and docs/templates/detent.label.yaml with docs/templates/WORKFLOW.label.md. The CLI-only local-status pair uses detent.github_local.yaml and WORKFLOW.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 from docs/templates/detent.non_code_artifact.yaml and docs/templates/WORKFLOW.non_code_artifact.md; the rest of this onboarding flow remains GitHub-focused. The /onboarding web wizard can write the same tracker blocks interactively: choose Repository labels for GITHUB_MODE=label, enter the repository and status label prefix such as detent:, and it will generate tracker.github_status_source: label in detent.yaml without 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.md
    

    When 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 Workpad contract so every update includes a fenced detent-status block, blockers are declared in that block, and narrative Workpad sentences are treated as prose only. Dependency blockers should prefer GitHub's native blocked_by relation, then the structured Workpad block, with issue-body Blocked by: or Depends on: lines kept only as a labeled legacy fallback during the deprecation window. Add or tighten a ## Required Execution Flow section when it is missing, with explicit For Todo, For In Progress, For Rework, and For Merging instructions. The For Merging section should invoke $go-workflow:ship, should not call gh pr merge directly outside ship, and should require exactly one terminal outcome: pull request merged and issue moved to Done, issue moved to Rework with an actionable defect, or issue remaining in Merging with a concrete external blocker recorded in the detent-status block. Before dispatching Merging, confirm the Detent host's Codex environment exposes $go-workflow:ship; otherwise install or enable that workflow, or replace the For Merging section with equivalent project-local merge instructions.

    Add a ## Project CI Quality Gates section 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, and detent doctor does 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 doctor estimates 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 rewrites WORKFLOW.md; use the existing skill-creation review flow described below to perform any extraction.

  2. Substitute the tracker and workspace answers in detent.yaml. In ProjectV2 mode, use the ProjectV2 node id as tracker.project_slug. In boardless issue-field mode, set the repository and issue field. In label mode, set the repository and status-label prefix. Set write_probe_issue for ProjectV2 or issue-field status-write proof; in label mode, set it only when using legacy/deep issue-object probes. Use absolute paths for workspace.source_root and workspace.root, and leave tracker.api_key out unless this workflow intentionally carries a workflow-local token instead of using github_token: gh from global.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.db
    

    Do not add tracker.github_status_source to github_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
    
  3. 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 integration for a trusted operator-owned local or private Detent instance. Keep fleet /kanban read-only; this setting only controls /projects/<id>/kanban. For a shared observer dashboard or explicit no-writes choice, set read_only. If post-authorization write probes fail, stop and repair permissions or ask whether to downgrade to read_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
    
  4. Set the dashboard bind from the interview. This writes the default server.host used when Detent starts without an explicit --host. Service managers can still override it in ExecStart with 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
    
  5. Set the worker model from the interview. Render the explicit answer in codex.command and 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_effort config override only when the operator explicitly requests it and the selected model accepts that setting.

  6. 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 Review or routes to Rework, 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.yaml
    

    Command 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:
          - p1
    

    Human 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: true
    

    Claude Code auth is ambient. A logged-in claude CLI uses subscription limits; ANTHROPIC_API_KEY uses API billing and takes precedence when both are present. Detent stores no Anthropic keys. Subscription limits in headless claude -p runs 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 and ANTHROPIC_API_KEY for sustained or parallel fleet runs. For fleet isolation, set a distinct CLAUDE_CONFIG_DIR per worker and use --no-session-persistence through extra_args when session continuity is not needed. Codex uses an OS-level workspace-write sandbox; Claude Code runs with permission_mode: bypassPermissions inside 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 and allowed_tools/disallowed_tools to tighten the role. For local Anthropic-compatible inference, point ANTHROPIC_BASE_URL at a local server such as Ollama and use the same claude_code backend; see Local Models With Codex And Ollama for model and context-window guidance.

  7. 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.yaml
    

    Label 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: 3
    

    Criteria-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: required requires a current-head automated GitHub PR review. optional waits until the gate deadline and then promotes if every other check passes; off does not wait. Any observed P1 bot findings still route the item to Rework. gate.required_status_checks should 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 to Rework by default; set ci_failure_action: skip only when red CI should stay parked in Human Review. agent.auto_promote.rework_limit defaults to 3; set it to 0 only when repeated rework should remain unlimited. A positive value requires Blocked in 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. With gate.validator.enabled: true, a validator-agent reviews the PR diff against issue acceptance criteria before auto-promotion; scores below min_score or findings whose severity appears in block_on route the item to Rework. detent doctor --port 0 reports sampled Human Review candidates and reasons such as automated_review_missing when 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_multiplier absent unless the operator explicitly requested the coarse ceiling. The primary catastrophe bounds are 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 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. Keep agent.no_progress_token_limit positive as the cross-session brake, especially when budget.billing_mode: subscription makes USD controls inert.

    Dependency auto-unblock default:

    tracker:
      dependency_auto_unblock:
        enabled: false
        source_states:
          - Blocked
        target_state: Todo
        readiness: terminal_or_merged
    

    Enable it only for projects that use Blocked as a dependency-waiting state with explicit machine-readable dependency references. Without this enabled, Blocked is an observed/display state and dependency completion will not move issues back to Todo.

    Blocked recovery default:

    tracker:
      blocked_recovery:
        enabled: false
        source_states:
          - Blocked
        target_state: Rework
        reason_codes:
          - merge_conflict
          - stale_base
          - missing_current_head_ci
    

    Enable it only when structured Blocked-entry reason codes should authorize PR repair. An intentional recovery park records reason_code: merge_conflict, stale_base, or missing_current_head_ci in its blocked detent-status block. A matching current PR condition and a new diff/base fingerprint are also required; issue descriptions and manual parking events never authorize recovery.

  8. Write the prompt body. Keep the ## Codex Workpad instruction, include the detent-status schema examples, include the native blocked_by dependency 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 Flow unless the human explicitly chooses stronger project-specific instructions. The flow should tell agents what to do in Todo, In Progress, Rework, and Merging, including the For Merging requirement to use $go-workflow:ship, record external blockers in the structured status block, and move the issue to Done only after the pull request is merged. Include the current state with Current 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
    
  9. Check the workflow contract before registration. This is a structural check; detent doctor --allow-write-probes in 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"
  1. 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
    
  2. Register the project. Skip this if the project is already registered and the human chose to repair or update the existing entry. priority and weight are 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"
    
  3. Set or preserve runtime keys in global.yaml. For local onboarding, prefer github_token: gh so Detent resolves the token from gh auth token at 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 in detent.yaml. Set instance_name to the interview answer or the short hostname on new installs. For existing installs, preserve env, log_level, github_token, port, and instance_name unless the human selected different values. Use a non-4000 port if another Detent instance is already running; keep the dashboard host in detent.yaml server.host or 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.yaml with github_app_id, github_app_installation_id, and either github_app_private_key or github_app_private_key_path.

  4. 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. Set tracker.write_probe_issue in 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
    
  5. Verify the systemd service PATH when Detent runs as a user service. User services do not inherit the interactive shell PATH. detent doctor verifies 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 exact Environment=PATH=... value from detent.service, then verify Detent tools and every command used by hooks.* and the selected gate from that same service context. Replace the placeholder tools below with the actual binaries required by the target repo, such as mix, 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:/bin
    

    Keep 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_create or 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 to Todo.

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"
  1. 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"
    
  2. ProjectV2 intake: bulk-add issues by the selected filter and set initial Status. Run only when GITHUB_MODE=project_v2. Use the exact gh issue list flags from the intake answer. Use Backlog for broad intake and Todo only for work that should dispatch immediately. This verifies the write project scope if no earlier board creation, linking, field creation, or item edit has already done so. gh project item-add and gh project item-edit are 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"
    
  3. Issue-field intake: set initial issue-field Status on selected issues. Run only when GITHUB_MODE=issue_field. Use Backlog for broad intake and Todo only for work that should dispatch immediately. Issue-field writes can trigger notifications and secondary rate limits, so keep broad edits deliberate and use detent doctor --allow-write-probes to 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"
    
  4. Label intake: set the initial status label on selected issues. Run only when GITHUB_MODE=label. Use Backlog for broad intake and Todo only for work that should dispatch immediately. Each issue should have exactly one configured status label with the selected prefix. Preserve ordinary labels such as documentation, bug, or enhancement; remove only labels that start with STATUS_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"
    
  5. 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"
  1. Start Detent or hot-reload the running process. Use the configured port, not 4000 when another Detent instance owns that port. Use the dashboard host chosen in Phase 2: 127.0.0.1 for SSH tunnels, a private/Tailscale IP for VPN-only exposure, or 0.0.0.0 only 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.1 for localhost-only binds, the selected private/Tailscale IP for VPN-only binds, or 127.0.0.1 for same-host checks when binding 0.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/state
    

    If 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
    
  2. 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'
    
  3. 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"
    
  4. 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>")'
    
  5. Verify Detent posted the workpad. The issue must have a persistent ## Codex Workpad comment with a detent-status fenced 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 .
    
  6. Verify cancellation cleanup when testing terminal moves. Moving an item to Cancelled is 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 records workspace_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"
  1. Verify the binary you are about to run. After pulling, rebuilding, or installing updates, confirm the expected version, commit, and build date:

    detent version
    
  2. Run the preflight again. detent doctor --allow-write-probes should pass before starting a stopped instance because Phase 5 expects the configured server port to be free:

    detent doctor --allow-write-probes
    

    If 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"'
    
  3. 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 credentials Live reload
    global.startup Live reload
    instance_name Live reload
    global.identity Live reload; project runtimes restart in-process and /api/v1/state.instance.name updates after the next telemetry snapshot
    global.max_concurrent_agents, global.scheduling, global.fair_share Live reload at the next dispatch decision; lowering capacity drains to the new limit without interrupting active workers
    log_level Live reload
    port, env, log_max_size_bytes, log_max_backups Restart required

    When a changed field requires restart, Detent logs global config setting change requires restart with the field name.

  4. Hold dispatch on failed preflight. Do not move new work to Todo from a failed detent doctor --allow-write-probes run unless the only failure is the expected live-port collision, detent doctor --port 0 --allow-write-probes passes, and /health is 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.