Issue hierarchy

One nullable relation carries epics, tasks, and subtasks: an issue may name a parent, and issues nest arbitrarily. There is no separate epic type.

Create a child

fp2 issue create \
--title "<short-title>" \
--parent <issue-reference> \
--client-id "<stable-retry-id>" \
--json

The server resolves the reference within the selected project and stores the full canonical ID. A parent is assigned only when the child is created — there is no re-parenting — so cycles are structurally impossible. Creating a child under a done or cancelled parent is a typed conflict.

When you hold a parent’s representation, its advertised create_child action carries the parent as a pre-filled constant, so you never type the ID.

Read the hierarchy

fp2 issue show returns:

  • parent — reference, title, and status, or null;
  • children — a bounded list of the most recently updated children;
  • children_total and children_open — the authoritative counts;
  • children_in_progress — how many of the open ones are underway;
  • children_held and children_stalled — how many open children somebody is holding right now, and how many were claimed, moved to in_progress, and then left for the lease to run out.

State ratios and completeness from the counts, never from the bounded list. The same five counts ride every row of a project’s top-level slice, which is how an overview says that work is happening one level below it: the children themselves are not in that payload.

children_held and children_stalled are the server’s comparison against its own clock at the moment of the read. Every other lease reading — held_until on a holder — is yours to make, and a client whose clock disagrees with the Date header should decline to make it rather than state a countdown it cannot justify.

The close gate

A parent cannot transition to done while any child is open (todo, in_progress, or review):

  • compliant clients simply do not see the done action; the blocked entry explains with reason_code: "open_children" and the count;
  • a constructed or raced attempt is rejected with a typed issue_children_open conflict carrying the count and the currently legal actions;
  • the check is atomic with the status write, so a child created concurrently cannot slip under a closing parent.

Cancellation is deliberately not gated: abandoning a parent asserts nothing about its children’s records. Close or cancel the children, re-read the parent, and done reappears.

Retries are safe throughout: a child create replayed with its original client_id returns the original child even if the parent has since closed.