Detent Hub API
See monthly budget enforcement for organization/project caps, full-issue drain, hard-stop recovery and delayed-cost exposure.
See monthly infrastructure usage for Sprite observation imports, shared UTC aggregation, corrections, scoped API/MCP reads and provider metering limits through the existing usage owner.
Detent Hub owns its SQLite database and exposes fleet coordination through an authenticated HTTP API. Clients must never open or copy the live database files.
This page documents implemented behavior, including native collaboration, Changes and scoped runner enrollment through /api/v2. See self-hosted operations for deployment, export/import and recovery, and artifact deployment for independent durable storage. The native Hub and Cloud RFC defines the broader architecture.
Cloud members get one scoped key for direct API and MCP access from
Settings → API & MCP, at the preserved /settings/mcp bookmark. See
API & MCP setup for organization URLs, tested requests,
private credential handling, agent prompts and the browser approval contract.
Cloud keys do not authorize compatibility, worker, runner or instance-admin routes.
Runner project diagnostics
GET /api/v2/organizations/{organization}/projects/{project}/runner-diagnostics
and the runner_project_diagnostics operator/worker MCP tool read the same
project-scoped service. Optional selectors are runner_id, issue_id,
attempt_id, cursor and limit (1–100). Select a runner when multiple enrolled
owners report the project. The cursor belongs to one observation; restart paging
when a newer heartbeat replaces it.
The existing project-configuration heartbeat carries at most 256 reconciled
attempt records and 256 latest issue admission observations. truncated describes
that source bound; next_cursor describes page continuation. Counts retain the
runtime unsettled count and separate Running, Claimed, deferred, durable-active
and native-claim memberships. Local attempt IDs establish attempt deduplication;
claim-only issue identities do not establish an attempt relationship.
Provider completion is recorded before host finalization. Pending host operations, redacted error classifications, deferred retry timing and lease renewal are separate fields. Admission predicates describe recorded callback/planner decisions and their observation times, never a new diagnostic evaluation. Unknown error text is omitted in full, including customer content, URLs, paths and credentials. The read neither renews nor releases a lease, retries completion, nor changes scheduler/tracker state.
Responses preserve runner and runtime observation times, Hub receipt time, current running build evidence and field-level unavailability. Historical update receipts never supply the running build. Offline, stale and older-runner observations remain explicit; an unreported decision is not an observed refusal. The normal release owner deploys the supported service and reporting runner builds before live operator acceptance. This API does not install a build or recover an attempt.
Work view preferences
Hosted sessions expose GET, PUT, and DELETE at
/api/v2/organizations/{organization}/work-view-preference for the
all-projects Work view, and at
/api/v2/organizations/{organization}/projects/{project}/work-view-preference
for a project view. Each member reads and writes only their own preference;
project scopes require a read grant. These endpoints accept session cookies,
with the bootstrap CSRF token on mutations, and refuse bearer credentials.
The response is { "query": "view=list&tab=closed" }, or { "query": null }
when no preference exists. PUT accepts the same query field containing a
serialized view query of at most 8192 bytes. DELETE restores defaults and is
safe to repeat. Explicit URL parameters override the saved preference without
writing it; user changes save after a debounce. Browser storage caches only
confirmed Hub values under an account, organization, and view scope key.
Saved Work views
/api/v2/organizations/{organization}/work-views stores personal or shared Work
views. The collection returns views and an optional next_cursor; send that
cursor as ?cursor=... to read the next page. Pages contain at most 25 views
and 256 KiB of JSON. Each saved view accepts at most 16 KiB of encoded input.
An owner can store 50 views per organization, and an organization can store 200.
Updates preserve that allocation; deleting a view releases it. Concurrent
creates competing for the same free allocation can return a conflict.
Release notes
Hosted sessions expose GET /api/v2/organizations/{organization}/release-notes
and PUT /api/v2/organizations/{organization}/release-notes/seen. Mutations
require the bootstrap CSRF token; bearer credentials are refused. The write
marks the running release seen for the current organization and user, without
a request body. Preferences persist across devices and only advance, so a
rollback does not make previously seen releases unread.
The response contains builds newest first, has_unread, and an optional
next_cursor. Each build includes version, build_time, running, patch,
unread, internal_count, and entries with category, text, pr_number,
and pr_url. limit defaults to 20 and accepts 1–100; pass next_cursor as
cursor to read older builds. has_unread covers the whole feed, including
builds outside the current page, and is also an additive field on app bootstrap.
A member without a preference is treated as having seen the running release.
The feed is generated and embedded during the binary build. Serving it makes
no external requests. Conventional commit types feat, perf, fix, and
revert map to New, Improved, Fixed, and Removed; internal changes are counted
without publishing their text. Cherry-picks link their original pull request.
Generic completion subjects use the first non-empty commit-body line. When
that line has no meaningful title, the entry reads Maintenance update with
the available original pull-request link and derived category.
Issue email notifications
Hosted sessions expose GET and PUT at
/api/v2/organizations/{organization}/issue-notification-preference.
The response and PUT body are { "enabled": true }. Each user controls
their own preference, which defaults to enabled; mutations require the
bootstrap CSRF token. Bearer keys and support impersonation cannot change it.
Disabling the preference also discards that user's pending batches.
Native issue filers and commenters with human actor identities receive transactional email for lane transitions, new comments, and added related links. Recipients must retain active membership and project read access. The actor's own action is excluded. Changes within one minute of the first event form one batch per issue and participant, persisted in the Hub database. Emails contain the issue number, title, transition or commenter, and links; they exclude issue bodies and comment text. Delivery runs after issue writes commit, and a send failure is logged without rolling back or retrying the write.
Each email links to /issue-notifications/unsubscribe?token=... on a dedicated
Hub, or /organizations/{organization}/issue-notifications/unsubscribe?token=...
through Cloud's shared entry. The opaque link opts out of that issue without
requiring a session and is safe to repeat. The shared entry forwards the token
through the signed service boundary; it does not read or change issue content.
Project deletion
Cloud owners and admins with a write grant on the selected project can call
DELETE /api/v2/organizations/{organization}/projects/{project} with
idempotency_key and confirm_name matching the current project name. The
existing project settings screen requires a named confirmation before sending
the request. Cancellation sends no mutation. Deleted project URLs and API
references return not found, including for previously granted credentials.
Deletion is an atomic tombstone, with no restore action. Project secrets and project access grants (member, API token, runner and unconsumed enrollment) are deleted. Issues, comments, runs, Changes, conversations, artifacts and attachment bytes, and audit and billing history are retained in storage but are no longer accessible through the project. The deleted project's name remains reserved. Retained storage remains subject to existing accounting. The connected GitHub repository, its issues and PRs, other projects, organization settings and shared runners are unchanged.
Deletion is refused until live work leases, running attempts, open workspaces, queued or running scripts, unfinished conversation turns or messages, pending imports and GitHub writes have finished or been cancelled through their existing lifecycle. Sprite pool members that still use this project's token must be removed through the existing pool lifecycle first; organization-owned capacity is retained. No work is implicitly stopped by deletion. Once deleted, the project cannot accept new work, runner grants or repository intake.
Organization Sprite pools
Hosted organizations expose GET and PUT
/api/v2/organizations/{organization}/sprite-pool.
There is one pool for all current and future projects; writes require owner/admin authority and the current
revision (zero for initial configuration). Settings are min_runners (default
zero), max_runners (default zero, disabled; at least the floor), idle_seconds (default
300; range 30–86400), and bootstrap. Enabling requires nonempty customer
bootstrap steps, hosted configuration and the organization secret store.
The response includes current settings, every active member and the most recent
20 deleted members, with lifecycle state, ordinary runner ID and last safe
bootstrap progress log. The runner settings page exposes these reads.
Native projects also expose GET and PUT
/api/v2/organizations/{organization}/projects/{project}/sprite-pool/allocation.
An owner/admin opts a project into managed members by writing desired_count,
expected_revision (a decimal string, initially "0") and idempotency_key.
The organization pool must already be configured. Desired count is independent
of runner job capacity and may exceed available capacity. Reads return
managed, desired_count, allocated_count, shortfall,
provision_failures and revision.
Allocated count includes reserved and retiring members until deletion finishes.
Replays return the original command response; reads return current allocation.
Each failed paid provisioning attempt for the current revision increments
provision_failures; after three, creation for that project pauses until the
desired count is written again, which resets the count.
The existing organization controller reserves members under the shared
organization ceiling and reported provider limits. It retains existing members
and assigns available slots to the oldest eligible unsatisfied project request, breaking
ties by project ID, before satisfying new organization demand. Each member has
one allocation owner. A managed allocation restricts Sprite execution to that
project's own members; it does not restrict authorized local execution. Existing
blended and local_first placement policies continue to permit local runners,
including when the allocation is empty, provisioning failed, or the organization
pool is disabled. sprites_only continues to exclude local runners. Desired zero
holds new managed Sprite claims, including workspace claims, while placement
continues to govern local claims. This supersedes the blanket desired-zero claim
hold from #1219. Organization and manually enrolled Sprites remain available only
to projects that have not opted into managed allocation. Manual adoption is not
supported. Every new member is a fresh runner identity with one
work slot. A managed member receives claims only after its runner's existing
setup observation for the project passes, which includes the provider sign-in
and repository checkout done once in the member's console. A Pi backend,
which has no authentication-status contract, does not block readiness. A runner
re-observes a project that is not ready at most every 30 seconds, so a console
login is picked up without a restart. Before every paid
provisioning step the controller re-reads the project's desired count; a
reservation that no longer fits is cancelled before creation or retired before
it can become eligible, so a completion after a decrement never dispatches.
Decreasing desired count drains surplus members, preferring members that are not
ready, then idle members, then busy ones: a draining member takes no new claims,
keeps its credential so active work can finish and publish, and its Sprite is
deleted only after the Hub holds no unreleased lease for it. An expired lease
or offline heartbeat never proves deletion safe; the lease must be released by
the runner, superseded through existing lease recovery, or released by an
audited runner revocation. Organization members keep the existing idle and
budget-retention checks, and downsizing never retires another allocation's
members. Lowering the organization ceiling retires excess members across allocations,
preserving desired counts and showing the resulting shortfalls. A project whose
budget prevents creation does not block eligible requests or organization demand.
Pool reads include each member's status: provisioning, awaiting_login,
awaiting_setup, ready, sleeping (paused and wakeable through the existing
Service restart), unreachable (credential revoked or expired, or the last wake
failed), draining (retiring with unreleased work), deleting or deleted.
A member awaiting login lists waiting_for steps, each with a check
(provider or github) and the exact console command, such as
codex login --device-auth, gh auth login and gh auth setup-git.
Legacy per-project pools convert in place at schema upgrade: their floors and
ceilings are summed, and their existing members retain Sprite names, runner IDs
and project-token overrides. A forward migration restores totals captured before
the historical capped cutover. If that cutover already deleted the legacy rows
without capturing their totals, the original bounds must be supplied through
the existing pool settings; the upgrade does not infer them from member counts.
Administrators can then change the organization
bounds through the existing pool settings. No project token is promoted
implicitly. Luna's promote_sprites_token previews the first existing project
token in organization project rank order as the organization default; an owner
or admin must explicitly approve it. The preview is bound to that exact source
token, and an existing organization token is never overwritten by promotion.
Store the customer's Sprites organization token through the write-only
/api/v2/organizations/{organization}/secrets/fly_sprites_token endpoint.
Projects inherit this token, with their existing project secret endpoint providing
a write-only override. Each use retains the supplying secret's encryption binding
and audit scope. Pool calls use only api.sprites.dev, refuse redirects, audit
each secret use, and bind each member to the organization used to create it.
Replacing a token with one for a different organization cannot delete old
members; restore authorized access to their original organization to finish
cleanup. Removing the configuring principal's authority stops provider work.
Customer bootstrap runs before enrollment, with DETENT_ORGANIZATION_ID and
DETENT_HUB_URL set. It must configure legitimate provider authentication, Git
credentials, repository checkout and dependencies in the runner's expected
workspace. Deliver secrets through the customer's existing authorized secret
service; do not store credential values in this readable script. The Hub then
runs the pinned public bootstrap script with a fresh organization-scoped enrollment,
pins the runner binary to its own released version, starts the ordinary runner
Service and creates a post-setup checkpoint. Development builds cannot provision
members. Arbitrary command output is suppressed; logs retain stage progress and
failure status without provider or enrollment credentials. Enrollment does not
prove provider authentication, checkout readiness or approved repository policy.
Pool settings accept an organization-scoped placement object. Its mode is
blended (local and Sprite runners eligible without preference weights),
sprites_only (local execution excluded), or local_first. Existing settings
and projects without a pool resolve to blended; omitting placement from a
PUT preserves the saved policy. Changing placement affects new claims only.
Runner owner allowlists, organization project rank, issue priority and age
retain their existing scheduling owner; placement never preempts running work
or modifies grants.
local_first requires explicit positive todo_threshold and overflow_slots
(1–100). There are no implicit numeric defaults and no overflow delay. At the
threshold boundary, overflow becomes eligible; below it, warm Sprites cannot
claim queued work. A Sprite still cannot claim an item with compatible free
local capacity, even when it polls first. Overflow slots bound concurrent Sprite
work, rather than provisioning one Sprite per queued item. max_runners remains
the managed instance ceiling. Optional max_sprite_slots (0–100) bounds Sprite
claims independently of instance count and warm capacity; zero preserves the
existing runner/host/provider limits. Local-first also enforces overflow_slots
as a concurrent Sprite-work limit, using the smaller bound when both are set.
The configured warm floor remains independent of demand, limited to the
overflow size in local-first mode; warm capacity does not authorize claims below
the threshold.
Only unleased, unanswered, dependency-ready, policy-compatible Todo work contributes demand. Backlog, other dispatchable states, unresolved dependencies, workspace items and incompatible model/host requirements never trigger overflow or state promotion. Capacity estimates apply approved requirements, runner and host ceilings, current runner health/availability, and reported shared-provider limits. Existing Sprite provider reports describe bootstrap compatibility; a first pool enrollment has unknown provider readiness until it reports capacity. Claims retain the runner's authoritative provider selection and admission gates. Exhausted Sprite provider accounts exclude Sprite work without excluding an independent eligible local account. Future monthly Sprite budget eligibility belongs in the existing admission owner.
The existing lifecycle uses the same resolved placement snapshot for wake and
provision decisions, and rechecks inside the creation transaction. Pending
bootstrap and recent successful wakes contribute capacity once. Eligible paused
members are woken before creation. The Sprite-pool API returns
placement_decision with the resolved policy, ready Todo demand, compatible
local capacity, Sprite usage, pending capacity, provisionable slots and the
current explanation. Current admission reads and recorded routing refusals
explain claim exclusions; these reads add no UI.
Idle deletion drains members only after active host leases finish and preserves the current floor. Heartbeats, finish/release and the existing Hub maintenance cycle provide lifecycle triggers; no separate scaler or provider retry loop runs. Failed or interrupted bootstrap leaves a member for deletion; a later native scale-up may provision its replacement. PR publication and landing remain on the local runner.
Approved repository policy
Every API claim requires an approved policy_id. Compatibility claims also
supply exactly one repositories entry; native claims use their authenticated
organization/project scope. Policies are resolved on the customer host. See
configuration precedence and approval.
| Endpoint suffix | Method | Authority and payload |
|---|---|---|
/api/v1/repositories/{owner}/{repo}/policy |
GET |
Read current descriptor and approval provenance with a compatibility credential |
/api/v2/organizations/{organization}/projects/{project}/policy |
GET |
Read current descriptor, approval provenance and workflow apply history within native project grants (limit, after) |
| Either policy endpoint | PUT |
Instance administrator only; expected_policy_id (empty for first approval) and either a complete policy descriptor or observed_policy_id from the scoped onboarding read. Exact retries are idempotent; concurrent distinct replacements have one winner. |
| Either policy endpoint | DELETE |
Instance administrator only; expected_policy_id. Revokes current authority while retaining revision/lease history. |
/api/v2/organizations/{organization}/projects/{project}/policy/observed |
POST |
A runner with the heartbeat operation reports its resolved descriptor, including successful matches. The Hub retains the latest report per runner and returns distinct mismatches with all reporting runner IDs in GET .../onboarding. Conflicting configurations and previously approved reports are not new approval candidates. An enrolled runner may include repository_source alongside the descriptor: repository, commit, default_branch, default_branch_head and default_branch_reachable. Every new committed workflow definition reachable from the bound repository's default branch, including replacements of an approved policy, is applied through the existing atomic policy approval path as repository-default-branch. Replaying a historical approval does not replace current authority. Other reports remain pending. |
Enrolled runner credentials receive the current approved descriptor and approval
provenance from GET .../policy, without workflow apply history. Operator reads
retain complete historical definitions and the existing limit and after
pagination. Retained history therefore does not enlarge the runner's current
execution contract.
Hosted organizations approve through PUT .../projects/{project}/onboarding/policy
(owner or admin); the generic PUT .../policy above stays instance-administrator
only and is not served in hosted mode. Runners report each distinct resolved
descriptor/provenance combination once, including successful matches that replace earlier mismatches.
Only the latest observations from currently authorized runners are actionable.
The MCP approve_project_policy tool accepts the same reference in input:
observed_policy_id is the chosen observed_policies[].policy.policy_id, and
expected_policy_id is the current policy.policy.policy_id (empty without an
approval). Omit input.policy when approving by reference. The Hub resolves and
validates the stored descriptor within the existing approval transaction; the
full-descriptor path remains available. Unknown request fields and invalid
field types are named in invalid-body errors.
Previously approved descriptors retain their history; they are reported as
configuration conflicts rather than newly generated policies.
Native descriptors include configuration, containing shared project behavior,
execution instructions and admission guidance. The existing approved policy
revision is the authoritative configuration delivered to every native runner.
Planning, validation, automatic promotion, review requirements, commands and
permission grants are shared. Credentials, host paths, capacity, backend routing,
active hours, isolation hooks and transport settings remain local. Native
configuration/source digests and revisions identify the portable behavior and
instructions rather than raw host-specific file bytes. Repository compatibility
policies retain their original file/revision approval semantics.
Native authored policy canonicalization version 3 omits local overlays whose
filtered configuration contains only the schema marker or no keys and whose
prompt is empty. Shared commands, permission grants and instructions still
contribute to identity. Keep host paths out of authored prompt prose; runtime
isolation context supplies the provisioned workspace and source checkout.
Templates can use {{ workspace.path }} and {{ workspace.source_root }}
without hashing the rendered host paths. Workspace roots resolve relative to
each runner's project workdir in global.yaml.
Upgrading to version 3 requires a one-time approval of the portable definition through the existing policy approval path. Version 1 and 2 snapshots remain verifiable and applicable; existing leases retain their pinned approval. All runners must use the same portable definition and canonicalization version before reporting one shared policy identity.
For repository-controlled native workflows, enrolled runners compare the loaded
definition with committed contents and verify ancestry against the configured
origin's advertised default-branch HEAD. The Hub automatically applies eligible
reports whose repository matches the project's checkout or integration binding.
Feature-branch commits, uncommitted definition edits and reports without this
provenance retain explicit approval through detent hub policy approve or the
existing policy endpoint. Authenticated native inspection consumes the same
approved shared definition as runtime; offline inspection describes the local
proposal. The approval command explicitly proposes the selected local
definition. The existing workflow refresh applies approved changes to idle
runners without synchronizing files or pausing the project. Active leases keep
their exact approved identity and prevent a replacement approval. No descriptor
from another ref is automatically approved.
Descriptors reject unknown JSON fields and validate all metadata, hashes and content-derived identities. Worker/operator tokens cannot approve or revoke a policy. Native cross-project and cross-organization reads follow existing grant checks. Native shared configuration is validated against its exact descriptor and gate metadata before storage or application. Host configuration and credentials are excluded from that payload. Conflicting runner reports without verified default-branch provenance do not change the shared definition.
Every workflow application records repository, commit, previous and new
definition_digest, runner_id, applying principal and timestamp atomically.
GET .../policy and MCP get_project_policy return newest-first history;
history_next supplies the next after cursor, with limit bounded to 1–200.
Exact retries add no history. Manual approvals without a runner observation or
Git revision retain empty values for unavailable provenance.
Claim allocation checks approval and requirements in the lease transaction and
persists the identity with the lease. Responses include policy_id; native run
events must match it. Missing/stale approval returns 409 policy_mismatch before
allocation. Unsupported or unmatched requirements return 409 selector_no_match.
Approval can replace the current policy while agents are running. Existing
leases retain their recorded policy revision; new claims require the current
approved policy. Revocation denies renewals and run events but permits release.
Legacy rows migrate without policy
pins and cannot be reused through the API as approved execution claims.
Start the Hub
Set a high-entropy bootstrap administrator token, then start the service on its loopback default:
export DETENT_HUB_ADMIN_TOKEN="$(openssl rand -hex 32)"
detent hub serve --database /var/lib/detent/hub.db
The bootstrap token is inserted only when the Hub database has no bootstrap credential. Later restarts do not replace a token that was rotated or revoked through the API.
The default listener is 127.0.0.1:7777. A non-loopback --listen value is rejected unless either --tls-cert and --tls-key are both set or --trusted-proxy explicitly declares that a trusted reverse proxy terminates TLS. The proxy or host firewall must prevent clients from bypassing that proxy.
Self-hosted instance administrators can call POST /api/v2/admin/backup with
{"output":"/private/backups/snapshot.db"} to create an online SQLite snapshot
on the Hub host. Success returns an empty 204 response. Output paths must be
new, files are private (0600), and response bodies never contain the path or
snapshot bytes. Project-scoped credentials cannot call it, and hosted Cloud
Hubs do not expose it. See backup and export
for snapshot consistency, offline maintenance and restore instructions.
Local board migration
An operator can import a read-only copy of a retired local_sqlite or
github_local store into an existing native project using
detent hub issue import-local. See the migration runbook
for dry-run, stable store identity, lane mapping, GitHub onboarding prerequisites,
and item-level transaction and replay behavior. The scoped command endpoint is
POST /api/v2/organizations/{organization}/projects/{project}/local-board-import.
Authentication
Send a bearer token with every control-plane and health request:
Authorization: Bearer <token>
Tokens have one of three scopes:
workerregisters and heartbeats machines, claims work, renews or releases leases, and appends fenced events.operatorreads state and performs typed workflow, dependency, priority, and queue-order mutations.admincan perform all operations and create, rotate, or revoke tokens.
Only SHA-256 token hashes and hash-derived fingerprints are stored. Plaintext is returned once by token creation or rotation responses, which carry Cache-Control: no-store. Token values are never included in Hub logs.
Create and rotate scoped tokens with:
POST /api/v1/tokens
POST /api/v1/tokens/{id}/rotate
DELETE /api/v1/tokens/{id}
Enrollment redemption accepts its separate one-time bearer token, which cannot call other APIs. /api/v1/webhooks/github uses GitHub's X-Hub-Signature-256 HMAC authentication and never accepts a Hub token as a substitute.
Platform AI credit adjustments
The Cloud entry accepts POST /api/cloud/platform/organizations/{organization}/ai-credits
from a platform entitlement administrator (admin or billing role) with a
current account session and its X-CSRF-Token, or from a bearer matching the
existing entitlement-administrator service token. Organization keys, including
organization-owner keys, cannot call this endpoint.
{
"idempotency_key": "complimentary-pilot-1",
"amount_usd": "10.00",
"reason": "Complimentary pilot credits"
}
amount_usd is a nonzero signed decimal string with at most six fractional
digits. Positive amounts grant complimentary credits; negative amounts correct
the balance and may leave it below zero. reason is required and bounded to
500 bytes. The idempotency key contains 1–128 letters, digits, underscores or
hyphens. AI credits must already be configured for the organization's active
billing mode.
The entry sends a signed service request to the tenant Hub's
/internal/v1/platform/ai-credits. The tenant applies the balance and ledger
row in one transaction on its open database. A successful response contains
organization_id and the current balance_micros (one USD equals 1,000,000
micros). Replaying the same key, amount, actor and trimmed reason returns the
current balance without applying another adjustment. Reusing the key with
different values returns 409 idempotency_conflict; invalid amounts or reasons
return 422 invalid_request.
Credit history and billing usage reports retain the complimentary kind,
signed amount, timestamp, actor and reason. Account sessions record the
administrator's email; the service token records entitlement-administrator.
Stripe purchases, auto-fund and chat usage retain their existing ledger kinds.
Organization-scoped runners enroll with scope: "organization" and an empty
project_ids list. Routing can switch between organization and projects;
omitting scope on existing project enrollments retains explicit project access.
The runner owner may change its allowed projects; an organization owner/admin
may also convert an existing project runner to organization scope. This routing
change preserves identity, enrollment, project rank overrides, checkouts and
provider logins, applies on the next refresh, and never releases active leases.
Luna's convert_runner_scope uses the same routing command with explicit admin
approval. Routing lists additional projects and links to their existing policy
review in project settings; conversion approves no repository policies.
Every claim still requires approved repository policy, supported isolation and
available capacity. Shared runners choose the organization project rank before
issue priority; project_ranks on a runner overrides the organization rank for
those projects. Each project's checkout and hooks.runner_setup remain separate.
Scoped runner onboarding
| Credential | Owner and lifetime | Revocation |
|---|---|---|
| Enrollment token | Hub issues a grant for one organization, either scope organization with no project list or scope projects with explicit projects, operations and, optionally, host-generated runner/machine IDs; valid for 1–900 seconds and one redemption |
Administrator deletes the unconsumed enrollment; expiry/revocation does not end an enrolled session |
| Runner credential | Customer host generates a random 256-bit bearer credential; Hub stores its SHA-256 hash; valid for 24 hours from enrollment or renewal | Administrator revokes the runner; no resurrection by renewal, rotation, generic token rotation or ID reuse |
| Provider/repository/storage credential | Customer login, keychain, workload identity or private host configuration; may outlive many runner sessions | Customer revokes it at its provider; revoking Hub access does not revoke this credential |
Each logical runner has its own credential and an administrator-approved machine binding.
runner_ and machine_ IDs are generated independently of hostname/display name.
Renaming preserves both IDs. Reinstallation requires new IDs, a new credential
and explicit enrollment; existing IDs remain reserved after revocation. Another
enrollment or a legacy registration cannot silently take over an existing ID.
Copying the private identity file copies bearer authority: never clone it into
machine images or share it across hosts. Multiple logical runners on one host
share the same machine ID and capacity ceiling through explicit enrollment
approval. Hardware attestation is not implemented.
Runner routing authority
Fleet reads include each runner's routing, revision, editability and, when editing is refused, the missing authority. Routing remains visible to organization viewers; work details remain restricted to readable projects.
Owners and admins can change any runner's routing. Members need manage_runner
on every currently allowed project and every project with an active lease on
that runner. Adding a project also requires its manage_runner grant. These
checks run again inside the mutation transaction. Unrelated projects do not
restrict runner routing edits, including taking-work state and capacity.
Organization-scoped runners can serve every organization project, so editing
them or switching to organization scope requires grants on every current
organization project.
An unassigned runner with no active leases requires an owner or admin.
Changing allowed projects additionally requires ownership of the runner.
Organization-wide enrollment, removal and machine routing retain their
all-project runner-management requirement. Scoped API keys must also cover
every affected project and allow writes.
Register a runner with one command
The Enroll dialog in the organization's runner settings asks for a display name and the projects the runner may work on, creates a token-first enrollment, and shows one command to run on the host:
detent hub runner register --url https://cloud.detent.build/organizations/org_example \
--token det_enroll_example --name "Build host" --capacity 2 --service
register does, in order:
- Generates the runner and machine IDs and a random credential on the host and
writes them to
identity.jsonbeside the runner configuration (default~/.config/detent-runner/, private, outside any repository). An existing identity there is reused, so a retry never creates a second runner. - Redeems the token. The request carries the IDs, host name, display name, capacity, version, OS and architecture, and the host credential, which the Hub stores only as a SHA-256 hash. No provider or repository credential is read or sent.
- Writes machine-only
global.yamlonce: theclient:block (hub_url,identity_file,organization_id,display_name,capacity),service_name, andworkspace_root(default~/detent-runner). Existing configuration files are left untouched. Cloud owns the project list; enrolled runners reject unknown keys at startup, naming the key paths. Remove localprojects, weights, priorities, scheduling, fair-share settings,global.agents.model_selection,client.native_projectsand other local project policy defaults before upgrading. Nested unknown machine keys also fail validation; the warning-and-ignore transition has ended. - With
--service, installs and starts a background service separate from a local board'sdetentservice on the same host. The default config path,~/.config/detent-runner/global.yaml, keepsdetent.runner(launchdcom.digitaldrywood.detent.runner, systemddetent.runner.service). New enrollments using other config paths append a stable hash of the absolute config path todetent.runner, even when installed first. Naming does not inspect installed services or depend on enrollment order. The name is stored inservice_name, so re-registration and updates target the same service. Existing config names remain unchanged. Use a separate config directory for each Hub enrollment; its credentials and service logs stay in that directory. At startup, the runner reads its current allowed projects from Cloud, clones missing linked repositories intoworkspace_root/PROJECT_IDusing host Git authentication, and loadsdetent.yamlandWORKFLOW.mdfrom the repository's current default branch. Existing matching checkouts underworkspace_rootkeep their directory and runtime project names so retained work can resume. Project setup failures retain Cloud grants and do not prevent valid projects from starting.
Native landing pushes use the runner host's GitHub CLI login for an HTTPS remote
when no worker token is handed off, or its SSH key for an SSH remote. Configure
that login with repository write access. worker.github_token controls agent
GitHub access; agents retain a private GitHub CLI configuration. An explicitly
handed off worker token also authenticates that worker's HTTPS Git operations.
detent doctor runs a named landing push credential check: a noninteractive
git push --dry-run that changes no remote refs. Runner startup readiness does
not contact the remote.
detent start and detent status default to the runner service when the
configuration has a client.hub_url, including older configurations without
service_name. An explicit service_name selects that service. If launchd
cannot bootstrap a disabled label, the error names it and prints the
launchctl enable command to run before retrying.
The token appears in the command because it is single-use and expires within 15
minutes; once redeemed it grants nothing. To keep it out of shell history,
omit --token and set DETENT_RUNNER_ENROLLMENT_TOKEN instead. A self-hosted
Hub whose URL does not include /organizations/ORG needs --organization.
Cloud-allowed projects use the Hub in place of the GitHub tracker their
committed detent.yaml names, so the checkout needs no local override.
register refuses an existing identity or configuration belonging to another
Hub or organization. Model selection comes from organization/project settings
in the Hub; local global.agents.model_selection is ignored.
Organization defaults are available on hosted and self-hosted Hubs through
GET/PUT /api/v2/organizations/{org}/model-selection and MCP
get_organization_model_selection / update_organization_model_selection.
Self-hosted organization operations require an organization-wide operator or
administrator bearer token; project-scoped credentials cannot access them.
Hosted organization operations retain their browser-session authorization.
Updates supply expected_revision (a JSON string), selection, and
idempotency_key for HTTP or request_id for MCP.
Project overrides use GET/PUT
/api/v2/organizations/{org}/projects/{prj}/model-selection or MCP
get_project_model_selection / update_project_model_selection.
Partial project settings override the organization default; a null project
selection restores inheritance. Runners receive the effective selection from
the Hub.
With no settings configured, every organization, including on a self-hosted
Hub, uses the seeded Codex selection: Sol (gpt-6.1-sol) at high for normal
and complex work, Astra (gpt-6-astra) at low for planning and medium for
validation, and Astra at medium for complexity:very-complex work. Per-issue
effort is clamped to each stage's configured ceiling. Configure these defaults
in the Hub, not in runner-local global.yaml or repository detent.yaml.
init and enroll below remain for scripted setups that bind the IDs before
the token exists.
An enrollment created without runner_id and machine_id is token-first: it
binds to the fresh IDs the host presents when it redeems the token, and the Hub
records them on the enrollment at that moment. The host still generates its IDs
and credential locally, the IDs must be unused, and the token can be redeemed
once. The steps below bind the IDs up front instead.
-
On the customer host, run
detent hub runner init --hub-url https://hub.example.com. It prints only runner/machine IDs and stores the credential under the OS user config directory atdetent/runner/identity.json. An explicit--identity-filemust be an absolute private path outside repositories and ordinary workspaces. Initialization refuses to overwrite an identity. -
Send those public IDs to an instance administrator. The administrator creates an enrollment using
POST /api/v2/organizations/{org}/runner-enrollments:{ "runner_id": "runner_0123456789abcdef0123456789abcdef", "machine_id": "machine_fedcba9876543210fedcba9876543210", "project_ids": ["prj_0123456789abcdef0123456789abcdef"], "operations": ["read", "claim", "heartbeat", "events", "collaborate"], "ttl_seconds": 300 }Use the actual IDs from initialization. The response contains
id,tokenandexpires_atwithCache-Control: no-store. Deliver the token through an approved private channel; never an issue, repository, shell argument or log. -
Supply it in
DETENT_RUNNER_ENROLLMENT_TOKENfor the enrollment command and rundetent hub runner enroll --organization org_example --display-name 'Build host'. Remove the enrollment variable from the host environment after the command.--enrollment-token-envselects another variable name. No provider credential is read or sent. The command sends its separate host-generated credential over HTTPS, and the Hub atomically consumes the enrollment, registers the machine, creates hashed authentication and installs the exact project grants. -
Configure the host scheduler using the private identity path:
client: hub_url: https://hub.example.com identity_file: /private/host-config/detent/runner/identity.json organization_id: org_example display_name: Build hostSet
workspace_rootto the checkout directory. Substitute the real private path and organization ID.identity_fileandtoken_envare mutually exclusive. An optionalmachine_idmust exactly match the enrolled ID. Cloud supplies the current allowed-project grants; no local mapping is needed. The host retains its existing local model login/API-key configuration.
| Operation grant | Permitted native operations |
|---|---|
read |
Capability negotiation and authorized project/issues/comments/history reads |
claim |
Claim, renew and release fenced work leases in authorized projects |
heartbeat |
Refresh only the authenticated machine's registration/heartbeat |
events |
Submit typed, fenced run events for this runner's own leases |
collaborate |
Create/edit issues and comments, transition workflow and edit dependencies within authorized projects |
Grants must explicitly list at least one project and operation. No empty list
means “all.” Enrolled runners cannot use v1, global health/outbox APIs, tenant
administration or token-management endpoints. Administrators may replace project
access through runner routing settings; operation grants require fresh enrollment.
Generic token grants cannot widen runner authority. Claims, lease mutations and
events check the individual runner's binding, organization, project and fencing,
including when another runner uses the same host. A supplied machine_id, tag,
capability or hostname never changes authority.
Named runners, host capacity and eligibility
Repository runners.profiles declare requirements; they do not register hosts,
assign tags or grant access. The approved policy descriptor pins the selected
profile and requirements to each lease. Runner and host names are descriptive;
selectors always use stable runner_ and machine_ IDs.
| Selector | Deterministic behavior |
|---|---|
| Required tags | Every required tag must be present in administrator-owned runner tags. Tags are trimmed, lowercased, sorted and deduplicated at configuration/edit boundaries; canonical tags are lowercase ASCII tokens, at most 64 characters, with at most 32 distinct tags. |
| Runner and machine IDs | Every supplied constraint must match exactly, together with all tags. Surrounding whitespace is trimmed in repository configuration; IDs are case-sensitive canonical lowercase tokens. Names never participate. |
| Empty selector | Any otherwise authorized and eligible executor may claim. Legacy registrations retain this compatibility behavior. |
| Unknown valid tag or ID | Work stays queued with selector_no_match; there is no fallback or selector widening. Invalid syntax is rejected during policy validation. |
| Renames | Administrator renames preserve stable IDs, grants, tags and active leases. Worker registration/heartbeat cannot overwrite approved names. |
| OS/architecture | Reported at enrollment, registration and heartbeat for operator visibility. Reports are capabilities, not grants or privileged routing tags. |
Selection, health and capacity are checked within the atomic Hub claim transaction.
An enrolled runner requires a heartbeat newer than two minutes; timestamps in the
future fail closed. All unreleased, unexpired leases on a machine count against its
shared host ceiling, including leases held by other runners and projects. Each
runner also has an administrator limit and a reported local capacity; the lower
value applies. Routing's optional project_capacity_limits maps authorized project
IDs to independent ceilings from 0 to 10000 on that runner. These ceilings do not
reserve capacity or grant access. Omission or null preserves overrides; an explicit
map replaces them, including {} to unset all overrides. Removing project access
requires explicitly removing its override in the same routing update; otherwise
the update is rejected. A project absent from the
map inherits the total capacity behavior. Changes to overrides require
idempotency_key through the routing API; update_runner_routing uses its request_id. Both share runner
revision conflicts and durable retry receipts. Lowering limits leaves active work
and renewals intact, and refuses new slots until usage is below every applicable
ceiling.
A slot is an unreleased lease whose expiry is strictly after the Hub time. Verified
native direct-landing members in the same machine/project/batch session group
share one host slot and one slot per participating runner and project. Other
leases count individually. Total, project and host limits are enforced together
inside the existing claim transaction. Enrolled claims and expired renewals share
the routing eligibility owner: project and total headroom form one runner capacity
budget, and shared-host capacity is checked there without a second machine brake.
Legacy claims retain the existing machine capacity owner. Capacity reads retain the existing total
desired, applied, effective and status evidence; their projects map adds
Hub-applied desired overrides (null means inherited), usage in slots, and effective
project ceilings clamped by the effective total. Unknown total evidence remains
unknown for positive project ceilings; an explicit zero is effective immediately.
The first enrollment initializes the host ceiling from its capacity; subsequent shared enrollment and worker reports cannot raise it. Existing local pool, project and backend admission limits still apply in addition to Hub limits.
To initialize another runner on the same actual host, choose a separate private destination and refer to the existing enrolled identity:
detent hub runner init --hub-url https://hub.example.com \
--identity-file /private/detent/release-runner.json \
--host-identity-file /private/detent/build-runner.json
This generates a new runner ID and credential, retaining only the machine ID.
The administrator must set shared_machine: true in the new enrollment request.
The host must already belong to that organization. Omitting explicit shared-host
approval preserves collision rejection; neither an enrollment token nor a new
credential can take over another runner's leases. Never copy a credential onto
another computer to simulate a shared host.
| Endpoint | Authority and behavior |
|---|---|
GET /api/v2/organizations/{org}/runners |
Instance administrator: fleet routing, host identity, platform, health, capacity and active-run eligibility. |
GET /api/v2/organizations/{org}/runners/{runner}/routing |
That enrolled runner or instance administrator: public routing and active leases; no secrets. |
GET /api/v2/organizations/{org}/runner-update/urgent |
Runner administrator: current urgent release request and revision (initially 0). |
PUT /api/v2/organizations/{org}/runner-update/urgent |
Runner administrator: expected_revision, published semantic version, confirm: true, and idempotency_key. Marks that release urgent for every older enrolled runner. |
PUT /api/v2/organizations/{org}/runners/{runner}/routing |
Instance administrator: expected_revision, display_name, tags, state, capacity_limit, project_ids. Replaces routing/access atomically; stale revisions return 409 revision_conflict. Empty project access denies every project. |
PUT /api/v2/organizations/{org}/machines/{machine}/routing |
Instance administrator: expected_revision, display_name, capacity. Capacity is shared by all bound runners. |
POST /api/v2/organizations/{org}/projects/{project}/leases/{lease}/validate |
Owning runner: fencing_token. Revalidates authority, selectors and pinned policy within one transaction. The customer scheduler checks the returned binding against its private local identity before adopting or renewing work. |
Runner availability uses timezone, string windows, optional window_slots,
and hard_deadline. window_slots maps an exact normalized window string to a
slot count from 0 to 10000; every key must name a window in windows. For example:
{"timezone":"America/Chicago","windows":["Mon-Sun 22:00-07:00","Mon-Sun 07:00-22:00"],"window_slots":{"Mon-Sun 22:00-07:00":6,"Mon-Sun 07:00-22:00":2},"hard_deadline":""}
Enrolled runners cap their reported capacity at the active window's count.
Uncounted windows retain their existing capacity, and capacity outside all windows
is zero. Runner, host and reported-capacity limits still apply, so a window count
can only lower capacity. Timezone and hard-deadline behavior are unchanged.
The routing API and update_runner_routing / list_runner_routing carry these
settings with the existing revision checks. machine_use is shared by default
and accepts shared or dedicated; omitting it in a routing update preserves
the current designation. It describes declared machine use and never detects
a person through input devices or console sessions. Routing without counts omits
window_slots; older runners ignore it and retain on/off availability.
The Fleet page links to runner list/detail and project eligibility. Project views, including Runs, link to the same eligibility details. Runner detail includes active run policy, selectors, lease expiry and contextual authority exclusions. The configured Hub administrator connection can edit routing; an enrolled worker connection sees its own runner without edit controls. Dashboard mutation authentication still applies.
An urgent release uses the existing draining state and enrolled update owner.
All older runners stop accepting claims; active leases continue renewing and
sessions finish before binary replacement and coordinated restart. The marker
persists across Hub restarts and covers offline runners when they reconnect and
older runners enrolled later. Matching post-start build evidence removes the
urgency-derived drain; administrator-owned draining or disabled settings remain.
The installed updater verifies the exact selected published artifact, signature
and provenance. Automatic update opt-outs do not refuse this explicit operator
request. An unsupported installation or failed verification retains the existing
refusal receipt rather than claiming update success. MCP exposes
get_urgent_runner_update and mark_urgent_runner_update; marking a release
uses the existing material-action approval and all-project runner authority.
| Change or exclusion | New dispatches | Active leases |
|---|---|---|
| Draining | runner_draining; queued |
May validate, renew and finish. |
| Disabled | runner_disabled; queued |
Validation, renewal and new Hub mutations are denied. |
| Required tag removed | selector_no_match; queued |
Affected validation, renewal and run events are denied. Unrelated tag edits preserve eligibility. |
| Project access or credential revoked | No authority to dispatch | Further affected operations are denied; ownership never transfers to another runner on the host. |
| Capacity reduced/full | runner_capacity or host_capacity; queued |
Existing leases retain occupancy until release/expiry; no new lease exceeds the limit. |
| Offline exact target | runner_offline or incompatible-caller selector_no_match; queued |
Existing fencing and expiry remain authoritative. |
Routing refusals do not create work attempts, consume failure retries or change the issue to Failed. The scheduler preserves their structured code as a scheduling deferral. Revocation is enforced on Hub operations immediately and on local execution at the next pre-start/renewal check. Work already executing on a disconnected host cannot be remotely undone; loss of renewal authority cancels the local attempt. Routing state, project access, host capacity and individual lease ownership survive Hub restart. The schema migration preserves existing identities and lease history.
Runner renewal, rotation and revocation
| Endpoint | Authority | Behavior |
|---|---|---|
POST /api/v2/organizations/{org}/runner-enrollments/redeem |
Enrollment bearer | Strict host identity/credential/machine metadata body; one transactional winner; replay, expired/revoked grant and wrong binding return 401; collisions return 409 |
DELETE /api/v2/organizations/{org}/runner-enrollments/{id} |
Instance admin | Revoke an unconsumed grant |
GET /api/v2/organizations/{org}/runners/{runner} |
That runner | Read its public identity, grants and expiry; no credential value |
POST /api/v2/organizations/{org}/runners/{runner}/renew |
That active runner | Body {}; retain credential and extend expiry to 24 hours from Hub time |
POST /api/v2/organizations/{org}/runners/{runner}/rotate |
That active runner | Body {"credential":"<new host-generated credential>"}; replace the hash and extend expiry atomically; previous credential stops working immediately |
DELETE /api/v2/organizations/{org}/runners/{runner} |
Instance admin | Remove the runner from fleet, routing and eligibility; revoke all future authenticated access. Refuses active work with 422; repeat removal returns 204. |
POST /api/v2/organizations/{org}/projects/{project}/machines/{machine}/heartbeat |
Owning runner with heartbeat |
display_name, capacity, version, optional os/architecture; enrolled runners update reported capacity/platform and liveness, preserving administrator-owned names and limits |
Hubs advertising native_heartbeat_changes accept an optional change_cursor
on enrolled runner heartbeats. Send an empty string for the initial snapshot.
The routing response adds changes: an opaque cursor, reset, changed
items (work_item_id, string revision, last_activity_at), the project's
approved policy_id, capabilities_digest, and a claimable queue hint.
The existing routing response retains its runner update target and claim state.
The cursor uses the board's project activity sequence and an observation time
for activity recorded outside collaboration history. Invalid, expired, reset,
or oversized deltas return reset: true; the runner performs a full project
refresh before acknowledging the returned cursor.
Runners retain the project snapshot and hydrate changed items before advancing their acknowledged cursor. Policy and capabilities reads are reused until their heartbeat identifiers change. Claims run only with the queue hint or after a slot frees, and the Hub continues to enforce policy, capacity, ordering and fencing when claiming. The heartbeat feed replaces list refreshes and dispatch long polling. Missing heartbeat fields retain the previous polling behavior for compatibility with older Hubs.
Removal uses the existing credential revocation transaction. Active leases and
unfinished attempts prevent removal; drain the runner and let its work finish
first. Identity and machine records remain available to historical reads. The
hosted fleet response excludes removed identities from runners and retains
their display names and hostnames in runner_names for historical run labels.
GET /fleet?include=names uses the same organization member authority and
returns only runner_names, including active and removed identities. Name
refreshes use this projection without reading Fleet quotas, routing or leases;
the ordinary GET /fleet response retains its full usage contract.
GET /fleet?include=host_metrics&runner_id={runner} reads one active runner's
hourly host history for organization members whose credential can read every
active project in the organization. Project-restricted credentials receive
404 because machine-wide metrics cannot be scoped to visible project activity.
Removed and other-organization runners also receive 404. Optional from and
to RFC3339 timestamps select an inclusive start and exclusive end, with a
positive range of at most 30 days and at most 720 rows. The default is the last seven days. Expired hours are omitted.
Responses include metric counts, sums, extrema and averages derived from each
metric's count; unavailable averages are omitted.
GET /api/v2/organizations/{organization}/runners/{runner}/capacity and
get_runner_capacity add an advisory recommendation to the existing capacity
read. Capacity fields retain their meaning. Recommendations never change routing,
capacity, slots or dispatch. Shared runners return person_hours and free_hours
periods; dedicated runners return one dedicated period. Lower-slot availability
windows declare person hours. Without those windows, person hours are 08:00–22:00
in the runner timezone (UTC when unset). A host hour overlapping person hours
uses the person reserve; run peaks are attributed by run start time.
Each period requires 24 covered hours and 10 completed runs with peak memory
and CPU/wall-time evidence. Covered hours use the minimum memory, CPU and disk
sample count divided by 120, capped at one per hour. Sparse periods return
insufficient_evidence and evidence counts without a slot range. The read uses
retained completed host hours and up to 10,000 recent completed runs; partial
reports run truncation. Running and infrastructure-interrupted attempts are excluded.
The memory ceiling is floor((RAM minus reserve) / p90 run peak), capped by cores with a p90 CPU requirement of at least one core per slot. Dedicated and free hours reserve max(2 GiB, 10% RAM) and one core; person hours reserve max(8 GiB, 25% RAM) and two cores. Disk reserves max(20 GiB, 15% total); insufficient free disk yields zero slots. When observed disk growth correlates with lease occupancy, its p90 bytes per slot additionally caps slots. Growth is measured between consecutive sampled hours with unchanged disk capacity; idle growth and gaps do not become per-slot usage. The disk cap accounts for each hour's estimated background usage. Otherwise there is no measured per-slot disk estimate. GPU is not reserved.
The range reflects the least and most headroom observed after estimating
background memory and CPU from host metrics and lease-derived slot hours,
within the resource ceilings. Any warn/critical macOS pressure, hourly mean
Linux PSI memory some avg10 above 10%, or swap maximum growth of at least
1 GiB between consecutive host hours with swap samples lowers both bounds by
at least one. A pressure hour with at least one lease-derived slot-hour also
caps both bounds at ceil(slot-hours) minus one. Idle or fractional occupancy
below one slot-hour does not impose that cap. Unchanged or declining swap
allocation alone does not count as pressure or add to over_capacity_hours.
Available pressure evidence counts even when other resource samples are missing;
those hours do not add resource coverage. Swap maxima show sampled allocation
growth, not swap activity; PSI summaries cannot identify individual sample spikes.
The evidence includes coverage, run counts, p90 memory and CPU, reserves,
measured disk bytes per slot, slot hours and over-capacity hour counts.
The result remains advice based on retained telemetry, including a zero range
when observed pressure leaves no safe slot estimate.
recommendation.suggested_limit turns the same hours into one advisory "Jobs
at once" value: limit, current, direction (lower, raise, hold or
insufficient_evidence), hours_covered, saturated_hours and up to three
reasons. An hour counts when it has memory and CPU samples, and is busy when
slots were in use. Shared runners leave person hours, as split for the periods
above, out of coverage and saturation because the person's own work loads them;
those hours still feed the low-disk reason and the adjacent-hour CPU and swap
checks. A busy hour is saturated when memory pressure warned or was
critical, PSI some avg10 averaged above 10, swap grew by at least 1 GiB over a
swap-sampled previous hour, or CPU busy averaged 90% or more in it and an
adjacent hour. Load average is not used. Fewer than 24 counted hours returns
insufficient_evidence without a limit. When more than 5% of busy hours are
saturated the limit lowers to the highest of the two levels below current
whose hours (rounded slot use, at least three) stayed at or under 5% saturated,
otherwise to current minus one, never below one. When saturation stays at or
under 5% and every slot was busy (slot use at least 90% of current) in at
least a quarter of the counted hours, it raises by one, up to 10,000. Otherwise
it holds. Free disk under 10% in any hour adds a reason dated in the runner
timezone and never changes the limit. The value is stateless and never changes
routing, capacity or dispatch.
Native Hubs advertise runner_host_metrics before runners include optional
host_metrics in a heartbeat. A batch contains at most 24 summaries of at most
1 KiB each. host_metrics_acknowledged identifies processed hours and restart
segments in the heartbeat response. Each runner/hour stores one aggregate with
at most 128 segment IDs; repeats do not write or change the aggregate. Once the
segment limit is reached, additional segments are dropped, logged and
acknowledged. Malformed or oversized summaries are dropped and logged without
rejecting the heartbeat. Expired retries are acknowledged without storage, and
existing native retention maintenance removes hours outside the 720-hour UTC
window. Shutdown partials use ordinary heartbeat availability and lease rules.
The existing revoke_runner_identity MCP operation performs the same removal.
Hub time decides validity, with an inclusive start and exclusive expiry boundary.
An invalid clock or time before issuance fails closed. Times are parsed before
comparison, including fractional-second boundaries. There is no grace period or
offline renewal of expired credentials: generate and enroll a new host identity.
Clients renew automatically before requests when fewer than 12 hours remain.
detent hub runner renew forces renewal; detent hub runner rotate generates
and persists a replacement before sending it to the Hub. Credential-management
operations cannot grant additional projects or operations.
The host file includes a pending credential during rotation. A restart first tries that credential, then retries the same rotation with the old credential only if the pending one is unauthorized. A lost enrollment response is recovered by authenticating the already-persisted credential, without redeeming again. File locks serialize local credential maintenance; atomic private-file replacement preserves recovery state. Identity responses, audit records and CLI output never contain runner credentials. Identity audit rows record enrollment, renewal, rotation and revocation using stable actor IDs and Hub timestamps.
Every request checks expiry and revocation against the owner database; there is no authentication cache. An already authenticated in-flight operation may finish. The enrolled scheduler treats subsequent authentication/authorization failure during lease maintenance as lost ownership, invoking its existing stop path. Previously granted leases remain fenced and expire through the existing lease recovery mechanism; revocation never transfers a running lease to a new host. It cannot forcibly erase credentials or stop an offline or hostile customer machine. Stop that host and revoke provider/repository access separately if needed.
Customer credential isolation and cleanup
Keep runner identity in the service account's private config directory (0700) with a regular 0600 credential file. On Windows, provision the equivalent private service-account/profile ACLs; POSIX permission bits do not enforce Windows ACLs. Do not expose the runner identity file, enrollment variable, Hub admin token, provider login directory or storage configuration through repository files, ordinary workspaces, shared caches, artifacts or verbose shell tracing. Back up private identity only into customer-controlled secret storage.
Use existing provider login or API-key facilities on the customer host. A trusted backend wrapper should select only the required provider environment variables or private credential mounts for the chosen backend. Run untrusted jobs under a separate OS account/container/VM with a restricted filesystem and environment; mount only that job's credential facilities. Environment variables and file mode bits do not protect secrets from code running with the same identity and execution context. Keep Hub runner authority in the control process's context, separate from the job. Neither the Hub nor these enrollment commands fetch, relay, store or validate provider/storage credentials.
Use operator-owned wrappers and the existing hooks as this cleanup contract:
hooks.before_run: fail before execution if the approved sandbox, private credential mounts and environment allowlist cannot be established. Hook child process exports do not change the worker environment; perform injection in the trusted backend launcher. Never echo credential values.hooks.after_run: stop job processes, unmount credential facilities, remove private job scratch and discard job-only capabilities. Make cleanup idempotent.hooks.before_remove: verify teardown before ordinary workspace removal. Both cleanup hooks are best effort and log failures; they are not security boundaries or guaranteed crash cleanup. An operator-owned host janitor must reconcile orphaned sandboxes after worker/host failure before reuse.
Raw prompts, credential fields and artifact content are rejected by native event schemas. Explicit user-authored comments remain content and must not contain secrets. Encrypted provider-key provisioning through Hub is a future design option requiring authenticated recipient keys and a browser/control-plane trust model; no such relay is implemented.
Legacy token migration
Migration 9 adds nullable expiry and separate enrollment/identity tables. Existing worker/operator/admin tokens retain their scopes, hashes, grants and non-expiring behavior; existing machine IDs and leases are not rewritten. A Hub restart or upgrade does not disconnect installations. Generic token creation, rotation and revocation still apply to legacy tokens; enrolled runners use the dedicated endpoints above. Legacy shared tokens retain their previous trust boundary and should only be used by mutually trusted clients.
Migrate deliberately: initialize and enroll a new identity, stop legacy claims,
allow existing leases to finish/release, switch native project configuration to
identity_file, and verify claims/heartbeats with the new IDs. Then revoke the
legacy token after every host sharing it has migrated. During overlap the old
token remains authorized under its old scope. Never convert a shared token in
place or reuse a legacy machine ID to transfer trust. Compatibility v1 projects
can continue using their legacy configuration until their native cutover.
Work and fleet endpoints
| Endpoint | Minimum scope | Purpose |
|---|---|---|
GET /health |
any scoped token | Service, schema, repository, and outbox health |
GET /api/v1/work-items |
any scoped token | Filtered, sorted work-item page |
GET /api/v1/work-items/{id} |
any scoped token | Normalized item, graph, PRs, lease, workpad, and event timeline |
POST /api/v1/claims |
worker | Atomically claim a requested item or the next compatible item |
POST /api/v1/leases/{id}/renew |
worker | Renew with the exact fencing token |
POST /api/v1/leases/{id}/release |
worker | Release with the exact fencing token |
POST /api/v1/work-items/{id}/events |
worker | Append a session event with the exact fencing token |
POST /api/v1/machines/register |
worker | Register or refresh a machine |
POST /api/v1/machines/{id}/heartbeat |
worker | Update heartbeat, capacity, version, or capabilities |
POST /api/v1/work-items/{id}/workflow |
operator | Change workflow state and enqueue its managed GitHub label |
POST /api/v1/work-items/{id}/dependencies |
operator | Add or remove a Hub-authoritative dependency |
POST /api/v1/work-items/{id}/priority |
operator | Change priority and enqueue its managed GitHub label |
POST /api/v1/work-items/{id}/order |
operator | Change Hub-authoritative queue rank |
GET /api/v1/repositories/freshness |
any scoped token | Repository synchronization health page |
GET /api/v1/outbox/health |
any scoped token | Outbox counts and operator-action page |
Worker events and operator mutations expose typed state changes only. They do not provide a generic GitHub request or arbitrary mutation surface.
The legacy event upload accepts kind: progress with an optional typed payload.step: plan, implement, test, review, or complete. Unknown payload fields and event kinds are rejected. Existing locally recorded v1 history remains readable; it is not copied into native event payloads. New integrations should use the versioned v2 schemas below.
Work-item queries and cursors
GET /api/v1/work-items accepts repeatable or comma-separated filters: repository, workflow_state, readiness, priority, label, assignee, machine, lease, pr, and sync_health.
Supported sort values are priority, created, updated, identifier, and workflow_state; order is asc or desc. The default priority order uses priority, queue-rank presence, queue rank, creation time, repository owner/name, issue number, and internal ID. Every other sort also ends with repository owner/name, issue number, and internal ID, giving every page a stable total order.
List responses contain an opaque next_cursor. Pass it back with the same sort and order. limit defaults to 50 and may not exceed 200. Repository freshness and outbox health use the same limit and opaque cursor convention. Work-item detail timelines use timeline_limit and timeline_cursor.
Omit work_item_id from POST /api/v1/claims to claim next. Candidate selection and lease creation execute in one SQLite transaction; clients must not list candidates and then attempt a separate specific claim. Renew, release, and event requests must carry the positive fencing_token returned by the claim.
Native organization and project setup
Each database receives a stable hub_ identity and a local org_ organization.
Local operation requires no hosted account. Native projects, issues and comments
use opaque prj_, wi_ and cmt_ IDs. IDs contain 128 random bits and survive
restart and owner backups. Project issue numbers are display values; APIs and
workers use immutable IDs. External references are optional.
An instance administrator can list organizations with GET /api/v2/organizations,
create another organization with POST /api/v2/organizations and {"name":"Team"},
and create a project with
POST /api/v2/organizations/{organization_id}/projects:
{
"idempotency_key": "create-project-example",
"name": "Example project",
"require_dependencies": true,
"states": [
{"name":"Todo","dispatchable":true,"terminal":false,"transitions":["In Progress"]},
{"name":"In Progress","dispatchable":true,"terminal":false,"transitions":["Review"]},
{"name":"Review","dispatchable":false,"terminal":false,"operator_only":true,"transitions":["Done","Todo"]},
{"name":"Done","dispatchable":false,"terminal":true,"transitions":[]}
]
}
Project state names and transitions are explicit; there is no prescribed workflow.
A project created through the hosted client starts from a fixed template instead:
Todo and In Progress (dispatchable), Human Review (neither dispatchable nor
terminal, and not operator_only, so the orchestrator can move a completed run's
Change Request there), Merging (dispatchable: an accepted Change Request waits
there for the runner that lands it) and Done (terminal). In Progress may move
to Todo, Human Review, Merging or Done; Human Review may move to Done,
Merging or back to In Progress; Merging may move to Done, back to
Human Review, or back to In Progress. A completed run whose published
version the review policy already accepts moves from In Progress straight to
Merging; any other committed change waits in the review lane.
Hub migrations 36, 38 and 39 move projects whose workflow is exactly an earlier
template onto it and leave customized workflows unchanged. A native project's Change Requests wait in the lane named by the
runner's auto_promote.source_state (default Human Review), so a customized
workflow needs that lane, non-operator_only and reachable from its active
lanes, for completed runs to reach review.
operator_only prevents workers from creating an issue in, or transitioning to,
that state. require_dependencies defaults to true. Setting it false disables
dependency readiness gating for that project while retaining scope and cycle
validation. A resolved dependency has a terminal workflow state. This setting
does not bypass lease ownership or machine capacity.
Create a worker or operator token with the existing administrator token endpoint,
then grant it project access with POST /api/v2/tokens/{token_id}/grants:
{"organization_id":"org_example","project_id":"prj_example"}
Use the actual returned IDs. The grant operation is idempotent and converts the
token to native-only access. Such tokens cannot use v1 or instance administration,
including when their stored role is admin. Token rotation preserves grants;
revocation prevents subsequent authenticated requests. The bootstrap administrator
cannot be converted to a project token. Instance administration is deliberately
separate from tenant access and hosted human membership.
Enrolled runners use the scoped host identities described above.
Every native project route requires both its organization and project grant. Unknown, guessed and inaccessible resource IDs return 404. Native machine IDs are bound to their organization and token principal; another token cannot take over registration or renew its leases. A legacy registration cannot overwrite a native machine. Native tokens cannot read legacy global lists through v1.
Native collaboration protocol
GET /api/v2/capabilities reports server identity, the Hub's running version,
supported protocol majors, event schemas, required native features, request size
and page limits. Native
clients negotiate major 2 and schema 1. A native claim supplies protocol_major: 2
and capabilities: ["native_issues", "scoped_collaboration"]. Incompatible
negotiation fails without switching tracker or scheduler.
Runner issue detail and recovery reads with claim permission include optional
scheduling_eligible. It combines the current lane, archive status and the Hub's
authoritative dependency predicate, including dependencies outside the runner's
read grants. Dependency details and public dependency_ready retain their normal
visibility filtering. A false value refuses recovery or renewal; an absent value
uses the existing project workflow and visible dependency projection for older
Hubs. The field also advertises support for cancel_attempt: true on a lease
release with reason cancelled. Recovery uses this only after rejecting a
retained execution: the Hub releases the lease and records attempt cancellation
in the same transaction. Ordinary releases leave attempt settlement with the
execution controller so live worker cleanup and source publication retain their
existing ordering.
A person-filed native issue that enters Done gets verification: {"state": "pending"}, and its filer's issue email asks them to verify it. Worker, service-key, imported and origin-stamped issues never enter pending. The filer, or a member and above, answers with POST /work-items/{id}/verification or MCP verify_fix; a Reporter may answer only on issues they filed, which is a Reporter's only path out of Done. verified records verified_by and verified_at and keeps the issue in Done. not_fixed adds the required comment and moves the issue from Done to Todo with reason user_requested, so quality analytics count the reopen exactly as a manual one. Leaving Done clears the state. GET /work-items accepts verification=pending or verified. Verification never dispatches, blocks or lands work.
Native issue type is required when a Reporter files and optional for other roles, including workers and machine reports. Untyped issues omit the field in reads. Members may edit the type. Reporters may edit only the title, body and attachment references on their own issues while in Triage; labels, priority, assignees, state and type remain refused. Once an issue leaves Triage, the response explains that it is already with the team and a comment can add details. Reporter edits do not confirm engineering issue-contract sections. Type edits appear in issue history, and labels remain independent of the type.
Reporter REST, SSE and MCP reads are restricted to issues whose actor principal matches the caller's human principal, including personal keys and OAuth clients. Lists, search, exports, board counts and board deltas use the same ownership boundary. Other filers' issues return not found; dependencies, related links and attachment references to those issues are omitted. Reporter issue resources include reporter_status: Triage is submitted; Backlog and Todo are planned; In Progress, In Review, Rework, Merging and Blocked are in_progress; Done is done; Cancelled is cancelled. Cancelling an own issue still uses move_item to Cancelled. Filing, comments and verify_fix retain their reporter contracts.
The following paths are relative to
/api/v2/organizations/{organization_id}/projects/{project_id}. Worker and
operator tokens can read and author collaboration; claims, machines and run
events require worker scope. Instance administrators can perform these operations.
| Method and path | Input or result |
|---|---|
GET / |
Project profile, states, transitions and readiness policy |
POST /work-items |
idempotency_key, title, full body, configured state, optional type (bug or change_request), report_context (url, app_build, browser, os, viewport, captured_at; bounded single-line strings, unknown keys refused), priority, labels, assignees, import provenance |
POST /work-items (scheduled reporter evidence) |
idempotency_key, scheduled_validation with repository, run_id, run_attempt, canonical run_url, pinned head_sha, and conclusion: success, failure, or skipped; no issue fields |
GET /work-items |
Paged issues; optional exact type (bug or change_request); repeatable exact state, label, assignee, priority filters (OR within a dimension, AND across dimensions; at most 32 values and 4096 bytes per dimension). q matches title, project name or ID plus #number, and labels; without include=work, it also matches body. include=work adds compact body-free items, bounded current open selection, and complete-filter lane/running totals independent of cursor. include=board reads indexed lane pages with attempt/change card summaries and maintained scope totals; include=summary provides the byte-bounded MCP list projection with explicit detail omissions and continuation; it cannot be combined with work |
GET /work-items/export |
Full stored work items and comments; repeatable state and label filters, open, archived, signed cursor and limit (default/max 500). OR within state/label, AND across dimensions, default excludes archived items. A 16 MiB page budget preserves full item content; total, truncated, next_cursor, generated_at and freshness report continuation and observation. Uses existing read scope and project grants; never refreshes linked sources |
GET /work-items/{id} |
Full content, immutable scope, revision, authenticated author, import provenance, dependencies and optional external references |
PATCH /work-items/{id} |
idempotency_key, expected_revision, supplied type, title, body, priority, labels or assignees |
POST /work-items/{id}/workflow |
idempotency_key, expected_revision, target state, typed reason |
POST /work-items/{id}/verification |
idempotency_key, optional expected_revision, result (verified or not_fixed), and comment, required for not_fixed |
POST /work-items/{id}/dependencies |
idempotency_key, expected_revision, related_work_item_id, operation: add or remove |
POST /work-items/{id}/dependency-reasons |
Source issue lease/fencing and idempotency_key; target work_item_id in the same project, kind: blocked or unblocks, text, current edge-set basis, and expected_revision of that reason |
POST /work-items/{id}/comments |
idempotency_key, explicit body, optional import provenance |
GET /work-items/{id}/comments |
Paged discussion with authorship, provenance, revisions, editor and timestamps |
PATCH /work-items/{id}/comments/{comment_id} |
idempotency_key, expected_revision, explicit body; deleted: true soft-deletes the comment |
GET /work-items/{id}/history |
Paged typed collaboration and run events |
GET /work-items/{id}/versions/{revision} |
Immutable issue content snapshot |
GET /work-items/{id}/comments/{comment_id}/versions/{revision} |
Immutable comment content snapshot |
POST /machines/register |
id, hostname, display_name, capacity, version; registration also refreshes heartbeat |
POST /claims |
Approved policy_id, machine_id, unique session_id, ttl_seconds, protocol negotiation, optional work_item_id, workflow and author/assignee/label filters |
POST /leases/{lease_id}/renew |
fencing_token, ttl_seconds |
POST /leases/{lease_id}/release |
fencing_token, typed reason; optional cancel_attempt for rejected recovery after the Hub advertises scheduling_eligible |
POST /work-items/{id}/events |
Idempotent, versioned run fact with current lease and typed references |
Dependency summaries include reader-scoped blocker and dependent numbers, titles,
links, workflow state and live attempt start times. Reasons are display-only
plain text, capped at 140 characters, with separate typed basis and revision
fields. The basis hashes the sorted directional edge set. A reason write changes
one issue and refuses an outdated basis or revision with 409 revision_conflict.
Reads omit stale text; cross-project edges suppress the corresponding sentence
for all readers. Runners refresh stale reasons on outcome reporting and dependency
mutations, including both endpoints, without changing lanes or eligibility.
With project duplicate_challenge enabled (the default), native filing may
return HTTP 409 with code: possible_duplicates, up to five
possible_duplicates, and a challenge_token, creating no issue. MCP
file_issue returns those challenge fields as a structured result. The agent
reviews the returned candidates, using supported reads such as work_item and
work_comments when needed, and explicitly decides whether they concern the
same problem. When filing is already authorized and the evidence clearly
establishes a distinct problem, proceed without another human question: repeat
file_issue with the same request_id and unchanged content, the
challenge_token, and not_duplicate_of containing the challenged issue
numbers. The new issue links those matches as related. For an existing problem,
use the existing issue and comment on its returned work_item_id within the
caller's authorization instead of creating a duplicate. Ask the human only
when overlap is genuinely ambiguous, a product decision is needed, or the
proposed action exceeds authorization; never automatically dismiss candidates.
This decision does not grant filing permission or confirm native issue-contract
sections on behalf of a human.
Tokens expire after one hour; expired filings need a new request identity.
Confirmation deletes the token, while request replay retains its original
result. Machine reports and linked GitHub intake are never challenged.
Scheduled reporter evidence requires operator or administrator authority and
the project's selected checkout repository. This report-only variant returns
the report and the Hub's recorded_at receipt time without creating or changing
an issue. A repository/run/attempt is recorded once per project; retries retain
the original receipt time and conflicting reports are refused. Failure issue
filing and deduplication remain separate writes through the same owner.
Skipped reports record scheduled activity when the pinned commit was already
attempted. They refresh reporting cadence without supplying green validation
evidence or changing the last green time.
The existing dead_man flow finding reports delivery stalled when landings stop
despite candidates and a free slot, or scheduled validation goes silent or
stays red. Its evidence counts identify landing_stalled, scheduled_silent
and scheduled_red. Silence uses twice the median of up to twenty recent
positive report intervals, with a six-hour minimum. Red requires repeated
failures still reporting, with no green report for twenty-four hours; without
a prior green, the first report starts that window. A green report clears the
scheduled signal on the next health evaluation. Projects without any scheduled
report history are excluded. Existing health reads, needs-attention surfaces
and email delivery carry this flow finding; it creates no tracker issue.
The dependency_hold flow finding reports delivery stalled when no candidate is
claimable, a runner slot is free, nothing landed for thirty minutes, and, in
a project that requires dependencies, dispatchable items have waited at least
thirty minutes on dependencies held in non-dispatchable, non-terminal lanes. Its evidence counts dependency_gated, held_blockers and
free_slots, and work_item_ids lists the held blockers.
Rate signals listed in baseline_unavailable lack a comparable history, but
absolute floors still report clear stalls with evidence reason
absolute_floor: retry_storm at five same-signature failures in an hour (or
twelve regardless of history), stalled_active_item after two hours without
an attempt heartbeat, and throughput_collapse when nothing landed for six
hours while Todo or active items have waited at least that long. Runner
transitions that record permission wait or lifetime limit in their reason
detail produce unanswered_human_wait and lifetime_limit_hit.
Every work-item representation returned by create, get, list and update includes
web_url. Hosted Hubs build this absolute link from the configured
hosted.public_url, never from request Host or forwarding headers. Hosted
browser links use
{public_url}/{organization_slug}/{project_slug}/issues/{number}. The project
board uses the same base without /issues/{number}, and changes append
/changes/{change_id}. Earlier organization and project slugs, work-item ID
URLs and legacy issue routes redirect to the current readable address. Slugs
are generated from names, unique within their scope, and editable by owners
and admins in settings; earlier slugs remain reserved. The same links are
returned by MCP, which accepts readable issue URLs as scoped issue references.
Non-hosted Hubs have no configured public URL
or conversation app and return an empty string. Replayed mutations and historical
work-item versions project the link using the serving Hub's current configuration;
the URL is not immutable content.
Collaboration mutations return the committed representation with status 200,
including identical retries. Idempotency keys are scoped to the authenticated
principal, organization and operation/resource. Reusing a key with different
content returns 409 idempotency_conflict. REST mutation receipts outside
conversation routes are retained for 24 hours; a retry with the same key after
that is processed as a new request. Concurrent edits return 409
revision_conflict with current_revision. Revisions, event sequences and fencing
tokens are decimal strings on the wire. Mutations, their saved content versions,
history and retry result commit in one SQLite transaction. Ordinary clients cannot
update or delete history.
Titles are limited to 500 bytes, bodies to 256 KiB, comments to 64 KiB, and requests
to 1 MiB. There are at most 100 labels and 100 assignees, each at most 200 bytes;
priority ranges from 0 (urgent) to 3 (low). Empty label/assignee arrays clear them.
Unknown request fields and unsupported query parameters fail validation.
Workflow reasons are user_requested, worker_progress, validator_rework and
dependency_ready.
Workflow mutations may include reason_detail, bounded to 8 KiB of UTF-8 text;
the Hub preserves it in workflow.transitioned history. Orchestrator lane writes
forward the lane ledger's reason as readable text through this field.
Release reasons are completed, cancelled, failed, released,
work_item_hydration_failed and work_item_identity_missing.
Ordinary native work-item lists accept q (query on MCP work_list) for ranked
full-text search of titles, bodies and comments. Search terms use stemming and
prefix matching and combine with OR. Documents matching more distinct query
terms rank first, followed by BM25 relevance and the ordinary
list order. Legacy substring matches of titles, bodies, labels and identifiers
remain included. Each search hit includes a short snippet; full-text matches are marked
by brackets. Search composes with the existing exact filters.
created_after and updated_after are inclusive RFC3339 timestamp bounds;
created_before is exclusive. Combine created bounds to select one day. Offsets
are supported and compared as instants. These selectors are available on REST
and native MCP lists; snapshot-backed lists and the board projection reject them.
Native issue lists accept created_by with a principal ID or me, alongside
state, label, query and archived filters. me resolves to the authenticated
human principal, including the owner of a personal key; service keys and worker
credentials receive invalid_request. The filter uses the native filing actor,
including for imported issues, and preserves project access checks. Issue reads
include the filer's available display identity in actor.display_name.
Native pages use limit (1–200, default 50) and an opaque cursor. Issue lists
group items in configured workflow state order. Within nonterminal states,
priority sorts first, then last_activity_at newest first; terminal states use
activity alone. Missing priorities and activity sort last, and identifiers break
ties. The first page retains matching item identities and SQL sort keys for the
cursor's lifetime, including relevance scores and matched snippets for searches,
so concurrent changes to state, priority, activity or filters
cannot skip or repeat those items. New items appear in a fresh query. Each page
loads current item details, so this is not a transactional export snapshot;
a consistent export still requires a quiescent owner backup.
MCP work_list accepts include: ["summary"] for a compact lane scan. Each
item contains exactly identifier, number, state, priority, title,
type, labels, last_activity_at, revision, and latest_transition_reason.
The reason is the latest workflow transition's reason_detail, falling back
to its reason, or an empty string if no transition was recorded. Related
items, contract hashes, dependency reason bases, actors and search snippets
are omitted. The response retains project identity, freshness, total count and
next_cursor; continue with the same filters and include. Default MCP and
REST responses, including the REST body-free include=summary projection,
are unchanged. The existing encoded byte budget still bounds pages; long
titles, labels, identifiers and reasons can make a page larger than 8 KiB or
require continuation before the requested limit.
MCP explain_item also accepts include: ["summary"]. It keeps schema,
found, observed_at, identity, current_lane, the latest transition's
from/to/time and reason/reason_detail, current and latest eligibility
outcomes and reasons, and required gate state, source state and reason.
Available attempt details retain identity, selection, freshness, runner,
role, start time and status; historical attempts retain their selection.
failure_signature is included when available. Capacity reports, native
runtime detail (including current_version.validation.output), coverage notes,
sessions and full evidence are omitted. Missing optional evidence stays
absent. The default explanation and work_item recovery view are unchanged.
Both compact projections use the existing authorized reads and expose no
additional credential or cross-project data.
Comment creation sequences and aggregate history sequences provide increasing page order. Signed cursors bind protocol, principal, organization, project, resource and query; they expire after one hour. Reuse with a different scope or filter fails. Restart preserves the signing key and retained issue sort keys. Cursors from the previous issue-number ordering require restarting the query.
Dependency writes require access to both projects within the same organization. Cross-organization links are prohibited by the database and API. Transitive cycle detection and the graph edit execute in the same transaction, preventing two concurrent edits from jointly introducing a cycle. Dependency history changes the dependent issue revision, so stale graph edits conflict too.
Authorship and event custody
The server derives actor from the authenticated token. Imported author IDs and
names remain in provenance; they never grant authenticated user authority.
Operator-authorized imports supply provider github, stable external_id,
author_id, optional author_display_name, and source created_at, updated_at
and observed_at timestamps. Local creation/update times remain server times.
Repeated source IDs return the existing imported record and do not overwrite
later native edits. GitHub profiles and import documents
resumable history retrieval, explicit cutover, optional summaries and limitations;
attaching provenance alone is not a claim that all external history was imported.
Workers can edit their own comments. Operators can edit project comments, with
the editor recorded separately from the original actor and provenance. Hosted
users can edit or soft-delete only their own comments, regardless of organization
role. Soft deletion clears the current body and sets deleted: true, preserving
the original author, creation time, revision snapshots and attachment references.
Deleted comments cannot be edited or restored. Explicit
issue and comment content can contain code or secrets and is retained as
collaboration data. The service does not promise that authored text is secret-free.
History records have an event ID, organization/project, aggregate identity and
sequence, type, schema version, server recording time, authenticated actor and
typed data. issue.created, issue.edited, comment.created, comment.edited,
dependency.changed and workflow.transitioned are server-generated. They refer
to content revisions instead of duplicating full text in event data.
Worker schema 1 accepts run.started, run.finished, run.checkpointed and, with the optional native_runtime_evidence capability, ordered run.observed.
Their data requires lease_id, positive string fencing_token, and typed
run_, attempt_ and policy_ IDs. Finished outcomes are succeeded, failed,
cancelled and interrupted. Only checkpoints accept artifact_ids, at most 20
typed artifact_ IDs. Arbitrary URLs, signed download capabilities, raw prompts,
transcripts, tool output, local paths, source, diffs and artifact bytes have no
payload fields. The current lease and machine principal must match the item.
Idempotent retries return the committed result; new events with an expired fence
fail. A run outcome records a worker report, not a verified check or merge grant.
The policy ID is policy_ followed by its 64-character lowercase SHA-256 digest
and must equal the approved descriptor pinned to the lease.
Health findings
GET /api/v2/organizations/{organization}/projects/{project}/health/findings
and the health_findings MCP read require the current project's read grant.
They accept state=open|resolved (default open), an RFC3339 since, and
cursor/limit paging (at most 100 findings). since includes findings seen
or resolved at or after that time. The response includes items, an optional
next_cursor, and the tenant detector's last_tick_at (null before its first
successful evaluation).
The HTTP endpoint additionally accepts state=all: open findings are always
included, while since filters only resolved findings. Live consumers can share
one read for open findings and recent resolutions.
The Hub evaluates static health rules every minute using current state and
at most 24 hours of recorded events. Findings and their native issue reports
share a transaction with the successful tick. The existing machine reporter
files diagnostic-only instance findings at Urgent with the infrastructure label
into an existing nondispatchable, nonterminal state that is neither operator-only
nor the reserved Triage state. Backlog is preferred; otherwise workflow order
selects the first eligible state. If none exists, findings and the successful
tick are retained without filing an issue.
Other finding classes remain available through health reads without new issue
reports. Reports remain attributed to the detector instance, even when the
subject is a work item. Their bodies include <!-- detent:no-dispatch --> and
diagnostic evidence, without generated repair acceptance criteria or human
confirmation. Scheduled-CI repair reports follow their separate authorization
contract. The detector does not claim work, change existing lanes, or make model
calls.
Finding reads include filed_by: health_detector and project-scoped issues
links (project_id, work_item_id); issue bodies carry the finding ID, summary,
next action, evidence and origin fingerprint. Changed evidence and reopened
findings comment occurrences on matching open issues; unchanged observations
do not repeat comments. Resolutions comment their time without closing issues.
Terminal matches are already handled and remain unchanged. Existing human
questions, holds, lanes, stronger priority, origin and imported history remain
with the reporter owner. The operator can retire this project's external
board-audit filing sweep after deploying the detector reporter.
A finding preserves its ID and opening time while active and when reopened within 60 minutes of resolution. A later recurrence opens a new record with the same signal/subject fingerprint. Evidence lists are bounded to 20 IDs. Evaluations read at most 100 projects, 1,000 enrolled runners, 10,000 current nonterminal items per tenant, and 10,000 recent events; an exceeded read bound or failed evaluation retains previous findings and the previous tick.
Runner admission contexts also report last_refresh_at and refresh_duration
(nanoseconds) from the project refresh owner. Existing runners can omit these
fields; the detector uses enrollment time until refresh timing is reported.
Human findings use current completion authority and active conversation
questions, without reviving legacy worker question receipts. Recorded scheduler
cycles are preferred for refusals, with reported capacity and approved policy
as the fallback when cycle observations are absent.
Native runtime evidence
With native_runtime_evidence, GET /work-items/{item}/runtime selects the
latest fenced attempt or an exact native_attempt_id (typed native ID) or
positive attempt_id (recorded local attempt ID). Selectors are mutually
exclusive and ownership is scoped to the item and current project grant;
ambiguous local IDs require the native selector. An absent selected attempt
returns not found. An item with no recorded attempt returns explicit unavailable
evidence. Older deployments return unsupported availability separately from
grant denial.
With native_admission_evidence, the same runtime read accepts a URL-encoded
JSON admission parameter from the current registered runner. It contains
policy_id, observed_at, and the runner's current workflow_states, authors,
assignees, label_include and label_exclude claim filters. The JSON before URL encoding
is bounded to 8 KiB, each filter to 32 values and each value to 128 bytes. Operator
credentials cannot supply runner context. The existing single-item claim query
evaluates those filters under current project grants and approved policy; no
claim or scheduler event is written. Per-runner admission snapshots include
runner revision, approved policy identity, observation times, existing refusal
codes and unavailable predicates. Provider model requirements and home selection
remain unavailable without actual evidence. explain_item projects observed
refusals separately from historical scheduler decisions. An older Hub retains its
existing runtime read with runner-specific selection unavailable.
Candidate refusals preserve no_claimable_work and expose up to 100 authorized
unresolved_dependencies, each with work_item_id, project_id, current
state and terminal, when the shared claim dependency predicate excludes the
item. Projects that do not require dependencies never report them as exclusion
evidence. Per-runner snapshot observed_at and projected refusal at identify
the current read. other_native_candidate_exclusions remains unavailable when
dependency evidence does not establish all exclusions. native_candidate_exclusion
remains unavailable for a generic refusal without authorized dependency evidence
or a recorded scheduler refusal. When a recorded refusal is available, unknown
current exclusion details remain other_native_candidate_exclusions.
Truncated dependency evidence also marks unresolved_dependencies unavailable.
These observations do not create recorded scheduler skips or analytics samples.
With native_admission_observation, the existing registered-runner heartbeat
accepts an optional bounded admission observation containing context and the
runner_revision captured when that context was published. The Hub stamps
received_at and stores the observation in existing machine metadata under the
authenticated runner and project. Hosted explain_item reads that observation
without accepting operator-supplied selectors. Current routing revision and
approved policy must match, and both original selector and server receipt times
must be fresh under the existing heartbeat window. Omitted observations do not
freshen existing selectors; missing, mismatched or expired observations remain
unavailable. Runner grants and machine identity remain authoritative.
GET /work-items/{item}/runtime/github-timings requires both
native_attempt_id and runner_id. The MCP work-read operation
github_scope_timings takes those same selectors plus project_id and
reference. Both reuse current project read grants and recorded attempt/runner
ownership; runner credentials cannot select another runner. Reads inspect stored
runtime evidence and make no GitHub or provider calls.
Naturally completed native PR landings record at most 128 REST count keys and 128 timing keys, with dropped-key counts, scope and observation timestamps, stage/step, endpoint family, protocol, query purpose, outcome, attempt/timed counts and nanosecond sums/maxima. The response includes the recorded native Change/version/head/merge identity. HTTP covers Do, body consumption and Close; token resolution is inclusive and can contain installation HTTP. Concurrent HTTP sums can exceed enclosing scope wall time. These values do not establish disjoint elapsed time, busy-wall, critical path, percentages or savings. Untimed durations are null; absent boundaries, build identity and sub-step wall timing have explicit unavailable coverage. This projection contains no raw URLs, queries, commands, prompts, credentials or credential attribution hashes. Historical receipts without timing snapshots remain unavailable; this read does not revive the retired legacy GitHub tracker refresh or satisfy its comparison.
The typed snapshot includes the current native item, authentic last workflow
transition and recorded scheduler decision, current Change readiness and
enrolled runner routing/host/provider capacity. A current fenced lease is shown
independently of the selected attempt, including a lease without a started run;
lease reads omit private isolation configuration. Current candidate readiness is
not full runner eligibility, and board counts or nil PRs cannot establish it.
Historical decisions are recorded only when the existing scheduler evaluates
them, with actor, item revision, source and server time; reads add no history.
Declined claims record scheduler.decision with outcome: skipped and the
existing refusal reason. Refusals are recorded once per item, runner and reason
while the item revision and current Change version remain unchanged and no
successful claim intervenes. Repeated polls do not append duplicate events.
The runtime snapshot's native_candidate_exclusion contains the latest recorded
refusal, including its runner, reason and item revision, even after a later
claim succeeds. explain_item exposes it as historical evidence; it does not
establish current ineligibility. Analytics skip_reasons counts these recorded
events rather than current admission snapshots.
GET /work-items?include=board returns compact items, matching cards
(issue, status line, current attempt and change summaries), the durable project
sequence, viewer-specific grant_version, and work totals. Board reads use the maintained card projection,
without reading attempts or Changes per card. Their scope totals ignore lane
and open/closed selection while respecting search, labels, assignees, priorities
and archive selection. Board pages accept up to 2000 items and use signed
keyset cursors; cursor/filter scope and expiry remain enforced. The client reads
one initial page per visible lane and pages Backlog on demand.
The hosted project activity stream accepts board=true&since=<sequence> for
board frames. Each JSON activity frame has an SSE id, sequence, gap,
deltas (ordered sequence plus previous/current card), grant_version, and scope work totals.
A null current card removes the item from the board. The stream accepts the
board's search, label, assignee, priority, archive and completed-window filters
for its totals. Last-Event-ID takes precedence over since on browser
reconnects. The Hub retains the latest 2048 card deltas per project; gap=true
means the requested sequence is outside retained history and requires a new
snapshot. A changed grant_version also requires a new snapshot: it fingerprints
the viewer's readable projects, so dependency redactions update even when no
card changes or the viewer reconnects after grants change. Otherwise reconnect
replays missed cards, and unchanged ticks need
no additional browser requests. Workspace and item-scoped streams retain their
existing activity protocol.
GET /work-items?include=work adds a compact work member to the existing
inventory page. work.lanes contains state, total and observed running counts
for the same authorized project and server filters, independent of the cursor.
Running counts require a recorded running attempt with a current unreleased
lease; In Progress membership is not evidence of a live worker.
work.as_of is the server observation time. work.items selects at most the
requested page limit of nonterminal items, ordered by live attempt, dispatchable
state and descending issue number. work.truncated marks additional open items;
state filters and the existing inventory cursor still navigate them.
Both item selections omit issue bodies and linked-source snapshots in this
projection; the existing item detail read retains full content. The inventory page uses a lane, priority and activity ordered snapshot with
a scoped opaque cursor. Its total counts all items in that filtered snapshot
and stays the same on continuation pages. Clients can omit include=work on
continuations without changing cursor scope; state, open, archive and other
filters must stay the same. Clients retain bounded per-item observations with known, unchecked,
unavailable and partial coverage rather than inferring worker state from lanes.
Ordered events may include runtime: local attempt/generation attribution,
observed backend/model/effort provenance, phase intervals, heartbeat, bounded
instruction profile, landing receipt and existing REST accounting. Attribution
cannot change and heartbeat cannot move backwards in an attempt. Runtime
freshness is unavailable without an observation, expired for future/old
heartbeats or lost running leases, and available for recorded terminal
observations. Lease renewal is not proof of agent activity.
Activity spans contain hashes and allowlisted evidence labels, never command
text, instruction contents or filesystem paths. The projection retains at most
128 KiB, 64 instruction sources and 32 actions/span, with partial coverage,
dropped/unpaired counts and no claim that reading an instruction caused an
action. board_session_history provides offset/limit pages (1–200).
Receipts, attempt lists, work history and explanations include profile metadata
and explicitly identify omitted activity spans; full spans have their existing
board_session_history owner. Exact runtime and attempt detail retain the bounded
recorded profile.
A landing observation names Change/version/head and either actual landed
merge SHA/base/method or a bounded refusal kind. The Hub verifies actual landing
against recorded Change authority; terminal attempt success alone cannot prove
a ship. Configured command gates also record landing.gate: command,
exit_code, duration_ns, validated head_sha and tree_sha, and up to 64 KiB
of output with output_truncated when needed. The same receipt is available
on the attempt and Change version. Historical receipts without gate do not
establish that a command ran or passed. Duration measures command execution,
excluding worktree preparation and scratch cleanup.
REST data projects existing operation-header observations and attribution windows, with credential digests, resource, timestamp, HTTP status and the presence of a Used header. Selected-client counts exclude worker subprocesses, other clients and hosts; an unaccounted delta does not identify its consumer. At most 64 operation and attribution windows are returned, with dropped counts. This adds no quota capacity policy or aggregate reporting owner.
The existing explain_item, board_receipt, board_session,
board_session_history, work_attempt_receipt, work_history and
board_activity operations delegate to this native application evidence
through supported local and Cloud adapters. MCP results retain the 256 KiB
bound and return unavailable on overflow. Native efficiency aggregates and
unrecorded runtime data remain explicitly unavailable. Hub schema 59 preserves
prior event history and refuses rollback when new event types would be lost.
Tenant administrators read request traffic at
GET /api/v2/organizations/{organization}/diagnostics/requests. The existing
Diagnostics and Reports API responses and MCP reports response also include
requests for administrators. Project-selected keys and other members do not
receive tenant-wide traffic data. Admin keys with all project access can read
the API and MCP report.
The report covers the last completed hour, with UTC minute boundaries in
from and to. top_callers and top_routes each return up to 20 rows sorted
by descending count. Caller rows identify caller_kind (runner, browser,
mcp, ci, or anonymous) and caller_id; route rows identify HTTP method
and Echo route template, with IDs and query strings removed. Each row includes
count, sum_ms, p50_ms, p95_ms, and max_ms. Percentiles are nearest-rank
estimates from merged power-of-two microsecond histograms, bounded by the
observed maximum; they are not averages of minute percentiles.
Each tenant Hub aggregates requests in memory and flushes every minute to
<tenant-database-path>.metrics.sqlite, using a separate SQLite connection and
Goose schema. Minute rows retain caller, route, status class and latency for
14 days. Neither the shared entry nor the host aggregates tenant metrics.
The same flush folds requests into hourly rows per route template and status
class, retained for 90 days. With window=<duration> (1h to 2160h) the
endpoint also returns route_latency with from, to, bucket and
buckets. Each bucket row has from, route, status_class (2xx, 4xx,
and so on), count, sum_ms, p50_ms, p95_ms and max_ms, for the 20
busiest routes in the window. MCP time_series returns the same
route_latency to administrators for its window, in buckets of at least one
hour and at most 240 buckets.
Authenticated runners use a UTC-minute request budget of
120 + 60 * effective capacity, where effective capacity is the minimum of
the host capacity, runner reported capacity, and runner capacity limit. This
replaces the hosted api_mutations allowance for those runner requests; other
callers retain the existing allowance. Excess requests return 429 with code
runner_request_budget and Retry-After seconds until the next minute. The
existing health detector opens an instance finding for the runner, including
its top routes. Claims, lease renewal/release, machine heartbeats and runner
credential renewal are exempt. Hub clients share the response backoff across
ordinary requests and downloads, honor seconds and HTTP-date Retry-After,
and allow exempt traffic through during backoff. A refused read or download
waits out the window and is sent again, at most three times; a refusal without
a usable Retry-After waits for the next minute. Other 429 codes do not
start the backoff. Mutations are never replayed automatically.
Native analytics_dashboard, time_series and reports return a paged
failure_signatures read model per project. It groups recorded terminal errors
and landing refusals (conflict, base_protected) within the half-open report
window by a normalized first line. Signatures retain at most 256 UTF-8 bytes;
examples retain at most 512 bytes of the recorded first line. IDs, hashes,
path digits, timestamps and counts collapse to placeholders. Each group reports
count, first and last seen, total failed-attempt usage cost and tokens, occupied
slot-minutes, and at most 20 sorted work-item IDs with the distinct count and
an omission flag. Usage and occupied time cover the whole selected attempt,
even when it started before the failure window. The newest 1,000 failure records
are selected; omitted records set the project partial flag and
failure_signatures_complete_population unavailable marker. Group pages use
row_offset and limit and preserve continuation under the MCP byte bound.
Five matching failures on one work item within an inclusive 60-minute span
classify as retry storm. Otherwise backend startup, protocol and workspace-hook
failures classify as infrastructure, landing refusals as merge, and remaining
failures as attempt. These read-time classes do not alter instance attribution,
retry allowance or lane state. explain_item.failure_signature reports the latest
failure signature and its count on that issue across the newest 1,000 failure
records, with partial identifying an incomplete count. It is absent without
failures. New terminal records retain a bounded, redacted first-line error and
infrastructure class; provider response bodies and continuation lines remain
omitted. Historical discarded error text cannot be recovered; stored provider
metadata and finalizer errors remain readable fallbacks.
Rework and escape rates
Native reports, analytics_dashboard and time_series expose quality per
project; Cloud Reports uses the same project read. Windows are half-open and
population reads are bounded to 1,000 records per source. Clipping sets
quality.partial, lists quality_complete_population, and leaves percentages
null. A zero denominator also yields null rather than a measured zero.
rework_percent counts unique work items returned before landing by a review
request or a Rework transition, divided by unique items worked in the window.
Worked items include recorded Code/Rework attempts, working/review/landing
transitions, returned items and landings. Multiple returns on one item count
once. escape_percent counts unique versions landed in the window with a
non-infrastructure escape detected by the window end, divided by versions
landed in that window. Buckets use landing cohorts for the rate and detection
time for escape counts and causes. An escape of an earlier landing remains in
occurrence and cause counts without changing the current landing cohort.
Each landing freezes the issue revision and body, required missing sections,
acceptance criteria and the last approved validator review's criterion evidence.
Snapshots are retained per version across later publications and issue edits.
get_change_version returns landing_quality. Historical landings without a
snapshot remain readable but require a human override for contract attribution.
Reopening Done detects an escape against the latest preceding landing. Its
workflow event ID is the occurrence identity. Until classified, it remains in
counts with pending_classification; no cause is inferred from timing or from
an absent historical snapshot. Reverts and attributed scheduled failures are
registered by an evidence assessment through the existing Change discussion
command (POST .../changes/:change/discussion, MCP discuss_change). Supply
version_id, body, and an escape object:
{
"occurrence_id": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"kind": "revert",
"observed_at": "2026-10-07T12:00:00Z",
"cause": "missing_criterion",
"evidence_reference": "revert commit eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"evidence_quote": "This reverts commit dddddddddddddddddddddddddddddddddddddddd",
"basis_quote": "Accept valid input"
}
A read-only agent reads the occurrence and its immutable version context and
returns an assessment. An authorized caller records that assessment; the
classifier needs no source, issue, workflow or filing write authority.
underspecified_issue quotes weak contract text or
Missing required section: <section> from the snapshot. missing_criterion
quotes a complete contract whose criteria did not cover the failure and leaves
criterion empty. validator_miss names the exact covered criterion and
quotes its evidence reference or behavioral assertion in basis_quote.
infrastructure requires instance_id; its occurrence is attributed to the
instance, omits the work-item attribution, and does not affect the change escape
rate. Every recorded assessment requires the incident evidence reference and
quote. Pending classifications remain explicit.
A revert's occurrence identity is its revert commit SHA and its quote names the
landed commit. A scheduled failure's identity is the existing scheduled evidence
occurrence key and its reference is the run URL. It must quote an existing red
scheduled-suite report; candidate inclusion or the pinned head alone is not
causal attribution. A reopen assessment references the actual workflow event.
Occurrence kind, detection time and source reference are stable across
assessments. Repeated assessments count once; the latest assessment wins.
A human browser-session caller can set human_override: true to replace the
cause, and a subsequent agent assessment cannot replace that override.
The discussion retains actor and evidence history. These writes never file
issues, alter deduplication, reopen work or move workflow state. Existing
scheduled-suite issue reporting remains the sole failure filing owner.
Organization attempt activity
GET /api/v2/organizations/{organization}/activity and the activity MCP read
serve the same tracker projection under current organization and project grants.
Optional project_id and runner_id filters apply to running attempts, finished
attempts and typical durations. limit bounds only finished attempts (default
100, maximum 200). from and to accept RFC3339 timestamps or UTC dates, default
to the last seven days, and must describe at most 90 days ending no later than now.
running[] contains every visible attempt with a live lease, regardless of the
recent window or analytics population cap. Rows identify the item, number, title,
project, runner and attempt, with role in stage, current phase, attempt start,
last recorded phase start in stage_started_at, and stage_elapsed_seconds at
observed_at. Available links include session_id, live workspace_ids,
change_id and pull_request_url. finished[] uses the same identities and
links, plus outcome, finished_at and stage_duration_seconds. It is ordered
by finish time descending, then attempt id descending, within [from,to).
Expired or released running leases project as interrupted, matching per-item
attempt reads; their finish is the earlier lease release or expiry.
typical_durations[] groups the Reports attempt population by project and role,
combining models and efforts. It includes count, seconds, p50_seconds and
p90_seconds, using the same sum of closed phase intervals for succeeded and
failed attempts and the same percentile interpolation as Reports. Roles with no
positive closed duration are omitted. The recent window selects overlapping
attempts as Reports does; durations are not clipped to the window. Each project
uses the existing 1,000-attempt analytics population cap. Clipping, missing,
malformed or dropped phase history sets partial; missing timing fields are
omitted rather than replaced with zero. The read adds no stored state or polling;
clients refresh through the existing project event stream.
Ordered attempts and recovery
GET /work-items/{item}?view=recovery with
X-Detent-Recovery-Projection: bounded reads candidate recovery in one bounded
response. The complete encoded response, including JSON escaping and envelope,
fits the existing 1 MiB SDK limit. Source version, frozen policy identity/gates, claim/checkpoint identities, latest
implementation/acceptance criteria and hold summary retain their authority.
Command outputs use the existing excerpts and output_truncated. Security audit
summary and finding prose use bounded excerpts marked text_truncated, preserving
audit identity, verdict and every retained finding's ID, status and severity.
If mandatory review identities alone exceed the review budget, reviews are omitted
as a group and the exact hold summary and read.omitted remain authoritative;
consume the original records through continuation before acting on that evidence.
Landing audits share a 64 KiB budget across version and attempt receipt copies.
An audit that exceeds the remaining budget retains its identity and verdict but
marks run_omitted and omits the run. That incomplete run
cannot satisfy audit validity; the precomputed hold summary and receipt gate
result remain authoritative. read.omitted names each affected landing path;
continuation retains the complete original run, including all finding states.
Discussion
and ordinary Change evidence have row and byte budgets; read.omitted identifies
context available through work_comments, get_change, attempt reads and the
scoped read.detail_path. Recovery consumers must read omitted context needed
for their decision through those owners and follow their cursors.
If combined evidence exceeds the response budget, read.omitted identifies
projected frozen policy details or further omitted evidence. Complete policy
remains in its original version record; a projected descriptor is not a policy
approval document. Clients advertising X-Detent-Recovery-Body: fragments may
receive read.issue_body with an authenticated cursor, digest and encoded byte
count instead of inline instructions. This addresses only the issue_body
record through the existing continuation. Its cursor also pins the issue
revision. The SDK verifies scope, revision, contiguous offsets, record length
and digest, decodes at most 256 KiB of issue body, and restores it before returning
successful recovery. Failure aborts candidate hydration. Older clients retain
complete inline instructions or an explicit refusal, never a successful partial
issue. Optional retained history is never automatically reconstructed.
GET /work-items/{item}?view=recovery&limit=1 explicitly reads retained evidence,
one record fragment per response. continuation identifies the scoped item,
section, record_id, original version_id, base64 data (at most 64 KiB
decoded), offset, total_bytes, record sha256 and next_cursor. The cursor
traverses reviews, checks, Change discussion, issue discussion, attempts,
versions and Change metadata. Each read retrieves one record, not the whole
history; large records continue before advancing. section: complete ends the
traversal. Cursors retain the existing credential/query binding, pin the current
Change version and refuse an edited record during fragmentation. Clients consume
individual records; candidate hydration never reassembles the complete history.
The authenticated worker work_item tool exposes this continuation with
project_id, canonical reference and view: "recovery". Repeat with the
returned data.next_cursor as cursor until data.section is complete.
The full ordinary review, command output, frozen historical policy and retained
discussion remain available through this selector when other reads return
excerpts or refuse a record that exceeds their result bound.
Plain recovery reads retain their original contract and response-size refusal. Older Hubs ignore the projection header: a new runner still reads small recovery and preserves the original size refusal for oversized recovery, without a retry. Deploy the Hub before runners to enable bounded hydration; Hub rollback restores that same explicit refusal for oversized work. Older runners retain their prior behavior against either Hub version. No runner identity or configuration changes are needed for either order.
The fenced_run_history capability extends schema 1 with a positive decimal-string
data.sequence and identity: {role, backend, model}. Identity components are
bounded to 128 identifier characters. The server derives machine, runner and Hub
session identity from the authenticated lease. The policy digest remains pinned.
One lease binds one ordered attempt; a run can span successive fenced attempts of
the same issue. Sequence 1 must be run.started; subsequent sequences are
contiguous. run.finished closes the attempt and cannot finish a successor.
An identical sequence replay returns success without adding history, even when
the command key changes. Changed content, gaps, identity changes, repeated starts
and progress after completion conflict. Client occurrence times never order events.
Legacy schema 1 events without sequence remain readable and writable for legacy
attempts, but cannot be mixed into an ordered attempt.
With native_pre_provider_failure, a failure before provider startup may begin
and close an attempt with run.finished at sequence 1, without provider
identity. It requires terminal_failure and positive local attempt/generation
attribution in runtime; provider activity, usage, checkpoints, finalization,
disposition and completion responses are excluded. The existing lease, runner,
project and pinned-policy checks still apply. The terminal diagnostic retains
its observation time, stage: pre_provider and a bounded redacted diagnostic.
Attribution follows the recorded error class: infrastructure failures belong to
the instance, while issue configuration and source recovery refusals belong to
the issue. Timing alone does not establish ownership; unclassified failures leave
attribution explicitly unavailable. Hook diagnostics contain only the hook name
and exit status; hook output, commands, environment values and local paths are
omitted.
Existing attempt, runtime and MCP reads expose this receipt independently of a
provider session. Older runners and unsupported Hubs retain explicit unavailable
history; no attempt or provider metadata is backfilled.
For native Code and Rework turns with a valid final detent-status, ordered
run.finished data includes a disposition with status, reason_code when
reported, blocker and human-action indicators, and final_summary. The summary
contains the text before the final status fence, bounded to 8 KiB with UTF-8
preserved. Attempts and recovery reads retain this disposition. Non-complete
completion comments include the same summary and the reported reason; human-action
parking comments preserve them through the existing human-attention path.
GET /work-items/{item}/attempts uses the same authorized pagination contract as
history. Attempts are ordered by the existing monotonic lease fence. The response
contains effective identity/policy, last accepted sequence, server timestamps,
terminal outcome and the latest retained checkpoint. A running attempt whose lease
is released or expired reads as interrupted. Hub restart retains both the
projection and append-only history; expired leases and attempts are not deleted.
Pages are bounded to 1 MiB of encoded JSON, including continuation; a single
record that cannot fit is explicitly unavailable rather than skipped.
view=blockers omits only runtime observations at the SQL projection boundary
and uses the existing 64 KiB history-page budget. Attempt reads also accept
view=recorded_blockers, which applies the same projection and budget and also
omits uploaded attachment evidence; the runner uses it for recorded blocker
authority during observed-state and epic transition refresh. Fences, identity,
status, revision, disposition and checkpoint remain intact in both views.
GET /work-items/{item}/history?view=blockers keeps every event's identity,
actor, ordering and timestamp plus revision, transition and run fence/attempt
fields, before content/runtime omission. This preserves edit and operator hold
evidence for the same blocker read. Full attempt, history and runtime reads
retain their existing detail contracts and validation/landing evidence. Cursors
are bound to the view as well as the authorized project, item and caller.
The native scheduler hydrates full issue content, paginated discussion, attempts and history, including legacy events, before dispatch. Runner prompts receive this Hub context without consulting GitHub issue APIs. Checkpoints may carry the last reported typed Change/version/head reference; it is not an independently verified current Change or a review/merge grant. Use the authoritative Change/version reads for current delivery evidence; absent checkpoint references are omitted.
Ordered run.checkpointed events require a bounded handoff object:
| Field | Accepted values or format |
|---|---|
resume |
resume_session, fresh_checkout, manual_recovery |
storage |
git_ref, local_only (legacy), customer_store |
ref, commit_sha, tree_sha, base_sha |
Required with git_ref (base_sha optional): a refs/detent/checkpoints/ ref and hexadecimal Git IDs; forbidden otherwise |
availability |
available, missing, inaccessible, unverified |
worktree_state |
clean, dirty, unpushed, unknown |
head_sha, expected_head_sha, workspace_digest |
Optional hexadecimal Git commit IDs or workspace digest |
external_effect |
none, git_push, pr_create, provider_turn |
effect_state |
none, pending, confirmed, ambiguous |
effect_id |
Typed opaque effect_ identity for an external effect |
change |
Optional typed change_id, version_id, and immutable head_sha |
Customer-store checkpoints require artifact IDs and cannot assert available:
independent durable receipt/integrity/access verification follows the
artifact access contract. IDs convey neither a
download capability nor proof of ownership or availability. Hub never receives
workspace paths, provider session state, manifest contents, source, diffs, raw
transcripts, storage credentials, signed URLs or artifact bytes through handoffs.
A git_ref checkpoint names work the runner pushed to the project's own Git
remote (INV-17): a commit whose parent is the item branch head and whose tree
holds every uncommitted change, force-pushed with plain git push to
refs/detent/checkpoints/<work_item_id>. The item branch is never rewritten,
the ref raises no branch push event, and Hub stores only the reference. Runners
push at each stage end, after agent turns that changed the worktree and at
most five minutes apart while files change; an unchanged tree is skipped and a
failed push is logged and retried at the next trigger without failing the job.
Excluded paths stay on the machine; credential content or more than 20 MiB of
unpublished objects refuses the push. Terminal workspace cleanup deletes the ref.
Any runner restores the newest recorded git_ref checkpoint into a fresh
workspace by fetching its exact commit; an identical local worktree is reused and
any other is quarantined. Provider session resume is only an optimisation for the
runner that still holds the session; otherwise a fresh session starts on the
restored workspace. A legacy local_only checkpoint whose workspace is not on the
claiming runner starts fresh from the branch or current Change version. Ambiguous
Git/PR effects still require reconciliation by their source runner.
Recovery placement never routes by where earlier work ran. It refuses only checkpoints reported missing or inaccessible, a current Change version without a verified Hub source bundle (recaptured on its source runner), and an uncertain Git/PR effect, which waits for its source runner. Missing source produces a recovery blocker rather than Human Review readiness.
GET /work-items/{item}/source-recovery reports the source runner, checkpoint,
current immutable version, retained-source availability, execution quiescence,
selected destination and eligible destination runners. Runtime and explanation
reads, including MCP explain_item, include the same recovery facts.
POST /work-items/{item}/source-recovery and MCP transfer_item require work-write
and runner-administration authority, a command request identity, the expected
issue revision and exact version_id. The selected destination_runner_id is
recorded and audited but no longer routes claims: any eligible runner restores
the source. The command and its read are retired with INV-17 source transfer.
The command records an issue edit and reuses dispatch generation without changing
workflow lanes, human/delivery holds, Change versions, validation or reviews.
Transfer requires acknowledged terminal execution and release of every lease, including a claim that has not started. Expiry is not quiescence. Active, partitioned, archived or terminal work and uncertain Git/PR effects are refused. It does not revoke Git credentials, force an unreachable process to stop, or recover unavailable copies.
Native executions revalidate the pinned policy and current lease before startup,
provider turns and epilogue hooks. Lease responses include server_time; the
local deadline uses the remaining server lifetime minus request elapsed time and
a safety margin (10% of TTL, capped at five seconds). Replaying an existing claim
does not incorrectly grant a fresh TTL, and clock skew cannot extend authority.
Renewal failure does not extend it. Loss of authority cancels the provider context;
reconnection cannot revive that context. External epilogue hooks are skipped on
claim loss, and local retention remains allowed. Hub outages use the existing
scheduler backoff; no independent local scheduler starts and failure retry counts
are preserved. Once ordered execution exists, worker issue/comment/dependency/
workflow mutations also require the current lease_id and fencing_token.
Off-lease human collaboration uses operator authority. Exact command replays may
return a previously committed response after expiry, but perform no new mutation.
Database fencing is not an exactly-once guarantee for external effects. An accepted Git push, PR creation or provider request can outlive a lost response or race cancellation. In-flight provider tool calls are bounded by process cancellation, not an atomic transaction with Hub. Reconcile an uncertain push against the exact remote ref and expected head, reuse an existing PR only after verifying its head branch, and use provider idempotency/session lookup when available. Never blindly repeat an ambiguous external write. Existing expected-ref/head and worktree preservation paths remain authoritative. The client retains an unacknowledged event and replays its exact sequence and command before advancing; a restart retrieves durable attempts rather than assuming the last request failed.
Worker connector inventory and compatibility
internal/hubclient/native_connector.go supplies the existing connector interfaces:
| Orchestration or agent operation | Native route and behavior |
|---|---|
| Candidate discovery and adoption | Existing Hub scheduler, candidate query and fenced lease writer |
| Refresh by issue ID or workflow state | Full native issue detail or paginated list |
| Create issue, update body, title, assignee or priority | Typed issue mutation with revision check |
| State update | Configured native transition |
| Create/update workpad or discussion comment | Native comment mutation with edit revision |
| Read comments/events for subsequent workers | Consume every authorized comment/history page |
| Comment author authorization | Locally authenticated human provenance; imports confer no authority |
| Add/remove native dependencies | Scoped, revision-checked graph mutation |
Unsupported generic fields and absent optional connector capabilities fail explicitly. No native connector method delegates to a GitHub issue connector. GitHub repository/PR/CI/review/merge capabilities remain separate integrations; native Change Requests link to these integrations through optional external PR references and retain their identity when no GitHub PR exists.
Schema migration retains existing issue/repository integer keys, GitHub node IDs, issue numbers, queue entries, dependencies, leases/fencing, work events and outbox links. Compatibility repositories receive stable project aliases in the local organization under the existing instance-administrator boundary. Project grants are an explicit administrator decision, never inferred from a current login. Existing v1 clients keep their compatibility IDs and content authority. Native rows have no mandatory repository or GitHub issue reference and cannot be fetched by guessing their integer keys through v1. Compatibility event history remains in v1; no arbitrary historical payload is automatically copied to v2.
Native Change Requests
The change_requests capability adds stable change_... records to a project.
Each record has a primary issue, additional issue links in the same project,
title/discussion, and an ordered set of immutable version_... records. These
records work in native and GitHub-compatible projects without changing issue-field
ownership. Creating a Change Request does not create a PR or authorize a merge.
A runner opens a native item's Change Request when a work run finishes with
commits, under the run's lease, and then publishes the run's head as the
change's version: base_sha and merge_base_sha are the attempt diff's base,
repository is the https form of the checkout's origin remote, and code
names the head commit under that repository with availability unverified.
The runner also captures a Git bundle of the finalized head, excluding its base
history, and publishes source metadata with base/head identities, bundle and
raw Git diff SHA-256 digests, format and byte count, plus a base64 source_bundle.
The Hub stores these bytes atomically with the fenced version and serves them
through its version-scoped source read. Bundles are limited to 20 MiB; publication
requests permit 28 MiB to accommodate base64 encoding. Version metadata reads
contain no bundle bytes. Captured source excludes workspace setup and credentials.
A rework run whose head and policy already match the current version reuses
that version, including its existing review identity. Another head, a policy
update, or explicit republication of a legacy version with retained source
creates a new version with current validation. A forge
commit URI marked unverified never establishes source availability. Rework and
landing retrieve retained source through the scoped Hub read, verify its digests
and Git identities, then use the existing refresh and validation path. An older
committed attempt checkpoint does not supersede the authoritative
current Change: a clean checkout at its recovered current head starts a fresh
session, even when the older checkpoint has another head or workspace digest.
Dirty work, manual recovery requirements, and pending or ambiguous forge effects
retain their recovery requirements. Lease fencing and current-version publication
checks remain mandatory, and an older review cannot validate a new head. A legacy
version without retained bytes must exist in the authorized checkout; otherwise
recovery refuses a fresh base checkout and requests resumption on the source-owning
runner to republish the exact head with source capture.
A version the Hub refuses (no https remote, unavailable source, a stale review policy) is reported on the run's completion comment and the Change Request stays a draft until the next successful run publishes one.
Native issue external_references and work_relationships preserve provider
id provenance and include url, repository and number when durable GitHub
source identity is available. Imported identities use the source repository and
GitHub number, independently of the native issue number. Linked identities use
the canonical linked source URL.
Change detail includes optional source_issues, deduplicated from the Change
Request's canonical issue delivery links. The runner uses these references for
fully qualified Closes owner/repository#number lines in landing PRs, without
querying upstream issues. Reusing an open PR preserves its body and appends
missing references; already merged PRs retain their historical attribution.
Native-only changes have no synthetic source references.
All paths below follow /api/v2/organizations/{organization}/projects/{project}.
Mutations require the existing idempotency_key; workers publishing versions
also supply their current lease_id, fencing_token, run_id, and attempt_id.
| Method and suffix | Authority and result |
|---|---|
GET /work-items/{item}/changes |
Project-scoped linked Change Requests |
POST /work-items/{item}/release-pick |
Project operator; associate a verified existing release PR with a dependent native delivery issue |
POST /work-items/{item}/changes |
Worker/operator; title, body, optional linked_issues |
GET /work-items/{item}/changes/{change} |
Consistent detail with versions, decisions, checks, discussion and current summary |
POST /work-items/{item}/changes/{change}/versions |
Publish through the primary issue with expected_version_id |
GET /work-items/{item}/changes/{change}/versions/{version}/source |
Worker/operator; bounded immutable Git bundle from the authorized project and Change version |
POST /work-items/{item}/changes/{change}/discussion |
Append body, optional version_id; only operators can import provenance |
POST /work-items/{item}/changes/{change}/versions/{version}/reviews |
Operator decision: approved, changes_requested, or commented, plus optional body |
POST /work-items/{item}/changes/{change}/versions/{version}/checks |
Credential pinned in the immutable expected check set |
POST /work-items/{item}/changes/{change}/versions/{version}/landing |
Worker under its lease: merge_sha, base_ref, method; records the landed commit and finishes the primary issue |
GET /change-review-policy |
Inspect the approved native review/CI expectations |
PUT /change-review-policy |
Self-hosted instance administrator, or hosted owner/admin with a target-project write grant; compare expected_review_policy_id and approve policy |
Hosted approval requires an active organization owner/admin membership, a valid human session and CSRF token, and a write grant for the target project. Runner management grants are not required. Bearer tokens cannot replace human membership. The server rechecks the session, membership role and project grant inside the mutation transaction, including before returning a cached approval response.
Before publishing, an administrator approves a review policy tied to the current
repository policy_id. Approving a native project's repository policy seeds the
default review policy when the project has none: no CI check is pinned, and
require_review follows the repository policy's review.human setting, which
defaults to false. A review policy that pins no checks follows that setting: approving the
repository policy again rewrites it to the default, and an approval that expects
no review policy may replace it. A review policy an administrator shaped by
pinning checks is never rewritten; it goes stale when the repository policy
changes and must be approved again. A repository policy whose gates the default
cannot satisfy (a required check count) seeds nothing. Hub
migration 40 applies the same rule once to every project with an approved
repository policy, so projects approved before seeding existed can publish.
Its require_review setting cannot weaken a repository
human-review gate. required_checks cannot fall below the repository check count,
and explicit independent checks retain their independent authority. Every check pins name,
principal_id (an existing token with a project grant), workflow_id,
workflow_sha256, source (customer or independent), and max_age_seconds
(60 seconds to 7 days). Independent checks require operator credentials. Use a
dedicated CI credential, kept outside the implementation runner's environment.
The repository validator flag instead runs the existing validator agent in a fresh session after immutable publication. Its version-pinned verdict uses the review endpoint under the runner's fenced authority; the implementing session cannot submit its own verdict. A validator pass satisfies the validator gate, while human approval and explicit checks remain separate requirements. Rework retains the findings on the version and returns the item to the existing Rework lane. No separately provisioned operator credential is required by this flag.
Each validator review stores criteria_evidence and a derived not_verified
list against its immutable version. Every acceptance criterion has a criterion,
kind (receipt, test, attachment, or not_verified), and reference. Receipt entries
name a successful host-observed command and its reviewed head_sha and tree_sha.
Test entries name the file and test and describe the independent behavioral
assertion in behavior; tests that restate the implementation are flagged and
excluded. Missing, duplicate, or unsupported entries become not_verified.
Native Codex validators can read issue comments and attachments through the existing
scoped read owners. Image reads verify the complete content digest and display the
image to the validator. Attachment entries cite the returned host read_receipt
IDs in receipt_ids, name the attachment and digest, and describe the observed
visual outcome and its relevance to the reviewed source in behavior. The review
stores these reads in attachment_reads, with attachment provenance and review
head/tree identities. Download success or worker prose does not establish visual
acceptance. Older captures remain usable when the affected UI is unchanged.
Complete UTF-8 text/plain attachments of at most 32768 bytes can also support
nonvisual criteria. Diagnostic producers follow the
bounded evidence publication contract: publish JSON as
text/plain and preserve larger documents in ordered bounded parts. Unsupported
MIME and oversized text return actionable inspection_error guidance during
validation, without a complete-inspection receipt. A verified digest for a
truncated preview does not establish complete inspection. The acceptance
validator obtains its own review-bound receipts for every required part;
ordinary implementation and Rework workers cannot issue them and must not edit
source solely to obtain them. Inspection receipts prove observation in that
review, not independent execution or that the captured source matches the
reviewed source; validators assess that applicability separately.
The Change review page shows this map and highlights the unverified list.
gate.validator.unverified_criteria defaults to rework, naming every gap.
Projects may select pass-with-disclosure to retain gaps while allowing a pass;
the verdict, minimum score, and blocked severities still apply.
These settings do not modify repository auto-promotion, opt-out, security audit, validator, external required review, or merge-method configuration.
Version requests include lowercase base_sha, head_sha, merge_base_sha, the
approved policy_id, a repository reference, a code reference, and optional
artifacts. Worker versions identify a real fenced run/attempt. Human-authored
versions may omit a run. external, when present, contains provider: github,
the PR number as string id, and its url. The server snapshots the approved
repository descriptor and review policy; workers cannot choose weaker checks.
expected_version_id is empty for the first version and must match the current
version thereafter. Reusing a command with different content or publishing from
an old current version returns a conflict. New versions require new approval and
fresh evidence even when a publisher reports the same head.
Operator MCP exposes this same owner as publish_change_version in the
changes_artifacts toolset. Supply project_id, work_item_id, change_id,
request_id, expected_version_id (explicitly empty for the first version),
base_sha, head_sha, merge_base_sha, repository, policy_id, and code,
with optional artifacts and external. It accepts no run, attempt, lease,
fencing, actor or producer fields. Current operator write grants and nested
ownership are checked on execution and replay. request_id uses the same
idempotency owner as the REST idempotency_key; identical retries across
transports retain the original immutable version receipt.
Optional source_capture contains the HTTP command's typed source metadata
(format, base_sha, head_sha, bundle_sha256, diff_sha256, bytes);
source_bundle contains its base64-encoded Git bundle. Both forward to the same
source validation and immutable storage owner. source_capture is separate
from the existing diff-read source selector. The complete MCP arguments,
including metadata and base64 encoding, must fit the existing 64 KiB limit.
Larger source captures require the existing authenticated HTTP command; MCP
does not support the HTTP owner's full 20 MiB source limit. Publication retains
source but does not schedule a validator, attest Code availability, or produce
a reviewable diff bundle.
The result includes version and receipt for the published version, detail
for the live current Change and review/check summary, and work_item_state for
the observed current lane. A replay after another publication retains its
original version while reporting the newer current version. Publication runs
directly with existing operator authority. Normal configured review/check
requirements still apply; no-review/no-check versions can automatically promote
through the existing owner. Unverified references remain unverified. An external
PR reference preserves the repository integration's protection and landing
checks; publication alone does not attest a worker success or a merge.
Code, manifests, diff bundles, logs and other artifacts remain in customer storage.
Hub retains only kind, uri, sha256, and availability. References support
https, s3, or gs, without embedded credentials, query tokens or fragments.
Kinds are code, manifest, diff, test, log, checkpoint, or artifact;
availability is available, missing, inaccessible, or unverified. Availability
is a report by the authenticated publisher, not a Hub download or independent
verification. A future viewer can resolve these references using the same change
and version IDs. Raw patches, source and artifact bytes are not API fields.
Every version generates fresh check_run_id values. CI submits the expected
check_run_id, head_sha, native run_id (empty for a human version without a
run), policy_id, config_digest, workflow_id, workflow_sha256, and source,
plus terminal conclusion, completed_at, and nonempty evidence references.
The submitting credential must match the expectation. Completion cannot predate
the version or be in the future. Only success with available, unexpired evidence
can pass; skipped, cancelled, missing, inaccessible and stale results do not.
An approved policy with no required checks reports not_required, never a
synthetic success; its native review requirement still applies.
Freshness is measured from completion, not delivery. The authenticated CI source
is responsible for producing truthful results for the pinned workflow and head.
Terminal results are immutable. Exact redelivery is idempotent even with a new command key; changing an existing result conflicts. Late callbacks remain attached to their original version and cannot move the current pointer. CI callbacks do not require a still-running implementation lease. To rerun validation, publish a new version with fresh check identities. Customer-run evidence is visibly distinct from independent validation and cannot satisfy an independent expectation.
A changes_requested decision puts the review text on the primary issue as a
comment by the reviewer, where the next run reads it with the rest of the
discussion, and moves an issue waiting in review or in the landing lane back to
a dispatchable working lane (In Progress when the workflow has it), so the
runner picks it up again; an issue already being worked stays where it is. The
rework run reuses the item's Change Request and publishes its new head as the
next version.
An approved decision that leaves the current version reviewed moves the
primary issue to the landing lane: the lane named Merging, when the issue's
lane may move there and it dispatches. The runner that holds the project claims
the issue there and lands the reviewed head with its own credentials. By
default it uses plain git: it fetches the base branch (the remote's default), verifies the
worktree still stands at the reviewed head, combines the two by the approved
policy's merge_method (a squash commit, a merge commit, or the head's commits
replayed onto the base), and pushes the result to the base branch, refusing to
move a base that changed underneath it. It then reports the commit through the
landing route, and the Hub records it on the Change Request (landed, with the
version, head, merge commit, base branch, method and actor), marks the summary
landed, and moves the issue to the first terminal lane its lane may reach, in
one transaction. A landed Change Request accepts no further review or landing.
The Hub never holds git credentials and makes no forge API call to land.
A landing the repository does not allow is refused, never worked around: a worktree that moved past the reviewed head, a reviewed head the checkout no longer has, a conflict with the base, a base that already contains the head, a base branch that moved during the landing, or a base branch the forge protects (a pull request requirement, a protected branch, a hook that declines). The runner reports the reason, and the orchestrator moves the issue back to the review lane with a comment that carries it, such as "allow the runner to push to develop, or enable GitHub pull request mode for this project". Only an infrastructure failure (the remote unreachable, the Hub refusing the report) fails the run and retries it.
A project may opt in with deliverable.github_pull_request: true in its
approved repository policy. For a github.com origin and an authenticated gh
on the project runner, landing then publishes the reviewed attempt branch,
finds or opens a pull request against the remote default branch, and asks
GitHub to merge the exact reviewed head using the policy's merge_method.
In the default gate.landing_mode: per_landing, the configured command gate
runs before publication on a detached combination of that head and the fetched
base. Nonzero exits retain command evidence and
failing output and return to configured Rework through the existing landing
refusal owner. Initial and retry preparation share the same executor. The base
is refreshed before the merge request; in per-landing mode an observed advance
refuses the request.
GitHub's merge API pins the head, but provides no atomic expected-base field,
so concurrent external base writes still depend on the repository's protection
and serialized landing policy.
Landing uses gate.landing_mode: per_landing; legacy rolling_barrier
configuration is interpreted as per-landing validation. gate.run remains a
scalar command string. Native finalization validates the exact reviewed head,
and a rebased landing runs the short validation command on its integrated tree.
The landing barrier endpoints are retired. Detent's project policy uses
gate.run: make check-landing and requires operator re-approval in the Hub.
Versions pinned to the retired rolling policy are stale and must return through
Rework for fresh validation. Stop or drain old runners before this Hub upgrade,
then update both sides and approve the replacement policy before resuming.
A stale summary prevents new landing targets; it cannot recall Git credentials
or a merge already authorized by an old runner.
Scheduled validation supplies release evidence and never holds ordinary landing.
GitHub's branch protection, required checks, and required reviews decide
whether the merge succeeds. The runner verifies that the returned merge commit
is on the base branch before reporting it to the Hub. A refused merge returns
the issue to review with the GitHub reason; the reviewer can approve it again
after fixing that condition. This mode uses the runner's gh credentials and
does not give them to the Hub. The default remains plain git and requires no
GitHub API access.
Native approval never becomes a required GitHub review. Detail optionally reuses
the existing projected PullRequestSummary, labels it as a snapshot, and identifies
a mismatched external head. Existing protected-merge verification and the exact-head
merge mutation remain authoritative; the native API has no auto-merge switch.
detent hub issue exposes changes, create-change, change, publish-change,
review-change, check-change, discuss-change, and approve-review-policy.
The existing --project, --organization, --hub-url, --identity-file, and
--token-env flags select the scope and credentials. Mutations read typed JSON
from stdin. For example:
detent hub issue create-change wi_example --project prj_example <<'JSON'
{"idempotency_key":"propose-work","title":"Proposed work","body":"Review context"}
JSON
detent hub issue changes wi_example --project prj_example
detent hub issue change wi_example change_example --project prj_example
The Work board/list opens native issue detail, which links to Change Request detail. Detail provides version selection, stale/missing-evidence messages, discussion provenance, artifact availability, and issue/run navigation. Native run recovery uses published change references from ordered issue history rather than trusting a stale worker checkpoint to identify the current version.
Existing release-pick delivery
POST /work-items/{delivery}/release-pick and operator MCP
associate_release_pick use the same project-scoped native command. The caller
needs current operator/admin authority and a write grant for the selected
project. Runner and worker credentials cannot associate PRs. The project's
approved policy must use GitHub PR landing and independent review (native
review policy, validator, automated review, or security audit). This command
never changes project policy, issue state, holds, assignees, or deployments.
The API accepts idempotency_key, string expected_revision, and these fields:
| Field | Required identity |
|---|---|
source_issue_id, source_change_id |
Same-project source issue and its primary, currently landed native Change |
source_merge_sha |
The Change's recorded merge commit on develop |
repository |
Exact enabled project repository URL, such as https://github.com/example-org/example-project |
pr_number, head_ref, head_sha |
Existing open, non-draft PR and its exact branch/head in that repository |
base_ref, base_sha |
Existing release/vX.Y branch and its current exact head |
frozen_sha |
Frozen source boundary: the merge-base of the recorded release head and current develop |
MCP accepts project_id, work_item_id (delivery), request_id, integer
expected_revision, and a release_pick object containing the identity fields
in the table. Native client callers use AssociateReleasePick.
The delivery must already have the source as a typed required dependency, with dependency admission enabled. The source must be terminal with its native develop landing recorded. Active delivery leases, existing Changes, conflicting PR associations, stale revisions, and repository or provenance mismatches are refused without partial writes. A held delivery may be associated; its existing hold and admission predicates remain authoritative. Identical associations are idempotent and return the original owner receipt; conflicting replacements are refused. Current grants are checked even on replay.
A machine-created delivery remains unconfirmed by default. An approved project
WORKFLOW.md may delegate its bounded contract explicitly:
## Release delivery contract
Verified release picks of reviewed develop fixes may receive the approved delivery contract.
### Acceptance criteria
Deliver only the reviewed develop fix identified by the verified association,
using its associated release PR. Do not add unrelated changes.
### Must not break
Preserve tests, provenance, human holds, independent review and landing gates.
Do not reset the release branch, publish tags or deploy production.
### How we know it worked
The associated release pick passes its required checks and independent review.
Use exactly the project's effective required section names. work_config exposes
this approved body and policy identity as issue_contract.release_delivery.
The reporter copies those criteria when it files the Backlog delivery. After
verification, association confirms only an exact match to the approved criteria;
it preserves the original body, including diagnostic and provenance fences.
It neither moves the issue nor clears a hold. Ordinary machine reports and
unverified associations receive no confirmation. Replay retains the original
receipt and cannot refresh or broaden a grant.
Native admission checks the grant against the current policy, unchanged body, stored association, required source dependency and recorded develop landing. Changes invalidate delegated confirmation. An explicit human body save uses the existing human-confirmation path instead. Required checks and independent review of the release pick still run before landing.
The configured repository transport verifies source ancestry on develop, the
release branch head and frozen boundary, and the PR's exact head/base and
repository. This thin slice accepts exactly one commit with one parent equal to
the recorded release base, one exact git cherry-pick -x trailer naming the
source merge, and the same file paths/status and added/deleted lines as the
source patch (binary files require matching blob identities). Hunk line numbers
and unchanged context may differ. Multi-commit PRs, truncated patch responses,
and adapted conflict resolutions are refused. PR and branch identities are read
again before native association. The existing landing owner rechecks the live
PR, protection, review and gate evidence before merging.
The receipt records the source issue/Change/merge, PR repository/head/base,
frozen source boundary, new delivery Change, actor, and association time.
Existing work-item reads expose release_delivery; existing Change and PR reads
show the delivery independently. Association opens a draft Change. The normal
runner captures the exact associated source bundle and publishes its immutable
version with the existing PR reference. Publication refuses a different head,
base, repository, or PR. Review and checks belong to that version, not the
source fix. The landing workspace and gate use the recorded release branch;
the squash message retains the source -x trailer. Ordinary source issues keep
their normal base. A moved source needing a replacement PR requires a separate
reviewed delivery; association does not adopt arbitrary new heads.
Hosted immutable archives
Cloud's shared entry accepts raw archive bytes at
POST /api/v2/organizations/{organization}/projects/{project}/work-items/{item}/archives.
Use the normal project write credential or browser session and CSRF token, with
X-Detent-Archive-Schema set to a positive integer. A request is bounded to
16 MiB of uncompressed content. Repeating the same content and schema is
idempotent; the work item must be readable and writable by the caller.
The attachment owner compresses the bytes with deterministic gzip level 6,
reserves the compressed byte budget atomically in the existing attachment
catalog before uploading, then reads back the exact bytes before publishing any
work-item reference. It never falls back to inline storage. The response contains
kind: archive,
retention: organization, key, sha256 (of the compressed bytes),
compressed_size, uncompressed_size, codec: gzip, and schema_version.
Keys have the form orgs/{organization}/blobs/sha256/{first-two-digest-characters}/{digest}.
The existing attachment catalog holds one immutable row per organization and
object key, with separate work-item references. Identical bytes across projects
in an organization share a blob; different organizations never share one.
GET /api/v2/organizations/{organization}/projects/{project}/work-items/{item}/archives/{digest}/access
checks the caller's normal work-item read authority, then returns the reference
with url and expires_at. The URL signs GET for that exact key for five minutes;
the expiry timestamp comes from the signature. The response is private, no-store.
Clients fetch and verify the compressed digest before decoding the single gzip
member. URLs are transient capabilities and must never be persisted or logged.
The metadata endpoints under /attachment-metadata/archives are private to
the signed entry-to-Hub boundary.
Browser detail reads send X-Detent-Archive-Read: 1. For archived sections,
the issue, attempt, Change, diff, conversation and revision endpoints return
{archive, current, projection} instead of assembling full detail. archive
contains organization, project and work-item identities, a decimal-string
revision, section_id, compressed SHA-256, compressed and uncompressed byte
sizes, codec, schema_version and an authorized same-origin access_path.
current contains the current SQLite metadata. Hot reads retain their existing
response shape. Agent reads without this header retain their existing contract.
The archive producer publishes a complete revision in issue_archive_sections
only after all objects have verified attachment references. Rows use the same
positive archive_revision for the whole work item; readers select its highest
published revision. Sections are issue, attempts, attempt:{attempt_id},
change:{change_id}, diff, diff:{attempt_id} (with :{sequence} for an
explicit generation), conversation:{conversation_id}, and
revision:{comment_id}:{revision} (empty comment ID for an issue revision).
Each gzip member contains UTF-8 JSON
{"schema_version":1,"section_id":"issue","data":{"body":"Example"}}.
Other sections' data uses the existing endpoint response schema. The issue
section includes attempts, an array of timeline run summaries using the
attempt schema, without full runtime detail. The issue and issue-revision
sections never contain dependencies, blockers, dependents, related issues or
viewer-filtered reason fields. Those fields come only from current authorized
SQLite reads. Comments and timeline events also remain SQLite reads.
Section access requires the descriptor's section and revision query
parameters. A digest-only request cannot access an object published in this
manifest. The Hub repeats issue/project authorization and conversation audience
authorization before issuing access. Reporters cannot access private execution
sections. Missing sections of a published archive return archive_unavailable,
never an empty history. The metadata-only linked-conversation read uses
GET /work-items/{item}/conversation?metadata=true; it authorizes the
conversation but does not load messages.
The browser fetches object bytes without credentials or redirects, verifies the
compressed digest and sizes, then uses a Web Worker and streaming
DecompressionStream("gzip") to bound decoding to 16 MiB. HTTP Content-Encoding
is refused, preventing automatic decoding followed by a second gzip decode.
The worker validates the envelope and the client validates the endpoint schema.
An expired URL renews through authorization once, then stops. The existing query
cache holds decoded sections under client/session, organization, project, issue,
archive revision and digest identities. Its archive budget is eight sections
and 64 MiB with ten minutes of idle retention. Account/organization/grant changes,
sign-out and archive revision changes clear the applicable cached data. Archive
payloads and URLs have no disk, localStorage, IndexedDB or service-worker cache.
The Cloud entry adds only the configured storage endpoint to CSP connect-src.
Archives use the existing hosted attachment bucket and organization-deletion
prefix. They survive source deletion and attachment expiry, remain charged once
per blob in the organization's collaboration_bytes storage-tier usage, and are
kept until organization deletion. Upload preflight commits the catalog's byte
reservation under the current storage allowance before any object write; competing
uploads cannot oversubscribe that allowance. Reservations grant no read access.
Publication requires the existing reservation and records only the verified
work-item reference. Failed or uncertain uploads and publication failures keep
the reservation charged until a retry reuses it or the organization is deleted.
The failed request never deletes a blob that another request may have published.
An interrupted upload can be replayed with the same bytes: the owner verifies and
reuses an existing blob and reservation without a second charge. The existing
quota admission owns both phases; no additional catalog, retry loop or garbage
collector is introduced.
The entry configures bucket CORS for its exact application origin, GET/HEAD only,
exposing Content-Length, Content-Type and ETag. Its bucket credential therefore
needs lifecycle-read and CORS-write permissions in addition to attachment access.
Startup refuses enabled expiration rules that can match an archive prefix;
rules restricted to attachment prefixes remain permitted. Archive objects use
Content-Type: application/gzip, Cache-Control: private, no-store, and no
Content-Encoding. The archive path uses basic PUT/GET and Signature V4 presigning
without conditional writes, object lock, mandatory versioning or AWS checksum
extensions; see Spaces compatibility.
Retention, export and deletion design
Collaboration content, content versions, typed events, idempotency responses and owner backups all contain retained data. REST mutation receipts expire after 24 hours. This release has no automatic native content expiry or issue-deletion endpoint. Append-only triggers deliberately reject ordinary history deletion. Self-hosted operations defines full-instance export/import, credential fencing on restore, whole-instance retirement and operator-managed backup expiry. Scoped purge remains unimplemented; append-only storage is not a promise of permanent retention.
The existing archive/retention sweep compacts archived work, work that has been
in a terminal state for more than 3 days, and conversations settled for more
than 30 days. Active leases, unfinished executions, queued
messages and pending questions exclude an aggregate. Each transaction examines
at most four work items and four conversations, deleting at most 500 events per
aggregate. It retains comments, content revisions, workflow transitions,
scheduler decisions, attempt checkpoints and final observations, validation and
landing evidence, messages, questions and command receipts. Superseded runtime
observations, message updates and execution progress, streaming deltas,
heartbeats and redundant conversation snapshots are removed. Retained work-item
run events drop their activity spans and record activity_spans_compacted in
the activity coverage notes. Conversation
metadata changes keep their timeline boundaries.
History pages and conversation snapshots carry an optional compaction summary
with removed event counts by type, retained event count, removed payload bytes,
last removed sequence, compacted activity event count and compaction time. Existing history cursors invalidate
when their aggregate is compacted and instruct the reader to restart. Conversation
SSE cursors behind a removed event receive closed with cursor_expired; clients
fetch the current snapshot and resume from its cursor. Open work streams retain
all events and their existing cursors. Ordinary collaboration updates and
deletions remain rejected; retention replaces and restores the deletion and update
triggers within its transaction, so a failed batch rolls back rows and enforcement.
The first compaction records database and event-table sizes in the existing maintenance record and marks space reclamation pending. The online diff-body owner never invokes VACUUM: a quiet hour does not make VACUUM safe for tenant health checks. Reclaim space through offline operator maintenance after the first compaction has finished, with the tenant Hub quiesced and the database ownership lock held. Schedule that maintenance with the release owner and report the recorded before-compaction sizes, sizes before and after VACUUM, and retained event counts. Workers do not stop or maintain live tenant services. Later compaction frees pages for reuse without requesting another first-pass VACUUM.
A future scoped deletion procedure must use the following contract:
- Authorize the organization and record the deletion scope outside the affected content. Fence writers and active leases, then take an owner backup when policy permits a temporary recovery copy. Export current issues, every content revision, comments, history and identity mappings when requested; artifact content must come from its separate authorized service.
- Run maintenance through the single database owner, with ordinary API writes stopped. In one transaction, temporarily replace the append-only triggers with maintenance-only enforcement, remove or redact affected current content, revisions, event references, idempotency response bodies, legacy workpad/outbox copies, webhook payload copies and affected graph links, and write permitted content-free tombstones. Restore append-only triggers before committing and verify foreign keys. A crash must roll back both the data and trigger changes.
- Retain a deletion ledger outside older backups. Do not reuse deleted IDs. Any future content retention gap must invalidate old cursors with an explicit snapshot/resync response rather than silently presenting incomplete history.
- Expire every affected backup according to the approved policy. Restore into an isolated owner, replay the deletion ledger and rotate cursor/session authority before exposing restored data or accepting claims. A restore must not resurrect deleted content or accept a formerly valid lease.
- Record which artifact stores and external projections require separate deletion and their outcomes. Database deletion does not erase a GitHub projection, independently hosted artifact, exported file or customer backup.
Privileged scoped purge is not a supported operation. The supported full-instance restore revokes copied credentials and releases leases; it does not implement scoped erasure or automatically replay a customer deletion ledger. Operators must retain backups deliberately and must not disable triggers on a running Hub as a substitute for supported deletion.
Provider capacity reports
Enrolled native runners report provider capacity for their configured backends by default. The runner uses its instance name as the local account alias, advertises backend model identifiers, and refreshes an unknown availability observation on its existing heartbeat within the two-minute freshness window. It does not derive an account concurrency limit from its own slots. An explicit global client setting takes precedence over these default reports:
client:
hub_url: https://hub.example.com
identity_file: /absolute/private/runner/identity.json
organization_id: org_example
provider_capacity_file: /absolute/private/runner/provider-capacity.json
The file is a JSON array written by an operator-managed local collector. Detent does not log into provider accounts or scrape billing pages to populate it. Use atomic file replacement when publishing a new observation. The collector maps each backend ID to the credentials already used by that local backend; reporting an alias never switches the backend's account. Each backend has exactly one local account in a report. Configure separate backend IDs for separately selected accounts through the existing agent routing configuration.
[
{
"provider": "openai",
"backend": "codex",
"account_alias": "local-work",
"shared_account_alias": "team-subscription-a",
"models": ["gpt-5.6-sol", "gpt-6-astra"],
"model_details": [
{
"id": "gpt-6-astra",
"label": "GPT-6-Astra",
"provider": "openai",
"default": true,
"reasoning_efforts": ["low", "medium", "high", "xhigh", "max"],
"default_reasoning_effort": "low"
},
{ "id": "gpt-5.6-sol", "legacy": true, "reasoning_efforts": ["low", "high"] }
],
"max_concurrent": 2,
"availability": "unknown",
"observed_at": "2026-09-05T12:00:00Z"
}
]
model_details is optional and advisory: it describes models models already advertises and never widens what capacity matches. The collector writes it; the runner does not yet fill it from its backends' model catalogues. Each entry describes one reported model at most once, with at most 16 distinct reasoning efforts in the backend's own order (the Hub does not check the order), a default_reasoning_effort that must be in that list when the list is non-empty, a label of at most 128 characters, and legacy set when the provider has named a successor. A report carrying detail for a model outside models is rejected. When a conversation's preferences change, the Hub checks them against fresh runner reports: an explicit model must be one a fresh report advertises or the Hub's configured conversation.model, which is accepted without any runner report, and an effort outside the fixed vocabulary is accepted only when that model's detail publishes it (docs/conversation/decisions.md section 14); GET /app/bootstrap does not republish it yet, so preferences.models[] stays empty.
Only these fields, model_details and optional reset_at are accepted. Files are bounded to
256 KiB, 32 backends and 128 model identifiers per backend. Aliases are opaque
lowercase ASCII tokens, at most 64 characters; use no email addresses, API keys,
login sessions, billing records or customer prompts. Provider/backend/model
identifiers are bounded, case-sensitive tokens; use the same provider ID on every
machine sharing an account. models must advertise the identifiers selected
by local routing. An existing unpinned route uses the explicit provider_default
capability; capacity never picks a different model or rewrites effort.
max_concurrent is an optional operator-declared account bound on simultaneous
reserved runs, between 1 and 10000. Omitted or zero means no known account limit;
runner and host limits still apply. Never set it from one machine's slot count:
adding a smaller runner must not lower the account's capacity. It is not a token
balance or an estimate of transferable credits. availability is available,
exhausted or unknown. Observations at
least two minutes old, observations in the future, and reset hints reached at
equality become unknown, never automatically available. Unknown quota permits
dispatch only within any declared account concurrency bound and existing local brakes.
An exhausted fresh observation waits for a refreshed report or its reset hint.
Declare the same provider and shared_account_alias on every machine using the
same provider account, even when local aliases or backend IDs differ. Hub scopes
sharing to the organization, uses the lowest declared limit and honors exhaustion
from any overlapping report. Distinct explicit aliases declare independent
accounts. An omitted shared alias keeps the account local to its enrolled runner. Local
accounts are identified by runner identity, provider and account alias; matching
local alias text on different runners never combines their limits or usage.
Only matching explicit shared aliases combine accounts across runners. Drain work before
changing account mappings; existing leases retain their original reservation.
Registration and heartbeats accept typed provider_reports on the native machine
endpoints. Once a runner advertises reports, requests without selected candidates
cannot bypass reservations. A missing/invalid configured report file defers
dispatch; it does not silently disable capacity checks. Omitted heartbeat reports
retain the last report, including its original observation time.
POST .../claims/preview returns authorized candidates in the existing Hub queue
order, in pages of up to 100 (after/next are opaque cursor values). The client
uses the existing local role/model/override resolution, then includes
provider_candidates (work item ID, revision and selected role/backend/model)
in POST .../claims. Hub rechecks current issue revision, approved policy,
organization/project authority, tags, fixed host, health, host capacity and
provider compatibility inside the claim transaction. Capacity removes ineligible
choices from that order; it does not reprioritize work or select a fallback model.
The negotiated feature is provider_capacity_reservations.
Hubs advertising project_landing_capacity accept nonnegative
landing_capacity on claims, previews and lease renewal. Native GitHub landing runners send
their existing configured Merging limit and delegate that lane ceiling to the
Hub instead of applying it separately on each host. The claim transaction
counts active project landing leases and pins the effective ceiling on the new
lease. The strictest positive incoming or active-owner ceiling applies.
Legacy leases count toward occupancy. Zero supplies no additional ceiling and
retains existing behavior when no active owner declares one. Claim replay and
renewal may tighten a positive pin but cannot widen it. An expired owner may renew only if the project
has room; live owners are never preempted. Preview and selection use the same
capacity predicate, and native-only landing batches retain their existing
accounting. Direct code-to-landing continuations reserve the same capacity on
their existing lease through acknowledged renewal before starting landing work.
An occupied project refuses that transition without revoking the coding lease.
Clients requiring this feature defer claims against older Hubs.
Feature-aware multi-project selection does not borrow one project's incoming
ceiling for another. An occupied foreign project's incoming ceiling is unknown,
so its landing candidates cannot veto ready work in the requested project.
An unoccupied foreign landing remains eligible for the existing project ranking;
native-only batches and legacy requests retain their existing behavior.
Runtime admission reads cannot assume an incoming ceiling when it was not
reported. They retain a proven full refusal when active positive pins establish
it; other occupied GitHub landing cases remain unknown with
project_landing_capacity in the existing unavailable-predicate list.
Unoccupied projects and native-only landings retain their usual readiness.
Hubs advertising native_dispatch_wait also accept a preview limit of 1–100.
Omitting it retains the 100-candidate page. The bound applies before issue
hydration; a page can be empty with a nonzero next when current landing
readiness excludes its ranked candidates. Follow next with the same filters.
GET .../claims/wait?after=<cursor>&wait=30 is an outbound, cancellable wait for
the existing native candidate/claim owner. Enrolled runners need the Claim
operation. wait is 1–30 seconds, defaulting to 30. The response is
{"cursor":"<opaque epoch and revision>"}. An absent or older cursor returns
immediately; an unchanged cursor waits for a postcommit change or the timeout.
Changes in the authorized organization coalesce into a hint, including queue,
dependency, policy, runner routing, capacity and lease-release changes. The
hint has no claim authority. Reread through the existing scheduling refresh and
claim transaction. Held requests hold no database resources. The runner keeps
the cursor across reconnects, uses a dedicated 40-second request deadline,
paces immediate responses to at most one per second and backs failed requests
off from one to 30 seconds. Unchanged timeout cursors do not trigger refresh.
Older Hubs retain the runner's existing timer refresh. Drain and shutdown cancel
the wait. Existing progress/completion APIs and dashboard SSE remain unchanged.
The claim response includes provider_reservation, pinned to the fenced lease.
The client rechecks the local account and model immediately before run.started;
Hub rechecks the shared pool while accepting the first ordered start. A changed
identity or newly exhausted pool defers the start without spending the issue's
failure budget. Existing local budgets, pools, host pressure and provider backoff
still apply. Cancellation, failed preparation, hydration failure and normal
release free capacity through the existing lease. Expiry frees abandoned
reservations after restart; stale fencing tokens cannot release a successor's
reservation. Lost acknowledgments remain idempotent. Capacity changes affect new
starts and preserve active execution identity and history.
These reservations coordinate enrolled Detent runs using reported accounts;
external provider consumers still require fresh observations and local provider
error handling. Fleet details show reports, shared usage and waiting reasons;
run details show the selected role/backend/model/account and reason. detent doctor reports the same capacity view. There is no additional global banner.
Selective onboarding issue intake
Native projects can import selected GitHub issues with runner credentials and
GitHub transport disabled on Hub. Associate a matching runner checkout first.
GET /api/v2/organizations/:organization/projects/:project/onboarding/issue-intake
returns batch (nullable) and configured lanes. Administrator mutations use
POST to the same path with idempotency_key, revision, and action:
start: begin one asynchronous job for all matching issues with adestination, optionallabels,label_lanes, andinclude_closed. The Hub's GitHub App reads 100 issues per GraphQL page, including complete bodies, labels, provenance and the first comment page. Additional discussion pages are fetched before intake. The existing Hub maintenance worker applies each page and persists progress. Runner fallback uses an enrolled runner reporting the matching checkout only when the App is unavailable or lacks repository access;runner_idmay select that fallback explicitly.discover: fetch one preview page using the same App-first transport.more: request the next preview page within the plan's job limit.apply: provide selectednumbersanddestination. A configured non-dispatchable, nonterminal lane is the default. GitHubdetent:<lane>labels map to matching Cloud states unlesslabel_lanessupplies a mapping; other labels are copied. Conflicting lane labels fail that item. Dispatchable destinations, including those selected by label, requireallow_dispatch: true. Existing source identities are reused without changes.retry: resume failed discovery/items, optionally on another fallbackrunner_id. Reported source retry deadlines must have elapsed. Completed items are retained.allow_dispatch: truemay confirm a previously refused dispatchable destination;destinationandlabel_lanescan correct failed item routing. A retry rechecks the current plan's job limit.
start and discover optionally accept a ProjectV2 source:
{
"project_v2": {
"id": "PVT_example",
"status_lanes": {"Ready": "Todo", "Waiting": "Blocked", "Complete": "Done"}
},
"destination": "Backlog",
"allow_dispatch": true
}
id is the board's GraphQL node ID. Supply 1–100 exact, case-sensitive
Status-to-native-lane mappings. Discovery reads the board's single-select
Status field, including archived items, through the existing App or runner
transport and credentials. The selected credential must already have read
access to that board; no additional credential or permission is requested.
An inaccessible board fails discovery instead of silently dropping lane state.
Without project_v2, intake performs no ProjectV2 reads and retains label routing.
When supplied, ProjectV2 Status takes precedence over lane-label routing.
Unmapped Status values, unset Status, and issues absent from the board use
destination, even when a lane label matches. Previews expose project_status;
item results report unmapped_project_status for this fallback (an empty string
means missing Status or board membership). Other label-copying rules still apply.
Dispatchable mapped lanes require allow_dispatch: true on start or apply.
apply and retry may supply corrected status_lanes in project_v2, retaining
the project ID selected before discovery. Omitting it retains the existing source
and mapping. Completed and skipped items remain unchanged.
An explicit retry routing correction also updates the lane of failed selected
items created before snapshot intake, while preserving their native content.
Items whose source snapshot is already complete retain their lane.
The per-job size cap uses the plan catalog's unarchived_issues allowance:
Free permits 200, Starter 2,000, Growth 10,000 and Scale 50,000. Unhosted Hubs
permit 1,000. Source filters can narrow a repository to fit the cap. Normal
organization capacity and storage allowances still apply to imported issues.
Results include aggregate progress counts and per-item completion, failure,
skip and retry information. MCP pages and command receipts omit issue bodies
and source snapshots. The runner fallback reports up to 100 results to the
worker-only /onboarding/issue-intake/result endpoint, whose receipt carries
its next page task without waiting for another heartbeat. Each successful item
commits its source snapshot, provenance, comments and completion checkpoint;
failed items roll back independently. Native edits retain field ownership.
Rate limits stop unstarted work until explicit retry, honoring source deadlines.
No idle GitHub polling occurs. The wizard refreshes progress only on request.
Reopening setup resumes persisted selection and results. Webhook intake of new
issues creates linked issues in Triage, except for explicitly delegated,
verified CI repair reports described below. New authoring and
landing continue through native Hub operations, including the existing Done
transaction; any later explicit source action keeps its own write authorization.
Trusted scheduled CI imports
A native project's approved repository workflow may delegate admission of new GitHub Actions repair reports through its existing Scheduled CI repair contract. Before its five required subsections, the section must contain this exact authorization sentence:
New verified GitHub Actions CI repair reports enter Todo with Urgent priority.
The repository's WORKFLOW.md supplies the five nonempty contract subsections:
Acceptance criteria, Must not break, How we know it worked, Invariants touched,
and Safety regression. The operator approves that exact workflow revision
through the existing project policy owner. Merely editing a repository file or
an issue does not grant this delegation. Projects without the approved sentence
retain their existing confirmation rules; Detent's direct scheduled reporter
keeps its existing project-specific delegation. Its release suites use one open
stabilization owner per release series, with one bounded occurrence per failed
job and the original replay identities. Native duplicate matching keeps that
series owner separate from root-cause repair issues in the selected Detent
project; existing repairs remain referenced with their ownership and holds
intact. Develop and other projects retain their existing matching behavior.
The producer opens an issue in the configured checkout repository using its
GitHub Actions identity. Its body contains those same five section contents,
a separate diagnostic-evidence heading, the existing detent-origin stamp
(doctor, github-actions, stable problem fingerprint and run-attempt URL),
and one detent-checks scheduled occurrence. That occurrence identifies the
repository, run, attempt, failed job, job name, tested commit and conclusion.
Place diagnostic text and occurrence metadata under the separate heading so
they do not change the approved contract section digests. For example, use
example-org/example-project as the fixture repository; production identity
comes from the selected project.
The signed issues.opened webhook is the automatic import path. Explicit
POST /work-items / MCP file_issue with only github_issue_url and the normal
request identity uses the same admission after the GitHub App fetches the full
source. Caller-supplied native title, body or priority retains ordinary intake
semantics. Historical bulk import and runner-supplied snapshots do not grant
this delegation.
The Hub requires the authenticated source author github-actions[bot], a source
repository equal to the configured checkout repository, and the existing
importer's independent verification of the repository's CI workflow and branch.
The approved repair contract may instead select one exact workflow and branch
with this authorization sentence before its five subsections:
New verified GitHub Actions CI repair reports from
.github/workflows/ci.yamlondeventer Todo with Urgent priority.
This selection comes only from the operator-approved project workflow, never
from the imported report, labels or webhook payload. Paths must name a single
YAML file directly under .github/workflows/; wildcards are not supported.
The original authorization sentence retains .github/workflows/ci.yml on
develop or release/v*. An explicit selection replaces those defaults and
requires both the named path and branch to match exactly. For example,
example-org/example-project may select ci.yaml on dev through its approved
WORKFLOW.md; the same run remains ineligible without that selection.
The approved contract and configured checkout repository binding are always
required. The importer verifies
the run attempt, repository and head repository, scheduled/push/manual event,
tested head SHA and completed failed or timed-out job. The approved policy and
checkout binding are rechecked during creation, and Todo must be dispatchable,
nonterminal and not operator-only.
Only then does a new report receive Todo, native priority 0 (Urgent), and
confirmation of the bounded approved sections. Source diagnostics remain quoted
as untrusted evidence. Labels such as detent:todo and hotfix confer no
authority. No review, security, validator or landing gate changes.
Incomplete, untrusted, held or unverifiable scheduled reports remain in Triage
without delegated confirmation; their github.imported history records the
refusal reason. Existing source identities retain native decisions. A matching
open machine fingerprint receives one occurrence comment per occurrence key,
preserving its body, state and priority; terminal matches remain untouched.
Repeated deliveries cannot readmit, reprioritize or reconfirm existing issues.
Producer delivery and this admission behavior are independently testable;
automatic recovery requires both plus the approved project policy.
Archived detail sections
Deep API and MCP reads use one Hub section resolver. SQLite selects an
organization/project/work-item reference from native_archive_sections and the
attachment catalog; a caller cannot supply an object key or download URL. The
Hub reads through the tenant's inherited Cloud Entry channel. Entry owns storage
credentials and signing and checks the current allocation generation. The Hub
verifies the compressed digest, both sizes, schema and a single gzip member
before querying the section. Each call fetches at most one section, limited to
4 MiB uncompressed. An unavailable, invalid or incompatible section produces an
explicit archive availability result instead of an empty successful history.
A schema-1 section has schema_version, organization_id, project_id,
work_item_id, section_id and tables. Each table has name, columns and
rows. Columns start with rowid, followed by the current SQLite columns in
schema order, including generated columns. Rows contain JSON scalar SQL values.
Only the run, Change, revision, history or conversation tables permitted for that section
are accepted; organization, project, item and parent identities must match the
catalog. The resolver overlays these records on a read-only SQLite snapshot.
Primary keys identify payload stubs; original row IDs remain ordering anchors.
Current issue visibility, relationship filtering and conversation authorization
continue to use live SQLite records.
The archive publisher retains run identities, diff headers, Change version and
evidence identities, and conversation/message identities needed by existing
selectors, foreign keys and recovery cursors. It registers each section only
after the attachment owner's immutable work-item reference is published. Run
sections record local_attempt_id when present. Revision sections contain the
checkpoint and deltas required to reconstruct every revision in their inclusive
first_sequence/last_sequence range. Conversation ranges use original message
sequences; a before read selects the range immediately preceding that sequence.
Private conversations remain live and never use a shared section. Storage
archival is independent of the issue's board-visibility archived flag.
Reads that resolve a section add an archive descriptor with availability,
section_ids and the compressed sha256. archive_url: true on supported MCP
reads, or archive_url=true on API reads, requests a five-minute URL when the
principal has unrestricted native administrator authority or an organization
owner/admin browser session. Filtered principals
receive the normal authorized fields without a raw section URL. URL presentation
does not change a native pagination cursor's scope.
Issue lists, run summaries, comments and factual timeline projections stay in SQLite. Full history pages can resolve a history section using the original event sequences; continuations cross section boundaries without returning retired payload stubs. Issue/comment search retains its existing FTS index and bounded matched snippets; its external-content view reads canonical issue and comment bodies. The document identity table contains no second body copy. Archived tool output and logs are not indexed.