Server-authored actions

Every single-issue read, project overview, workspace representation, and transition response carries two additive keys, as does every claim, release, and pull-request link:

  • actions — the operations that are legal right now, for you, with every input pre-filled or described;
  • blocked — the operations that are not, each with a stable reason_code and a human-readable message.

Clients render what they receive. No client holds a copy of the lifecycle, so when the rules change on the server, every client follows without an update.

Execute an action

Each action names its HTTP method and resolved same-origin href, and declares its fields:

Field kindWhat you do
constantEcho the server’s value verbatim (for example expected_status)
textAuthor content within the stated bounds
selectChoose one of the enumerated options
client_idGenerate a stable retry token and keep it for retries

That table is the entire out-of-band contract. Send method to href with a JSON body containing every field, and the request is exactly what the server expects — the compare-and-set token and any linkage IDs ride along as constants, so you never compose one.

fp2 issue show --id <issue-reference> --json | jq '.actions[].name, .blocked'

Blocked reasons

reason_codeMeaning
illegal_edgeThe status graph has no edge from the current status
terminal_statusThe issue is done or cancelled
open_childrenClosing requires every child done or cancelled first
open_questionsClosing requires every question answered first
held_by_otherSomebody else holds a live claim; the message names them
claim_requiredNobody holds it and you asked to start work; claim it first
owner_onlyThe membership operation needs an organization owner

Treat the codes as stable and the messages as presentation.

held_by_other outranks the other reasons on a transition: somebody who may not act at all is not helped by being told the issue has open children.

claim_required is distinct from held_by_other because the repair differs: claim the issue and proceed, rather than wait or take the lapsed lease. It appears on transition:in_progress alone — both edges into it, since resuming rejected work is still picking it up.

owner_only appears on the four membership actions alone, never on an issue, and unlike every other reason here it is not a state you can wait out or repair. Every agent principal receives it, because an agent is never an organization owner.

Errors carry the fix

Rendering is a hint; the handler is the enforcement. A transition the representation never offered — or one built from a stale read — is rejected with a typed conflict whose body includes the current legal actions: recover from the error body directly. That holds for the five transition conflicts today. Every other mutation conflict — a rejected comment kind, an invalid resolves, a child under a terminal parent, a reused client_id, issue_held, issue_not_held — returns a stable error code, a message, and executable repair commands; re-read the issue to obtain a fresh action set.

Rules of engagement

  • Act only on advertised actions; never construct a mutation from memory.
  • Use the returned action set (or re-read the issue) after every write.
  • Choose and persist your client_id before the first send, and reuse it verbatim to retry — for a create, a comment, or a transition the server replays the original result instead of duplicating the write. claim and release require the field but keep no ledger for it: an immediate re-send is safe because a re-claim succeeds and a release of what you do not hold is a no-op, while a duplicate arriving after the state moved on takes real effect. Re-read the issue rather than sending a stale retry.

Action sets are principal-aware, and by identity rather than by kind: an agent and a human reading the same issue receive different comment kinds, only humans receive answer actions, and release renders only for the principal that actually holds the issue. If an option is not offered to you, the server will also refuse it.