---
satisfies: [R1, R3, R4]
---
## Goal
Extend `references/jira.md` with the Jira-specific weight: Markdown↔ADF translation, the workflow-aware status model (transitions API + statusMap + fn-66 terminal invariant), the relation pair (is-blocked-by links), and `listOpenIssues` (JQL via `/search/jql`). (Spec R1-relation+ninth, R3, R4.) [dep: fn-70.2 — same file.]

## Files
- `plugins/flow-next/skills/flow-next-tracker-sync/references/jira.md` (extend the fn-70.2 doc).
- `plugins/flow-next/skills/flow-next-tracker-sync/references/status-sync.md` — **edit**: add the Jira readiness-projection branch (fn-58 DESIRED derivation + stale-config existence check, mirroring fn-69's GitLab branch).
- Reads: `status-sync.md` (fn-66 terminal invariant), `adapter-interface.md` (relation + comment structs, incl. the now-mandatory `linkPresent` field).

## Approach — endpoints verified live 2026-06-28 (`~/work/agent-scripts/flow-smoke/out/FINDINGS-jira.md`)
- **ADF translation**: Markdown ↔ ADF (`{"type":"doc","version":1,"content":[...]}` — round-trips exactly on the supported subset) at the boundary, round-trip-safe over a DOCUMENTED subset. **Unknown-node preservation algorithm:** on a body write, **fetch the current ADF first**, splice the new content only into regions mapping to the supported markdown subset, and **carry unknown/unsupported ADF nodes through verbatim** — NEVER blind-regenerate ADF from markdown (drops them). Reconcile stays transport-blind.
- **Status / transitions**: `GET /issue/{issueIdOrKey}/transitions` returns ids valid **from the current status** → `POST /issue/{issueIdOrKey}/transitions {transition:{id}}` (HTTP 204; prefer the durable `tracker.id`). Never a direct status set. **Terminal detection = `status.statusCategory.key=="done"`** (NOT the status name). `statusMap` = **normalized status → an object `{"name": "In Review"}` or `{"id": "10031"}`** (`id` preferred over `name` — names are renamable), the FULL normalized set (`planned`/`in-progress`/`in-review`/`done`/`verified`/…), not just ready/done; a normalized status with **no entry → defer + receipt**. **A project lacking the target** (e.g. no "In Review") ⇒ **defer + receipt** (deterministic default; "nearest" only via an explicit configured fallback, never inferred). **`readStatus` (read normalization, inverse of the write-map):** reverse-map Jira status → normalized via `statusMap` (`id` first, then `name`); else `statusCategory.key` fallback (`done`→`done`; `new`/`indeterminate`→safe nonterminal only when unambiguous); else unmapped → defer receipt. fn-66 terminal invariant: locally-`done` → In Review until merge; terminal Done gated on `MERGED`; unreachable transition → defer + receipt.
- **Relations**: `listIssueRelations`/`setIssueRelation` via `POST /rest/api/3/issueLink` type `Blocks` (`outwardIssue` **blocks** `inwardIssue` → current = `inwardIssue` "is blocked by", dep = `outwardIssue`). `listIssueRelations` returns each edge with the **mandatory fn-69 field `linkPresent: true`** (Jira native links never orphan — no GitLab-style block/link divergence) + **`source: "unknown"`** (ledger-authoritative). `setIssueRelation`: **read-before-write dedup** (Jira does not reliably no-op a duplicate `POST /issueLink`), additive-only / never-delete-non-ours / **defer-on-collision** — a ledgered edge **missing remotely is a human removal → defer + receipt, NEVER silently re-create** (shared steps.md `projectDepRelations` rule, hardened in fn-69).
- **Readiness projection — additive Jira branch in `status-sync.md` (fn-58; the one carve-out to "status-sync untouched").** Mirror fn-69's GitLab branch: add a **Jira branch** to the readiness DESIRED derivation + the stale-config existence check. `DESIRED = (issue status NAME == tracker.readyState)` — a **name match like Linear** (Jira has workflow states, not labels; case-insensitive/trimmed on the status name, never `statusCategory`); stale-config = the configured `readyState` status still exists in the project ⇒ genuine not-ready, absent ⇒ warn + noop (never mass-un-ready). One-way, tracker-authoritative, change-only receipts. (Distinct from the `listOpenIssues` JQL filter and the ceremony's readyState collection.)
- **listOpenIssues — endpoint by `apiVersion`**: **Cloud v3** = **`POST /rest/api/3/search/jql`** (legacy `GET /search` **removed**, HTTP 410) + **cursor pagination loop** (pass the returned `nextPageToken` in the next body until `isLast: true`; bounded max + truncation receipt note); **DC/Server v2** = `/rest/api/2/search` — endpoint + pagination (likely **offset** `startAt`/`maxResults`) are **verify-at-build** (Cloud 410 evidence doesn't prove DC) — **DC is implemented, NOT deferred** (R1/R9 require all nine for DC). Cloud body `{jql:"project = <projectKey> AND status = \"<readyState>\"", fields, maxResults}`, where `readyState` is a **raw Jira status name used directly in the JQL** (NOT via `statusMap` — the tracker-side promoted-lane value, like Linear/GitHub), no-op when unset; normalized structs (ADF descriptions translated). **JQL safety:** escape quotes/backslashes in `readyState` before interpolating into `status = "<readyState>"`; validate `projectKey` against the Jira key grammar (no JQL injection). REST-only (no MCP).
- **Endpoint family by deployment:** all paths above are **Cloud `/rest/api/3`** (live-validated 2026-06-28); **DC/Server uses `/rest/api/2`** and may differ in body format — branch on the persisted `apiVersion`/`authScheme`; **flag the DC/Server endpoint set + body format as verify-at-build** (no DC instance was available to validate). **Unambiguous (rp-review risk): IMPLEMENT `/rest/api/2/search` + the DC paths NOW** from Atlassian's published DC REST docs — "verify-at-build" means *confirm the exact endpoint / pagination / body shape against those docs (+ a live DC smoke IF an instance is available)*, NOT defer the code. `listComments` uses Jira **offset pagination** (`startAt`/`maxResults`/`total` on the Cloud + DC comment endpoints) — bounded read with a truncation receipt note.

## Acceptance
- ADF round-trip subset documented + unknown-node preservation (R3).
- Transitions API (per-current-state ids) + statusMap + `statusCategory` terminal signal + fn-66 invariant + defer-on-unreachable + missing-status tolerance (R4).
- Relation pair via `POST /issueLink` `Blocks`, each edge returning the mandatory **`linkPresent: true`** + `source: "unknown"`; read-before-write dedup + **defer-on-human-removal-collision** (never silently re-create) (R1-relation).
- **Jira readiness-projection branch added to `status-sync.md`** — name-match like Linear (DESIRED = status name == `readyState`) + stale-config existence check; additive, mirrors fn-69's GitLab branch (R4).
- `listOpenIssues` by `apiVersion`: **Cloud v3 `POST /search/jql` + cursor**; **DC/Server v2 `/rest/api/2/search` implemented, verify-at-build (NOT deferred — R1/R9)**; exact `readyState` (raw Jira name, JQL-escaped), no-op when unset (R1-ninth).

## Test notes
- Reference doc. ADF round-trip + status model are prose contracts; mirror/parity in fn-70.4. Endpoints already smoke-tested live.

## Description
TBD

## Done summary
Extended `references/jira.md` (+620) with the Jira-specific weight + added the status-sync readiness Jira branch (+17). **ADF translation** — Markdown↔ADF round-trip-safe subset; the WRITE path is fetch-current-ADF-first → splice only the supported-subset regions → carry unknown nodes + the `<!-- flow:deps -->` block forward verbatim (never blind-regenerate); DC/Server v2 body format flagged verify-at-build. **Status** via the transitions API (never a direct set): statusMap full normalized set ({id|name}, id preferred), terminal = `statusCategory.key=="done"` (not the renamable name), readStatus reverse-map, fn-66 invariant (locally-done→In Review until MERGED), no-entry/unreachable ⇒ defer+receipt never force. **Relations** via native `Blocks` issue links: listIssueRelations returns `linkPresent:true`/`source:"unknown"` (native links never orphan — ledger is provenance authority); setIssueRelation read-before-write dedup + additive-only + ledger-says-projected-but-link-absent ⇒ human-removal defer (never re-create). **listOpenIssues** by apiVersion: Cloud v3 `POST /search/jql` + cursor (nextPageToken until isLast), DC v2 `/rest/api/2/search` + offset (implemented, verify-at-build); JQL injection guards (projectKey grammar + escape order). **status-sync.md**: additive Jira readiness-projection branch (DESIRED = status NAME match like Linear + stale-config existence check); who-wins untouched.

Recovered from a lost worker process (prior Claude Code process exited mid-run): the worker had committed all work (f283acb7) before dying but not run `flowctl done`. Host verified: full tracker gate green (203 tests), `.flow/bin/flowctl.py` byte-identical to canonical, and every deliverable present + correctly structured (ADF fetch-first-splice, statusCategory terminal, linkPresent + human-removal-defer, /search/jql cursor + DC offset, JQL guards, status-sync Jira readiness branch). rp impl-review triage-skips (docs-only diff — jira.md + status-sync.md prose, same fast-path as .2); the thorough adversarial review lands at the PR resolve-pr loop (as gitlab.md got in fn-69).
## Evidence
- Commits: f283acb7
- Tests: python3 -m unittest discover -s plugins/flow-next/tests -p 'test_tracker*.py' → 203 tests OK; cmp canonical==mirror flowctl.py byte-identical
- PRs: