API Reference

Generated from docs/openapi.json (84 paths, 119 operations) by scripts/gen-api-reference.py — do not hand-edit. Regenerate with ./scripts/regen-generated.sh after the spec changes.

This page lists every path, method, parameter and request/response schema name. It does not inline schema bodies — load docs/openapi.json into an OpenAPI viewer (Redocly, Scalar, Swagger Editor) for the full definitions, or read them directly in the spec file.

Two authentication surfaces, the WebSocket endpoint (not in this spec), and worked examples are in docs/API-REFERENCE.md.


System

Health and debug probes.

GET /api/debug/db-stats

Database statistics

StatusMeaningSchema
200Per-table row counts—

GET /api/debug/info

System info (only in debug builds)

StatusMeaningSchema
200Build, version, database size, and non-sensitive config—

GET /api/health

Liveness + readiness check

StatusMeaningSchema
200Service is live; reports version and applied migration count—

Projects

Projects: the top-level container for work.

GET /api/projects

StatusMeaningSchema
200All projects in the workspaceProject[]

POST /api/projects

Request body: CreateProject

StatusMeaningSchema
200Project createdProject
400Validation errorErrorEnvelope

DELETE /api/projects/{id}

ParamInTypeRequiredDescription
idpathstringyesProject ID
StatusMeaningSchema
200Deleted—
404Project not foundErrorEnvelope

GET /api/projects/{id}

ParamInTypeRequiredDescription
idpathstringyesProject ID
StatusMeaningSchema
200The projectProject
404Project not foundErrorEnvelope

PATCH /api/projects/{id}

ParamInTypeRequiredDescription
idpathstringyesProject ID

Request body: UpdateProject

StatusMeaningSchema
200Updated projectProject
400Validation errorErrorEnvelope
404Project not foundErrorEnvelope

Items

Items: the universal work unit (epics, tasks, bugs, …).

DELETE /api/items/{id}

ParamInTypeRequiredDescription
idpathstringyesItem ID
StatusMeaningSchema
200Deleted—
404Item not foundErrorEnvelope

GET /api/items/{id}

ParamInTypeRequiredDescription
idpathstringyesItem ID
StatusMeaningSchema
200Item with roles and dependencies; carries an ETag header for a later conditional PATCHItemDetail
404Item not foundErrorEnvelope

PATCH /api/items/{id}

ParamInTypeRequiredDescription
idpathstringyesItem ID
If-Matchheader['string', 'null']noOptional ETag from GET /api/items/{id}; a stale or malformed value returns 412 and writes nothing

Request body: UpdateItem

StatusMeaningSchema
200Updated item; carries the ETag for the exact returned snapshotItem
400Invalid transition / validation errorErrorEnvelope
404Item not foundErrorEnvelope
412If-Match did not match the current item version — nothing was writtenErrorEnvelope

Removes an item's manual (or imported) GitHub link. 204 even when the

ParamInTypeRequiredDescription
idpathstringyesItem ID
StatusMeaningSchema
204Unlinked (or was already unlinked)—
404Item not foundErrorEnvelope
ParamInTypeRequiredDescription
idpathstringyesItem ID
StatusMeaningSchema
200The item's current GitHub linkGithubLinkBody
404Item not found, or not linkedErrorEnvelope

Manually links an item to a GitHub issue so status and comments sync both ways — the

ParamInTypeRequiredDescription
idpathstringyesItem ID

Request body: GithubLinkBody

StatusMeaningSchema
204Linked—
400Invalid repo or issue numberErrorEnvelope
404Item not foundErrorEnvelope

GET /api/projects/{project_id}/items

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
statuspath['string', 'null']yes
item_typepath—yes
prioritypath—yes
sprint_idpath['string', 'null']yes
parent_idpath['string', 'null']yes
assigneepath['string', 'null']yes
tagpath['string', 'null']yes
searchpath['string', 'null']yes
pagepath['integer', 'null']yes
per_pagepath['integer', 'null']yes
StatusMeaningSchema
200Paginated itemsPaginatedItems

POST /api/projects/{project_id}/items

ParamInTypeRequiredDescription
project_idpathstringyesProject ID

Request body: CreateItem

StatusMeaningSchema
200Item createdItem
400Validation errorErrorEnvelope
404Project not foundErrorEnvelope

GET /api/projects/{project_id}/items/tree

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
StatusMeaningSchema
200Item hierarchy (parents with nested children)Item[]

Sprints

Sprints / iterations within a project.

GET /api/projects/{project_id}/sprints

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
StatusMeaningSchema
200Sprints for the projectSprint[]

POST /api/projects/{project_id}/sprints

ParamInTypeRequiredDescription
project_idpathstringyesProject ID

Request body: CreateSprint

StatusMeaningSchema
200Sprint createdSprint
400Validation errorErrorEnvelope

GET /api/sprints/{id}

ParamInTypeRequiredDescription
idpathstringyesSprint ID
StatusMeaningSchema
200The sprintSprint
404Sprint not foundErrorEnvelope

PATCH /api/sprints/{id}

ParamInTypeRequiredDescription
idpathstringyesSprint ID

Request body: UpdateSprint

StatusMeaningSchema
200The updated sprintSprint
400Validation errorErrorEnvelope
404Sprint not foundErrorEnvelope

PATCH /api/sprints/{id}/status

ParamInTypeRequiredDescription
idpathstringyesSprint ID

Request body: UpdateSprintStatus

StatusMeaningSchema
200Status updated—
400Validation errorErrorEnvelope
404Sprint not foundErrorEnvelope

Roles

Roles / specialties and their assignment to items.

DELETE /api/items/{item_id}/roles/{role_id}

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
role_idpathstringyesRole ID
StatusMeaningSchema
200Role removed from item—

PUT /api/items/{item_id}/roles/{role_id}

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
role_idpathstringyesRole ID
StatusMeaningSchema
200Role assigned to item—

GET /api/projects/{project_id}/roles

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
StatusMeaningSchema
200Roles for the projectRole[]

POST /api/projects/{project_id}/roles

ParamInTypeRequiredDescription
project_idpathstringyesProject ID

Request body: CreateRole

StatusMeaningSchema
200Role createdRole
400Validation errorErrorEnvelope

DELETE /api/roles/{id}

ParamInTypeRequiredDescription
idpathstringyesRole ID
StatusMeaningSchema
200Deleted—
404Role not foundErrorEnvelope

Mrp

An attempt's Merge-Readiness Pack and the human verdict on it.

GET /api/executions/{request_id}/attempts/{attempt_number}/mrp

GET /api/executions/:request_id/attempts/:attempt_number/mrp

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID
attempt_numberpathintegeryes1-based attempt number
StatusMeaningSchema
200The parsed pack and its review recordMrpResponse
404No pack for this attemptErrorEnvelope

POST /api/executions/{request_id}/attempts/{attempt_number}/mrp/review

POST /api/executions/:request_id/attempts/:attempt_number/mrp/review

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID
attempt_numberpathintegeryes1-based attempt number

Request body: MrpReviewRequest

StatusMeaningSchema
200The recorded reviewobject
400Blank reasonErrorEnvelope
404No pack for this attemptErrorEnvelope
409The pack was already reviewedErrorEnvelope

POST /api/executions/{request_id}/attempts/{attempt_number}/mrp/viewed

POST /api/executions/:request_id/attempts/:attempt_number/mrp/viewed

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID
attempt_numberpathintegeryes1-based attempt number
StatusMeaningSchema
200The review record, with viewed_at stamped onceobject
404No pack for this attemptErrorEnvelope

Metrics

Factory metrics measured from a project's rows.

GET /api/projects/{id}/metrics/factory

GET /api/projects/:id/metrics/factory

ParamInTypeRequiredDescription
idpathstringyesProject ID
sincequerystringnoStart of the window (RFC 3339); absent means all time.
StatusMeaningSchema
200Factory metrics measured from the project's rowsFactoryMetrics
400since is not RFC 3339ErrorEnvelope
404Project not foundErrorEnvelope

Briefs

An item's brief: acceptance criteria, constraints, definition of done.

DELETE /api/items/{item_id}/brief

DELETE /api/items/:item_id/brief

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
StatusMeaningSchema
204Brief deleted—
404Item not found, or it has no briefErrorEnvelope

GET /api/items/{item_id}/brief

GET /api/items/:item_id/brief

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
StatusMeaningSchema
200The item's briefItemBrief
404Item not found, or it has no briefErrorEnvelope

PUT /api/items/{item_id}/brief

PUT /api/items/:item_id/brief

ParamInTypeRequiredDescription
item_idpathstringyesItem ID

Request body: UpsertItemBrief

StatusMeaningSchema
200The brief as storedItemBrief
400Validation errorErrorEnvelope
404Item not foundErrorEnvelope

Comments

Comments on items.

GET /api/items/{item_id}/comments

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
StatusMeaningSchema
200Comments on the itemComment[]

POST /api/items/{item_id}/comments

ParamInTypeRequiredDescription
item_idpathstringyesItem ID

Request body: CreateComment

StatusMeaningSchema
200Comment createdComment
400Validation errorErrorEnvelope

Dependencies

Directed dependency edges between items.

GET /api/items/{item_id}/dependencies

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
StatusMeaningSchema
200Dependency edges for the itemDependency[]

POST /api/items/{item_id}/dependencies

ParamInTypeRequiredDescription
item_idpathstringyesSource item ID

Request body: CreateDependency

StatusMeaningSchema
200Dependency createdDependency
400Cycle detected or duplicateErrorEnvelope

DELETE /api/items/{item_id}/dependencies/{dep_id}

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
dep_idpathstringyesDependency ID
StatusMeaningSchema
200Deleted—
404Dependency not foundErrorEnvelope

Attachments

File attachments on items.

DELETE /api/attachments/{id}

DELETE /api/attachments/:id

ParamInTypeRequiredDescription
idpathstringyesAttachment ID
StatusMeaningSchema
204Attachment deleted—
404Attachment not foundErrorEnvelope

GET /api/attachments/{id}

GET /api/attachments/:id

ParamInTypeRequiredDescription
idpathstringyesAttachment ID
StatusMeaningSchema
200Attachment file bytes—
404Attachment not foundErrorEnvelope

GET /api/items/{item_id}/attachments

GET /api/items/:id/attachments

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
StatusMeaningSchema
200Attachments on the itemarray
404Item not foundErrorEnvelope

POST /api/items/{item_id}/attachments

POST /api/items/:id/attachments

ParamInTypeRequiredDescription
item_idpathstringyesItem ID

Request body: string

StatusMeaningSchema
200Attachment metadata—
400Missing/oversized fileErrorEnvelope
404Item not foundErrorEnvelope

Boards

Saved board views and their grouped item layout.

DELETE /api/boards/{id}

Delete a board

ParamInTypeRequiredDescription
idpathstringyesBoard ID
StatusMeaningSchema
204Board deleted—

GET /api/boards/{id}

Get a specific board

ParamInTypeRequiredDescription
idpathstringyesBoard ID
StatusMeaningSchema
200The boardBoard
404Board not foundErrorEnvelope

PATCH /api/boards/{id}

Update a board

ParamInTypeRequiredDescription
idpathstringyesBoard ID

Request body: UpdateBoard

StatusMeaningSchema
200Updated boardBoard
422Validation errorErrorEnvelope

GET /api/boards/{id}/view

Get board state with items grouped and filtered

ParamInTypeRequiredDescription
idpathstringyesBoard ID
StatusMeaningSchema
200Board with items grouped into columnsBoardViewResponse
404Board not foundErrorEnvelope

GET /api/projects/{project_id}/boards

List all boards for a project

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
StatusMeaningSchema
200Boards for the projectBoard[]

POST /api/projects/{project_id}/boards

Create a new board for a project

ParamInTypeRequiredDescription
project_idpathstringyesProject ID

Request body: CreateBoard

StatusMeaningSchema
200Board createdBoard
404Project not foundErrorEnvelope
422Validation errorErrorEnvelope

Custom Fields

Per-project custom field definitions and values.

DELETE /api/custom-fields/{id}

Delete a custom field

ParamInTypeRequiredDescription
idpathstringyesCustom field ID
StatusMeaningSchema
204Field deleted—

GET /api/custom-fields/{id}

Get a specific custom field

ParamInTypeRequiredDescription
idpathstringyesCustom field ID
StatusMeaningSchema
200The field definitionCustomFieldDefinition
404Field not foundErrorEnvelope

PATCH /api/custom-fields/{id}

Update a custom field

ParamInTypeRequiredDescription
idpathstringyesCustom field ID

Request body: UpdateCustomField

StatusMeaningSchema
200Updated fieldCustomFieldDefinition
500Update failedErrorEnvelope

GET /api/items/{item_id}/custom-fields

Get all custom field values for an item

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
StatusMeaningSchema
200All custom field values for the itemCustomFieldValue[]

DELETE /api/items/{item_id}/custom-fields/{field_id}

Delete a custom field value

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
field_idpathstringyesCustom field ID
StatusMeaningSchema
204Value deleted—

GET /api/items/{item_id}/custom-fields/{field_id}

Get a specific custom field value

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
field_idpathstringyesCustom field ID
StatusMeaningSchema
200The field valueCustomFieldValue
404Value not foundErrorEnvelope

PUT /api/items/{item_id}/custom-fields/{field_id}

Set a custom field value for an item

ParamInTypeRequiredDescription
item_idpathstringyesItem ID
field_idpathstringyesCustom field ID
StatusMeaningSchema
200Value setCustomFieldValue
404Item or field not foundErrorEnvelope
422Value failed field validationErrorEnvelope

GET /api/projects/{project_id}/custom-fields

List all custom fields for a project

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
StatusMeaningSchema
200Custom field definitionsCustomFieldDefinition[]

POST /api/projects/{project_id}/custom-fields

Create a custom field for a project

ParamInTypeRequiredDescription
project_idpathstringyesProject ID

Request body: CreateCustomField

StatusMeaningSchema
200Field createdCustomFieldDefinition
404Project not foundErrorEnvelope

Templates

Reusable project templates.

POST /api/projects/from-template/{id}

Create a project from a template

ParamInTypeRequiredDescription
idpathstringyesTemplate ID

Request body: CreateProjectFromTemplate

StatusMeaningSchema
200Project created from templateProject
404Template not foundErrorEnvelope
422Validation errorErrorEnvelope

POST /api/projects/{project_id}/save-as-template

Snapshot a project's configuration as a reusable template

ParamInTypeRequiredDescription
project_idpathstringyesProject ID

Request body: SaveAsTemplateRequest

StatusMeaningSchema
200Template snapshot createdProjectTemplate
404Project not foundErrorEnvelope

GET /api/templates

List all project templates

ParamInTypeRequiredDescription
project_typequeryProjectTypeno
StatusMeaningSchema
200Templates (optionally filtered by project type)ProjectTemplate[]

POST /api/templates

Create a new project template

Request body: CreateProjectTemplate

StatusMeaningSchema
200Template createdProjectTemplate
422Validation error (workflow shape, custom field options)ErrorEnvelope

DELETE /api/templates/{id}

Delete a template (user-created only)

ParamInTypeRequiredDescription
idpathstringyesTemplate ID
StatusMeaningSchema
204Template deleted—

GET /api/templates/{id}

Get a specific template

ParamInTypeRequiredDescription
idpathstringyesTemplate ID
StatusMeaningSchema
200The templateProjectTemplate
404Template not foundErrorEnvelope

Import

Import from JSON/YAML/CSV, GitHub Issues, and Linear.

POST /api/projects/import

POST /api/projects/import

StatusMeaningSchema
200Import result with the new project and stats—
400Invalid import payloadErrorEnvelope

POST /api/projects/{id}/import-csv

csv

ParamInTypeRequiredDescription
idpathstringyesProject ID

Request body: string

StatusMeaningSchema
200Counts of created and skipped rows—
400Malformed CSVErrorEnvelope
404Project not foundErrorEnvelope

POST /api/projects/{id}/import-github

github

ParamInTypeRequiredDescription
idpathstringyesProject ID

Request body: GitHubImportRequest

StatusMeaningSchema
200Counts of created/skipped issues and rate-limit remaining—
400Bad repo, token, or rate limitErrorEnvelope
404Project or repo not foundErrorEnvelope

POST /api/projects/{id}/import-linear

linear

ParamInTypeRequiredDescription
idpathstringyesProject ID

Request body: LinearImportRequest

StatusMeaningSchema
200Counts of created and skipped issues—
400Bad API key, filter, or rate limitErrorEnvelope
404Project not foundErrorEnvelope

Export

Project export to JSON / YAML / CSV.

GET /api/projects/{id}/export

GET /api/projects/:id/export

ParamInTypeRequiredDescription
idpathstringyesProject ID
formatquerystringno
StatusMeaningSchema
200Export file (JSON, YAML, or CSV per the format query)—
400Unsupported formatErrorEnvelope
404Project not foundErrorEnvelope

Full-text search within a project or globally.

GET /api/projects/{project_id}/search

ParamInTypeRequiredDescription
project_idpathstringyesProject ID
qquerystringyes
StatusMeaningSchema
200Matching itemsItem[]

GET /api/search

ParamInTypeRequiredDescription
qquerystringyes
StatusMeaningSchema
200Matching items across all projectsItem[]

Backup

Local and S3-compatible cloud backup / restore.

GET /api/backup

VACUUM INTO snapshot streamed as application/octet-stream.

StatusMeaningSchema
200SQLite snapshot (secrets scrubbed)—
400Not a file-based databaseErrorEnvelope

GET /api/backup/remote

list remote backups newest-first.

StatusMeaningSchema
200Remote backup manifests, newest first—
409Remote backup not configuredErrorEnvelope

POST /api/backup/remote

create a bundle and upload it to the configured S3

StatusMeaningSchema
200Backup manifest—
409Not configured, or another device has newer workErrorEnvelope

POST /api/backup/remote/restore

download a bundle and stage it for next restart.

Request body: RestoreRemoteRequest

StatusMeaningSchema
200Restore staged for next restart—
404No remote backups foundErrorEnvelope
409Not configured, or restore would lose newer workErrorEnvelope

POST /api/backup/remote/verify

download a bundle and validate it (sha256 +

Request body: RestoreRemoteRequest

StatusMeaningSchema
200Verification verdict plus the manifest—
404No remote backups foundErrorEnvelope
409Remote backup not configuredErrorEnvelope

POST /api/restore

Validate a SQLite backup and stage it for the next restart.

StatusMeaningSchema
200Restore staged for next restart—
400Not a valid SQLite fileErrorEnvelope
409Uploaded schema is newer than this binaryErrorEnvelope

Settings

Runtime-editable server settings (cloud backup).

GET /api/settings/backup

current cloud-backup configuration (secret masked).

StatusMeaningSchema
200Cloud-backup config (secret masked as secret_key_set)—

PUT /api/settings/backup

save cloud-backup configuration.

Request body: UpdateBackupSettings

StatusMeaningSchema
200Updated config (secret masked)—
422Validation errorErrorEnvelope

Execution Operator

Harness-agnostic runner fleet (Part III): PM-side execution-request/fleet/runner-enrollment/agent-profile management. Authenticated the same way as the rest of this API (operator session or API token); scopes idempotency and audit actor to the server-derived x-tack-principal, which a client cannot set (see crate::middleware::inject_operator_principal).

GET /api/agent-profiles

StatusMeaningSchema
200Every agent profile, by nameAgentProfileListResponse

POST /api/agent-profiles

Request body: CreateProfile

StatusMeaningSchema
200Agent profile createdCreateProfileResponse
409conflict (name already exists)RunnerV1ErrorEnvelope

POST /api/attempts/{attempt_id}/decisions/{decision_id}/resolve

Resolve a pending decision with an operator-supplied answer

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID the decision belongs to (opaque)
decision_idpathstringyesDecision ID, scoped to attempt_id: a decision_id that exists but belongs to a different attempt resolves as 404 not_found, indistinguishable from one that never existed at all — an attacker guessing another attempt's decision_id learns nothing.
x-tack-decision-tokenheaderstringyesTACK_EXECUTION_DECISION_TOKEN — a second, independent operator credential on top of the ordinary operator auth every other /api route uses (never a substitute for it). Fail-closed: every call is rejected with 403 whenever the server has not configured TACK_EXECUTION_DECISION_TOKEN at all — there is no "no secret configured, allow everything" fallback the way the plain Bearer gate has for an unset TACK_API_TOKEN.

Request body: ResolveDecisionRequest

StatusMeaningSchema
200Decision resolved — either a fresh write or a byte-identical idempotent replay of one (replayed distinguishes the two).ResolveDecisionResponseSchema
400invalid_request (missing/malformed answer, or answer.option_id is not one of this decision's own recorded options)RunnerV1ErrorEnvelope
401unauthorized — no x-tack-principal; a runner bearer credential never satisfies this, by constructionRunnerV1ErrorEnvelope
403forbidden — x-tack-decision-token missing, unconfigured server-side, or mismatched (details.required_scope = "operator:decisions")RunnerV1ErrorEnvelope
404not_found — no decision exists for this exact (attempt_id, decision_id) pairRunnerV1ErrorEnvelope
409decision_expired / idempotency_conflictRunnerV1ErrorEnvelope
413payload_too_large (answer exceeds decision_answer_bytes_max, 32768 bytes)RunnerV1ErrorEnvelope

GET /api/executions

ParamInTypeRequiredDescription
item_idquerystringno
item_idsquerystringnoComma-separated item ids — returns exactly one row per id, its own
limitqueryintegerno
StatusMeaningSchema
200Execution requests, newest first. item_ids present: exactly one row per id that has at least one execution, the batch's own most recent one. Else scoped to one item when item_id is given, otherwise every request the install has recorded up to limitExecutionListResponse
400invalid_request (a malformed item_ids entry, or more ids than the route's cap)RunnerV1ErrorEnvelope

POST /api/executions

Request body: CreateExecution

StatusMeaningSchema
200Execution request created or idempotently replayedCreateExecutionResponse
400invalid_requestRunnerV1ErrorEnvelope
404not_found (item does not exist)RunnerV1ErrorEnvelope
409conflict / idempotency_conflict / runner_revokedRunnerV1ErrorEnvelope

GET /api/executions/{request_id}

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
StatusMeaningSchema
200Execution request detailExecutionDetailResponse
404not_foundRunnerV1ErrorEnvelope

GET /api/executions/{request_id}/attempts

the operator read path

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
StatusMeaningSchema
200Every attempt made against this request, oldest first (may be empty)AttemptListResponse
404not_foundRunnerV1ErrorEnvelope

GET /api/executions/{request_id}/attempts/{attempt_number}/artifacts

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
attempt_numberpathintegeryes1-based attempt number
StatusMeaningSchema
200Every artifact manifested for this attempt, oldest first (may be empty)ArtifactListResponse
404not_found (execution_request or execution_attempt)RunnerV1ErrorEnvelope

GET /api/executions/{request_id}/attempts/{attempt_number}/artifacts/{artifact_id}/content

Download a verified artifact's raw content

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
attempt_numberpathintegeryes1-based attempt number within the execution request
artifact_idpathstringyesArtifact ID, scoped to the attempt that reported it (opaque)
StatusMeaningSchema
200The artifact's raw bytes. Content-Type is the artifact's declared media_type, or application/octet-stream when none was declared.string
401unauthorized — no authenticated operator principalRunnerV1ErrorEnvelope
404not_found (details.artifact_id) — no artifact manifest matches this (request_id, attempt_number, artifact_id) tripleRunnerV1ErrorEnvelope
409conflict (details.artifact_id) — the artifact manifest exists but its content has not been verified yet; distinct from not_found, never silently treated as "gone" or zero bytesRunnerV1ErrorEnvelope

GET /api/executions/{request_id}/attempts/{attempt_number}/decisions

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
attempt_numberpathintegeryes1-based attempt number
StatusMeaningSchema
200Every decision raised against this attempt, oldest first (may be empty)DecisionListResponse
404not_found (execution_request or execution_attempt)RunnerV1ErrorEnvelope

GET /api/executions/{request_id}/attempts/{attempt_number}/events

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
attempt_numberpathintegeryes1-based attempt number
StatusMeaningSchema
200Every event this attempt has reported, oldest first (may be empty)EventListResponse
404not_found (execution_request or execution_attempt)RunnerV1ErrorEnvelope

POST /api/executions/{request_id}/cancel

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)
StatusMeaningSchema
200Cancellation requested — not yet terminalCancellationRequestedResponse
404not_foundRunnerV1ErrorEnvelope
409conflict (already terminal)RunnerV1ErrorEnvelope

POST /api/executions/{request_id}/requeue

ParamInTypeRequiredDescription
request_idpathstringyesExecution request ID (opaque)

Request body: RecoveryConfirmation

StatusMeaningSchema
200Requeued (or replayed) after an audited recovery decisionRequeueResponse
409conflict / idempotency_conflict / invalid_transitionRunnerV1ErrorEnvelope

GET /api/runner-fleets

StatusMeaningSchema
200Every runner fleet, by nameFleetListResponse

POST /api/runner-fleets

Request body: CreateFleet

StatusMeaningSchema
200Fleet createdCreateFleetResponse
409conflict (name already exists)RunnerV1ErrorEnvelope

POST /api/runner-fleets/{fleet_id}/members

ParamInTypeRequiredDescription
fleet_idpathstringyesFleet ID (opaque)

Request body: AddFleetMember

StatusMeaningSchema
200Runner is now (or already was) a member of the fleetFleetMemberResponse
404not_found (fleet or runner does not exist)RunnerV1ErrorEnvelope

DELETE /api/runner-fleets/{fleet_id}/members/{runner_id}

ParamInTypeRequiredDescription
fleet_idpathstringyesFleet ID (opaque)
runner_idpathstringyesRunner ID (opaque)
StatusMeaningSchema
200Runner removed from the fleetFleetMemberResponse
404not_found (runner was not a member of this fleet)RunnerV1ErrorEnvelope

GET /api/runners

the read path for agent_runners

ParamInTypeRequiredDescription
fleet_idpath['string', 'null']yesOptional roster filter — a runner is included only if it is a
StatusMeaningSchema
200Every enrolled runner (optionally filtered to one fleet's roster)RunnerListResponse

POST /api/runners/enrollment

Creates a pending runner and stores only a SHA-256 enrollment-token hash.

Request body: CreatePendingRunner

StatusMeaningSchema
200Pending runner created; the raw enrollment token is returned exactly onceCreatePendingRunnerResponse
400invalid_requestRunnerV1ErrorEnvelope
409conflict (name already exists)RunnerV1ErrorEnvelope

POST /api/runners/{runner_id}/enrollment-tokens/{token_id}/revoke

ParamInTypeRequiredDescription
runner_idpathstringyesRunner ID (opaque)
token_idpathstringyesEnrollment token ID (opaque)
StatusMeaningSchema
200Token revokedRevokeEnrollmentTokenResponse
404not_foundRunnerV1ErrorEnvelope
409conflict (already consumed)RunnerV1ErrorEnvelope

POST /api/runners/{runner_id}/revoke

ParamInTypeRequiredDescription
runner_idpathstringyesRunner ID (opaque)
StatusMeaningSchema
200Runner revokedRevokeRunnerResponse
404not_foundRunnerV1ErrorEnvelope

Runner Protocol V1

Harness-agnostic runner fleet (Part III): the pull protocol a tack-runner process speaks at /api/runner/v1 (enroll, claim, heartbeat, report). Authenticated by a distinct, per-runner hashed bearer credential — never the operator token, and never substitutable for it (docs/contracts/runner-v1/protocol.json: credentials_are_not_substitutable). Every wire shape is frozen by docs/contracts/runner-v1/, not independently re-specified here.

POST /api/runner/v1/attempts/{attempt_id}/accept

Report the attempt entering preparing

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Transition accepted or replayed—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/artifacts

Submit an artifact manifest (content upload is the separate PUT .../artifacts/{artifact_id}/content operation below; content download is a distinct, operator-facing route — see execution-operator's "Download a verified artifact's raw content")

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Manifest accepted; per-artifact upload URLs issued—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

PUT /api/runner/v1/attempts/{attempt_id}/artifacts/{artifact_id}/content

Upload one manifested artifact's verified raw content

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
artifact_idpathstringyesArtifact ID from this attempt's prior manifest submission (POST .../artifacts, opaque)
x-tack-fencing-tokenheaderstringyesThe attempt's current fencing token. The request body is raw bytes, so — unlike every other runner-protocol write — the fencing token cannot travel inside a JSON body, so it travels as a header instead. docs/contracts/runner-v1/ fixes the manifest exchange's payload shape, not this upload URL (see this fragment's own doc comment).
StatusMeaningSchema
200Content verified and committed: {protocol_version, attempt_id, artifact_id, state: "content_verified", size_bytes, sha256}—
400invalid_request (Content-Type mismatch, or the upload stream ended early)RunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
409conflict (content already recorded and is immutable; or the attempt is not currently running/waiting_decision) / artifact_checksum_mismatch / stale_leaseRunnerV1ErrorEnvelope
413payload_too_large (artifact_content_bytes_max)RunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/cancellation-observation

Report the observed effect of a requested cancellation

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Cancellation observation committed or replayed—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/completion

Report the attempt's terminal outcome

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Completion committed or replayed—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/decisions

Create a decision for later out-of-band operator resolution

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Decision recorded—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/decisions/poll

Poll for decision resolutions since a given timestamp

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Resolved decisions since after, plus the new next_after cursor—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/events

Append a fenced, checkpointed batch of execution events

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Batch committed (accepted/duplicate event ids, committed checkpoint)—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/recovery-observation

Report a post-restart recovery observation for an attempt (additive v1 operation; exact path fixed by protocol.json)

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Recovery observation committed or replayed; server-authoritative disposition returned—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/attempts/{attempt_id}/start

Report the attempt entering running

ParamInTypeRequiredDescription
attempt_idpathstringyesAttempt ID, issued at claim time (opaque)
StatusMeaningSchema
200Transition accepted or replayed—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/claim

Claim the next eligible execution request for this runner or its fleet

StatusMeaningSchema
200A fenced lease and the immutable request snapshot, or no_eligible_work—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/enroll

Exchange a single-use enrollment token for a runner identity and bearer credential

StatusMeaningSchema
200Runner enrolled; the raw bearer credential is returned exactly once—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/heartbeat

Report liveness, capacity, and active-attempt state in one fenced batch

StatusMeaningSchema
200Renewed lease facts per reported attempt—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

POST /api/runner/v1/refresh

Refresh reported capabilities and optionally rotate the runner's bearer credential

StatusMeaningSchema
200Capabilities accepted; a rotated credential, if requested, is returned exactly once—
400invalid_requestRunnerV1ErrorEnvelope
401unauthorizedRunnerV1ErrorEnvelope
403forbidden / runner_revokedRunnerV1ErrorEnvelope
404not_foundRunnerV1ErrorEnvelope
409conflict / idempotency_conflict / invalid_transition / stale_leaseRunnerV1ErrorEnvelope

Local Runner

GET /api/local-runner

the persisted preference, the live runtime

StatusMeaningSchema
200Embedded-runner preference, runtime state, and provider catalog—

PUT /api/local-runner

save the preference and start/stop the embedded

Request body: UpdateLocalRunner

StatusMeaningSchema
204Preference saved and the runtime reconciled to match—

GET /api/local-runner/secrets

names and set-at timestamps only.

StatusMeaningSchema
200Stored secret names and set-at timestamps, never values—

DELETE /api/local-runner/secrets/{name}

not an error if already absent.

StatusMeaningSchema
204Removed (or already absent)—

PUT /api/local-runner/secrets/{name}

store a value. Never echoes it

Request body: SetLocalRunnerSecret

StatusMeaningSchema
204Stored; the value is never echoed back—