Skip to content

API

fishhawkd serves a versioned REST API under /v0. Runs, stages, plans, approvals, scope amendments, and the audit log are all reachable through it — the CLI, the Web UI, and the MCP server are three clients of this one surface.

The source of truth is docs/api/v0.openapi.yaml; the human companion is docs/api/v0.md.

GET /healthz needs no credential and reports the build’s commit and its embedded schema hashes. Everything else requires a bearer token. A daemon with a dev-only surface mounted (--dev-fixtures / --dev-stub-forge, the shape scripts/dev preview runs) advertises dev_mode: true there; such a daemon refuses the host-dispatch spawn marker for every caller (403 host_dispatch_refused_dev_mode, before the credential check) and stamps forge_writes: deny on every prompt response, so no runner it could dispatch pushes a branch or opens a pull request on the real forge.

Two runner-facing report bodies share one endpoint each and are discriminated by an outcome field the table below does not spell out: the pull-request report (POST /v0/runs/{run_id}/pull-request) accepts the implement stage’s success, failure, child-push, fix-up, park and conflict-resolution shapes, the verify-only merge-candidate result (merge_candidate_verified), plus the acceptance runner’s scenario-corpus reports (acceptance_scenarios_pushed, acceptance_scenario_retirement_dropped), and the acceptance verdict (POST /v0/runs/{run_id}/acceptance) may carry a runner-injected replay object alongside the agent’s rows. The shape of each is in the companion docs/api/v0.md.

The merge verb (POST /v0/runs/{run_id}/merge) verifies the merge candidate, not the branch (ADR-090). On a GitHub run it refuses a pull request whose head does not contain the current base tip (409 merge_base_behind) and, when the workflow declares a verify command, a head Fishhawk wrote (a base advance, a conflict-resolution push, a fan-in integration), or an operator-vouched commit that brings in new base commits, that has no passing verify for exactly that commit (409 merge_candidate_unverified or merge_candidate_verify_failed). A forge or audit read error there fails closed (502 merge_candidate_check_failed). Each refusal names the verb that clears it; the error-code table in the companion document has the details.

Before that check, the merge verb refuses while another run in the same repository has an implement stage dispatched or running whose planned files include a path this pull request changes (409 overlapping_implement_in_flight): merging would move the base under that agent. Runs that are only planned or parked do not block, and neither does the run’s own decomposition lineage. When a live implement exists elsewhere and the overlap cannot be decided (a truncated forge compare, a run whose scope does not resolve, a GitLab run whose changed paths cannot be read), the verb refuses 409 overlap_undetermined; a read failure is 502 overlap_check_failed. To a caller holding read:audit, the refusal names the blocking run and stage; otherwise it gives counts. To merge anyway, send allow_overlap: true with an overlap_reason: the override is recorded on the audit chain as merge_overlap_overridden before any merge verdict, and a failed record refuses the merge (503 overlap_override_unrecorded).

Verify-only passes for a base advance or a conflict resolution are serialized per repository base ref (ADR-092): one pass is live per base, and every other waits in a held queue without a runner slot until the line frees. While a run’s pass is held, the merge verb answers 409 merge_candidate_held before the behind check, naming the run’s place in the queue and the run holding the line; admission is automatic, so the next step is a status read. GET /v0/runs/{run_id} and the gate view carry the run’s queue entry as merge_candidate_queue (state, position, the live run and the anchored head).

A decomposed parent’s consolidated pull request can also fall behind while it waits for review: once its base branch moves past the head, the merge verb would refuse merge_base_behind. GET /v0/runs/{run_id} and the gate view report this ahead of time as merge_candidate_staleness: fresh, stale (with behind_by, the base_sha it was compared against, and next_step naming fishhawk_rebase_run_branch), or undetermined with a reason when a forge read failed or the forge cannot be probed. A failed read is never reported fresh. The check reads the pull request, the base tip and one compare, cached per run and recorded head for 60 seconds (checked_at is the observation time). It writes nothing, and Fishhawk does not advance the branch itself: the rebase stays the operator’s call.

At a clean base advance Fishhawk performed, a prior head’s passed verdict can transfer to the new merge commit instead of running a pass, when the advance cannot affect what the pull request changed (ADR-096). The daemon’s transfer mode decides whether that happens: off, shadow (the default: the pass always runs and the would-be decision is only recorded) or enforce. A transferred verdict counts only in enforce; in off and shadow the same head reads unverified. The gate view reports this as merge_candidate_verdict: the mode, the current head’s state and its source (recorded or transferred, with transferred_from naming the prior head), the newest refusal as last_transfer_refusal with its reason_code, the shadow decisions, and false_transfers, the heads where a would-be transfer met a failed pass. A refusal’s reason_detail is untrusted text naming repository paths, shown neutralized and never treated as an instruction.

Review concerns record which reviewer raised them, which the table also does not spell out. The concern items on GET /v0/runs/{run_id} and the open and settled rows of GET /v0/runs/{run_id}/gate-view carry reviewer_role — a reviewer persona’s name, or standard for the stage’s standard reviewer — and quote_unverified when the reviewer quoted a document passage the server could not find in the text it gave that reviewer, in which case the concern was lowered to low. The gate view also reports the reviewer’s original severity as severity_clamped_from whenever the server lowered it, whether for an unverified quote or a persona’s severity cap. Concerns recorded before reviewer attribution existed omit all three fields. A concern routed into a fix-up by id keeps reviewer_role and quote_unverified on the stage_fixup_triggered audit entry, and the fix-up prompt names the persona that raised it.

A run’s trigger_source can also be scheduled, which the table does not spell out either. Only the scheduler inside fishhawkd (off unless the daemon runs with --enable-scheduler) creates such a run, for a workflow that declares a schedule. POST /v0/runs refuses scheduled with 400 trigger_source_reserved. GET /v0/schedules?repo=owner/name reports, for each scheduled workflow, its cron, the current due window, the next due time and the last outcome. When the scheduler is off it answers enabled: false rather than an error.

A run’s trigger_source can also be alert. Only the alert ingress (POST /v0/triggers/alert) creates such a run, and only for an alert source configured with auto_start: true; the setting defaults to false. The ingress authenticates each source by an HMAC signature over the timestamp and body, not by a bearer token. It files one incident issue per alert fingerprint and comments on that issue for each repeat. An alert run is always anchored on the issue the ingress filed, and applies_to routes it as the diff trigger form. POST /v0/runs refuses alert with 400 trigger_source_reserved.

The stage prompt responses (GET /v0/stages/{stage_id}/prompt and its prompt-render twin) carry a gate_container object when the run’s workflow-v2 spec declares one, which the table does not spell out either. It holds exactly one source, image or dockerfile plus context, and a source of stage or workflow naming the level that declared it. The server picks the stage’s own block over the workflow’s and resolves the stage by its position among stages of the same type, so two implement stages can run their gates in different images. The runner applies its image policy, pull and build to that declaration. The object is absent when nothing is declared.

The host-dispatch spawn marker (POST /v0/runs/{run_id}/stages/{stage_id}/host-dispatch) also holds a stage that sits behind a deploy, which the table does not spell out. A pending or awaiting_host_dispatch stage sequenced after a deploy stage that has not succeeded (still awaiting approval, in flight, failed or cancelled) is refused with 409 dispatch_not_admissible, details.reason deploy_not_succeeded and the deploy stage’s id and state, and the stage keeps its state. It is the same rule the server applies when it advances a run on its own: a post-deploy acceptance stage runs only once every earlier deploy stage succeeded.

The same marker can also hold new dispatches while the host is short of memory, which the table does not spell out either. The hold is off by default and interim (the planned adaptive concurrency controller replaces it): fishhawkd serve --enable-ops-monitor --ops-monitor-dispatch-hold on a local deployment turns it on. While the ops monitor reads critical memory pressure, a pending or awaiting_host_dispatch stage is refused with 409 host_memory_pressure, details.reason and details.retry, and the stage keeps its state. A stage that is already dispatched keeps its idempotent 200, and nothing in flight is stopped. Re-dispatch once the condition clears.

Generated from the canonical sources by scripts/gen-site-reference; do not edit between the markers. Description-only edits to a source are not diffed — the delta tables below compare shape (type, requiredness, enum members, default).

See also: Driving a run — the operator loop these reference surfaces serve. This page is the field reference, not a restatement of that guide.

The v0 REST API exposes 181 operations across the paths below, generated from docs/api/v0.openapi.yaml. That document is the source of truth; this table is its published rendering.

Method Path Summary
POST /mcp MCP streamable-HTTP endpoint (client-to-server messages)
GET /mcp MCP standalone event stream (not offered)
DELETE /mcp MCP session termination (not offered)
GET /.well-known/oauth-authorization-server OAuth 2.1 authorization-server metadata (RFC 8414)
GET /.well-known/oauth-protected-resource RFC 9728 protected-resource metadata (bare well-known path)
GET /.well-known/oauth-protected-resource/{resource_path} RFC 9728 protected-resource metadata (RFC 9728 §3.1 path-suffixed)
GET /v0/oauth/authorize OAuth 2.1 authorization endpoint (consent)
POST /v0/oauth/authorize OAuth 2.1 authorization endpoint (consent decision)
POST /v0/oauth/token OAuth 2.1 token endpoint
GET /healthz Liveness probe
GET /v0/auth/github/login Begin GitHub OAuth sign-in
GET /v0/auth/github/callback GitHub OAuth callback
GET /v0/auth/gitlab/login Begin GitLab OAuth sign-in
GET /v0/auth/gitlab/callback GitLab OAuth callback
GET /access-denied Membership-gate denial page
GET /v0/auth/github/manifest-flow-start Begin GitHub App manifest registration
GET /v0/auth/github/manifest-callback Receive GitHub App manifest-flow callback
GET /v0/auth/me Current user
POST /v0/auth/logout Sign out
GET /v0/onboarding/readiness First-run readiness for a repository
GET /v0/onboarding/start Cell entry point for a directory-routed caller
GET /v0/tokens List API tokens for the current user
POST /v0/tokens Mint a new scoped API token
GET /v0/tokens/login OAuth token-login discovery
POST /v0/tokens/login Mint a user-bound token from an OAuth device-flow access token
DELETE /v0/tokens/{token_id} Revoke an API token
GET /v0/runs List workflow runs
POST /v0/runs Create a workflow run
GET /v0/runs/{run_id} Get a run
POST /v0/runs/{run_id}/cancel Cancel a run
POST /v0/runs/{run_id}/consolidate Run the decomposed-parent fan-in (E24.2) on demand
POST /v0/runs/{run_id}/integrate-wave Non-settling per-wave fan-in of a decomposed parent (E24.X /
POST /v0/runs/{run_id}/recover Recover a category-B-failed run against its approved plan
POST /v0/runs/{run_id}/redrive Re-drive a failed decomposition child run
POST /v0/runs/{run_id}/revive Revive a terminal-failed run (one-verb batch re-park)
POST /v0/runs/{run_id}/reset-branch Force-reset a run branch off a foreign on-top commit
POST /v0/runs/{run_id}/rebase-branch Advance a run branch onto its declared base (operator-gated)
POST /v0/runs/{run_id}/vouch-commit Vouch a foreign commit as run-authored lineage
POST /v0/runs/{run_id}/retrigger-ci Re-run a run PR’s failed CI at the current head
POST /v0/runs/{run_id}/merge Record an operator merge verdict and queue the squash merge (skipped when already merged)
POST /v0/runs/{run_id}/grooming-dispositions Record per-entry grooming dispositions (operator-only)
GET /v0/runs/{run_id}/grooming-dispositions Read back the recorded grooming dispositions for a run
POST /v0/runs/{run_id}/upkeep-dispositions Record per-finding upkeep dispositions (captain-only)
GET /v0/runs/{run_id}/upkeep-dispositions Read back the recorded upkeep dispositions for a run
POST /v0/runs/{run_id}/comms-dispositions Record per-draft comms dispositions (captain-only)
GET /v0/runs/{run_id}/comms-dispositions Read the comms dispositions view for a run
POST /v0/runs/{run_id}/acceptance-arbitration Record the operator arbitration that discharges a paged acceptance triage
POST /v0/runs/{run_id}/auto-drive Drive the run’s parked gate under ADR-040 delegation
POST /v0/runs/{run_id}/auto-drive/acts Record a driver stage dispatch (record-before-dispatch)
GET /v0/runs/{run_id}/stages List stages for a run
GET /v0/runs/{run_id}/stages/{stage_id} Read one stage by its (run, stage) handle, with optional terminal-wait long-poll
POST /v0/runs/{run_id}/reviews/reconcile Terminate a review round orphaned by a fishhawkd restart
POST /v0/runs/{run_id}/reviews/rerun Re-run a verdict-less implement-review round, or start a decomposed parent’s first one
POST /v0/runs/{run_id}/stages/{stage_id}/reap-failure Report a runner failure so a stranded ‘dispatched’ or ‘running’ stage fails
POST /v0/runs/{run_id}/stages/{stage_id}/host-dispatch Mark a host-local spawn attempt so a parked stage becomes ‘dispatched’
POST /v0/runs/{run_id}/stages/{stage_id}/progress Record a stage’s mid-execution progress heartbeat
GET /v0/runs/{run_id}/audit List audit entries for a run
GET /v0/runs/{run_id}/gate-view Gate-scoped decision view for a run
GET /v0/runs/{run_id}/scope-overlap Live cross-run scope overlap for a run
GET /v0/runs/{run_id}/campaign-pending-amendments Pending scope amendments on every run of this run’s campaign
GET /v0/runs/{run_id}/budget Current periodic-budget status for a run’s workflow
GET /v0/runs/{run_id}/cache-efficiency Per-run prompt-cache efficiency metric
GET /v0/runs/{run_id}/cost Per-run estimated cost with per-stage and per-merged-PR rollup
GET /v0/runs/{run_id}/latency Per-run gate-latency (wait-on-human) rollup
GET /v0/runs/{run_id}/diagnostics Product-facts-only diagnostic bundle for a run
POST /v0/runs/{run_id}/product-reports File a deduped, audited upstream product report for a run
GET /v0/runs/{run_id}/status-comment Get rendered sticky-comment body and stored GitHub comment ID
POST /v0/runs/{run_id}/status-comment Record the GitHub comment ID after posting the sticky comment
GET /v0/audit Search the cross-chain audit log
GET /v0/attention Cross-run attention queue (“Needs You”)
GET /v0/restart-blockers Work a daemon restart would orphan right now
GET /v0/digest “Since you last looked” digest for one repository
POST /v0/digest/mark-read Advance the caller’s digest read watermark
GET /v0/handover-brief Handover brief for one repository
GET /v0/schedules Scheduled workflows and their due windows for one repository
POST /v0/triggers/alert HMAC-authenticated alert ingress — file (or comment on) an incident issue
GET /v0/captain The captain record for one repository
POST /v0/captain/offer Offer the captain seat to a named successor
POST /v0/captain/withdraw Withdraw a pending handover offer
POST /v0/captain/accept Accept a pending handover offer
POST /v0/captain/relinquish Relinquish the captain seat
POST /v0/captain/claim Claim a vacant captain seat
POST /v0/crew-messages Send a crew message
GET /v0/crew-messages List crew messages by recipient or by anchor
GET /v0/crew-messages/{sequence} Read one crew message in prompt-rendered form
POST /v0/crew-messages/{sequence}/respond Answer a crew message (refused over HTTP)
POST /v0/crew-messages/{sequence}/escalation-decision The captain’s decision on a crew escalation
POST /v0/crew-messages/{sequence}/convert-to-concern The captain converts a crew finding into a review concern
POST /v0/first-officer-seats Attach a first-officer seat to a campaign or run
GET /v0/first-officer-seats Read an anchor’s first-officer seat
POST /v0/first-officer-seats/retire Retire (detach) an anchor’s first-officer seat
POST /v0/specialist-jobs Request a specialist job
GET /v0/specialist-jobs/{job_id} Read one specialist job
POST /v0/specialist-jobs/{job_id}/admission Admit, heartbeat or take over a specialist job
POST /v0/specialist-jobs/{job_id}/result Ship a specialist job’s completion
GET /v0/audit/export Compliance export in the verifier’s Export v1 wire shape
GET /v0/audit/export.csv Flat CSV rendering of the compliance export for spreadsheet workflows
GET /v0/reports/agent-changes Canned compliance report of all agent changes in a date range (JSON)
GET /v0/reports/agent-changes.md Canned compliance report of all agent changes (human-readable markdown)
GET /v0/releases/notes/preview Preview evidence-derived release notes for a ref range
POST /v0/releases/notes Persist evidence-derived release notes as an artifact
POST /v0/releases/cut Record the operator’s ratified release-version decision
POST /v0/releases/publish Publish persisted release notes to a GitHub Release
GET /v0/campaigns List campaigns
POST /v0/campaigns Create a campaign from an epic ref
POST /v0/campaigns/preview Preview a campaign without creating it
GET /v0/campaigns/{campaign_id} Get a campaign
GET /v0/campaigns/{campaign_id}/items List a campaign’s items
POST /v0/campaigns/{campaign_id}/items Append issues to an existing campaign
POST /v0/campaigns/{campaign_id}/hotfix Add an urgent issue to a campaign as an expedited item
GET /v0/campaigns/{campaign_id}/status Get a campaign’s rollup status + next action
POST /v0/campaigns/{campaign_id}/runs Start a run for an eligible campaign item
POST /v0/campaigns/{campaign_id}/resume Resume a paused campaign
POST /v0/campaigns/{campaign_id}/cancel Cancel a campaign
POST /v0/work-items File a work item via the repo’s work-management conventions
POST /v0/work-items/preview Preview a work-item filing — conventions plus intake signals, nothing filed
POST /v0/refinement/sessions Draft an epic/children preview from a brief, optionally for a target repo
GET /v0/refinement/sessions/{session_id} Get a refinement session’s preview + approval state
PATCH /v0/refinement/sessions/{session_id}/draft Edit a refinement draft (agent amendment or direct field edit)
POST /v0/refinement/sessions/{session_id}/decision Approve or reject the latest refinement draft revision
POST /v0/refinement/sessions/{session_id}/file File an approved refinement draft into tracker items
GET /v0/calibration Get runtime calibration statistics
GET /v0/precedent Query prior decisions of the same kind (precedent)
GET /v0/repos/{owner}/{name}/throughput Get a repository’s throughput rollup
GET /v0/repos/{owner}/{name}/health Get a repository’s health rollup
GET /v0/repos/{owner}/{name}/economics Get a repository’s economics rollup
GET /v0/repos/{owner}/{name}/posture Get a repository’s workflow posture
GET /v0/repos/{owner}/{name}/delegation Get a repository’s per-workflow delegation posture
GET /v0/repos/{owner}/{name}/delegation/confirmation Which workflows’ delegation the captain has confirmed since the latest handover
POST /v0/repos/{owner}/{name}/delegation/confirm The sitting captain confirms one workflow’s delegation exactly as shown
POST /v0/repos/{owner}/{name}/delegation/lower The sitting captain proposes a strictly lower delegation for one workflow
GET /v0/repos/{owner}/{name}/merge-queue Get a repository’s per-base merge queue
GET /v0/acceptance-triage/stats Get acceptance-triage statistics
GET /v0/stages/{stage_id} Get a stage
GET /v0/stages/{stage_id}/artifacts List artifacts for a stage
GET /v0/stages/{stage_id}/prompt Fetch the constructed prompt for a stage
GET /v0/stages/{stage_id}/prompt-render Fetch the constructed prompt for a stage (SPA-readable)
GET /v0/stages/{stage_id}/trace Stream the redacted trace bundle for a stage
GET /v0/stages/{stage_id}/checks Latest state per blocking check on a stage
POST /v0/stages/{stage_id}/approvals Approve or reject the gate on a stage
POST /v0/stages/{stage_id}/clarification Answer a plan stage parked at awaiting_input
POST /v0/stages/{stage_id}/revise Re-plan a plan stage in place against a binding operator constraint
POST /v0/stages/{stage_id}/retry Retry a failed stage
POST /v0/stages/{stage_id}/acceptance-admission Pre-spawn acceptance short-circuit admission
POST /v0/stages/{stage_id}/fixup Route advisory implement-review concerns back to the agent
POST /v0/runs/{run_id}/merge-queue/promote Promote a held merge-queue entry to the head of its base’s line
POST /v0/stages/{stage_id}/concurrency/promote Promote a queued waiter to the front of its concurrency group
POST /v0/concerns/{concern_id}/waive Waive an open review concern with an audited reason
GET /v0/runs/{run_id}/concerns List a run’s review concerns, optionally with its decomposition children’s
POST /v0/runs/{run_id}/concerns/waive Waive several of one run’s open concerns with one audited reason
POST /v0/runs/{run_id}/concerns/resolve Resolve routed concerns as addressed on the operator’s evidence (human-only)
POST /v0/runs/{run_id}/divergence/{sequence}/answer Answer a divergence question — one-off, or a change of doctrine
POST /v0/concerns/{concern_id}/defer Defer an open review concern into a follow-up work item
GET /v0/artifacts/{artifact_id} Get an artifact
POST /v0/runs/{run_id}/signing-key Issue a per-run Ed25519 signing key
POST /v0/runs/{run_id}/trace Upload a signed trace bundle
POST /v0/runs/{run_id}/plan Upload a plan-stage artifact (plan or a discriminated sibling)
POST /v0/runs/{run_id}/pull-request Upload a pull-request artifact for an implement stage
POST /v0/runs/{run_id}/deployment Record a deployment artifact for a deploy stage
POST /v0/runs/{run_id}/acceptance Record an acceptance-evidence artifact for an acceptance stage
POST /v0/runs/{run_id}/acceptance/transcript Record an acceptance transcript artifact for an acceptance stage
POST /v0/runs/{run_id}/deployment/rollback Re-dispatch a delegating deploy’s rollback path
POST /v0/runs/{run_id}/installation-token Mint a GitHub App installation token for the run’s repo
POST /v0/runs/{run_id}/mcp-token Mint a short-lived MCP bearer token for the run
POST /v0/runs/{run_id}/scope-amendments Request a mid-stage scope amendment (implement agent)
GET /v0/runs/{run_id}/scope-amendments List a run’s scope amendments
POST /v0/runs/{run_id}/scope-amendments/{amendment_id}/decision Approve or deny a scope amendment (operator)
POST /v0/runs/{run_id}/scope-completeness/decision Exempt, amend or fail a parked scope-completeness shortfall (operator)
POST /v0/runs/{run_id}/reconcile-merge Supersede the stages a merge left parked and complete the run (operator)
POST /v0/runs/{run_id}/record-merge-observation Read the run’s pull request off the forge and record a merge observation (operator)
POST /webhooks/github GitHub App webhook receiver
POST /webhooks/gitlab GitLab webhook receiver
GET /v0/dev/fixtures List the seeded fixture scenarios (dev only)
POST /v0/dev/fixtures Materialize a seeded fixture scenario (dev only)
POST /v0/dev/sign Sign a raw body with a run’s Ed25519 private key (dev only)
GET /v0/dev/forge Snapshot the in-process stub forge (dev only)
DELETE /v0/dev/forge Reset the in-process stub forge (dev only)
GET /v0/dev/forge/issues Read one stub forge issue (dev only)
POST /v0/dev/forge/issues Seed one stub forge issue (dev only)
POST /v0/dev/forge/pulls Seed one stub forge pull / merge request (dev only)
POST /v0/dev/forge/deliveries Sign and dispatch a webhook delivery in-process (dev only)