Issues and the timeline
fp2’s issue workflow is durable capture, server-authoritative status transitions, a typed timeline, opt-in exclusivity through claims, and handoff. It does not compute readiness.
Create an issue
Choose a stable retry ID before the first request:
The server creates the canonical 32-letter issue ID and derives its short
FP-xxxxxxxx display reference.
Retrying the same client_id and identical payload returns the original
issue. Reusing that ID with a changed payload is a conflict.
To create a child under an existing issue, add --parent <issue-reference>.
One relation carries epics, tasks, and subtasks; see
Issue hierarchy for the rules and the close gate.
Inspect work
Short references are project-scoped. If one is ambiguous, list issues and
retry with more ID characters. issue show returns the issue, its parent and
children, the full timeline, its linked pull requests, and the
server-authored actions that are legal right now.
Search a project
Search is an API route; there is no CLI subcommand for it, so calling it needs a
key in FP2_API_KEY. A credential held in the CLI config under a profile cannot
reach this route, and reading the stored key out of the config to get around that
is not a supported path.
It is also the one route no representation advertises: nothing hands you a search
href, so the URL below is assembled from a project id rather than followed. That
is a gap in the affordance surface rather than a pattern to copy — everywhere
else, take the href the server gave you.
Encode the query rather than pasting it into the URL: spaces, &, #, and %
are ordinary characters in an issue title, and each one truncates or corrupts a
hand-built query string.
It matches issue titles and bodies. Comments are not searched, and
scope.fields reports which fields were read rather than leaving a client to
imply it searched everything. q is a substring match of up to 200 characters,
not a set of words: a two-word query finds only the issues that contain those two
words adjacently.
status is a filter, and omitting it searches every status — the opposite of
fp2 issue list, which shows open issues until you name one. A search that
returns a done issue is behaving correctly.
There is no cursor: results is a slice and the counts are the truth about it.
Search rows leave holder null, because a result is a way to find an issue
rather than a claim about who is on it.
Record typed timeline events
Every comment is an immutable, attributed timeline event. Type yours:
Omitting --kind records an untyped legacy note. The server rejects kinds
that do not belong to your principal; post only the options the issue
representation advertises. There is no edit and no delete — correct a wrong
event with another event.
Questions gate closure
Posting a question withholds the issue’s done transition until the
sponsoring human resolves it:
The human’s representation advertises one answer action per unresolved
question with resolves pre-filled; the console renders the same composer
inline. Agents cannot answer. Each question accepts exactly one resolving
answer. While waiting, keep working other issues and re-read this one to see
whether the answer arrived — non-done transitions stay legal.
Transition issue status
Move status with an explicit retry-safe command. Starting work takes the claim, so a start is two commands:
Every other target is one:
The server enforces:
todo → in_progress | cancelledin_progress → todo | review | cancelledreview → in_progress | done | cancelleddoneandcancelledare terminaldoneadditionally requires no open children and no unresolved questions- either edge into
in_progressadditionally requires that you hold the issue
The CLI reads the current issue status and the server uses it as an atomic
compare-and-set precondition. A concurrent update returns
issue_transition_conflict; an illegal edge returns
invalid_issue_transition; a gated close returns issue_children_open or
issue_questions_open with the count and the currently legal actions; a start
on an issue you do not hold returns issue_claim_required, whose body carries
the claim action that fixes it. Retrying the exact command returns the
original attributed transition.
Claim an issue
Both commands carry nothing but the retry key. The holder comes from the credential, so there is no way to claim on another principal’s behalf and no flag inviting the attempt.
A claim is a lease, not a lock. holder carries held_until, 30 minutes out,
and the writes its holder authors on the issue push it further out — today a
comment, a transition, giving it a child, or re-claiming it, which the
compare-and-set admits so that an agent which keeps working keeps its claim.
Linking a pull request does not extend it. There is no heartbeat endpoint
to remember. The expiry is on the wire so
that an agent returning from a long tool call can see whether it still holds what
it thinks it holds. A lapsed holder keeps being reported until somebody claims
the issue; the new claim then overwrites it, and nothing records who was
displaced.
Exclusivity is otherwise opt-in, and gating is otherwise conditional on a claim existing:
- apart from starting work, an issue nobody holds may be transitioned, given children, or given a pull request by anyone, exactly as before claims existed;
- an issue somebody else holds withholds those actions and blocks them with
held_by_other, naming the holder and the expiry; - commenting stays open to everyone, because a human answering a question on an issue an agent holds is the case that matters most.
Claiming an issue somebody else holds returns issue_held, which carries the
holder and is retryable — a lease expires, so waiting is a real strategy here,
unlike every other conflict in this API.
issue_not_held refuses a release for one reason only: somebody else holds a
live claim. Releasing an issue nobody holds, or whose lease has lapsed, is a
successful no-op, because release means “ensure I am not holding this” and that
is already true.
Both verbs require client_id and neither keeps a ledger for it, so neither replays. An immediate
re-send after a lost response is safe — a re-claim succeeds and refreshes the lease, and a release
of something you do not hold is a no-op. A duplicate that arrives late is a real write: a claim
landing after the holder released re-acquires the issue, and a release landing after a later claim
drops the newer lease. Re-read the issue instead of sending a stale retry.
Link the pull request
github.com only, and fully qualified: nothing in fp2 knows which repository a
project belongs to, so the server parses the owner, repository, and number out
of the URL rather than inferring any of them. There is no --client-id,
because linking one pull request to one issue is idempotent by identity; a
re-link corrects a stale title.
No pull-request state is stored — not draft, not ready, not merged. A
caller-supplied state word is written once and has no path to update, so it
would say draft forever; the state word arrives with a webhook mirror.
Links cannot be removed either, and the action says so with
reversible: false. Linking never gates a transition.
Recover context
At the start of each work session:
The response includes the resolved project, compact open-issue summaries, an
optional active issue with its timeline and actions, and executable suggested
operations. It is orientation, not a readiness signal; holder is what says
whether somebody is on an issue.
Do not invent ready, report, plan, blocker, close, label, criteria, or orchestration
commands. They are not shipped. Parents exist only at creation time; there is no re-parenting, and
a pull-request link cannot be removed.