# Tracker sync

Source: https://flow-next.dev/integrations/tracker-sync/

Project a spec onto a Linear, GitHub, GitLab, or Jira issue and reconcile body, status, and comments two-way. Positioning, the discovery ceremony, and the three ways in.

`/flow-next:tracker-sync` projects a Flow-Next spec onto a tracker issue (Linear, GitHub, GitLab, or Jira) and reconciles body, status, and comments two-way. The skill owns discovery choices, semantic body and comment composition, 3-way merge conflict adjudication, and recovery choices. `flowctl tracker` owns provider transport and mutations; `flowctl sync` owns local bridge state, receipts, and audits.

`/flow-next:sync` is **plan-sync** - it updates downstream *task* specs after implementation drift inside Flow-Next (see the [Sync skill](https://flow-next.dev/skills/sync/)). `/flow-next:tracker-sync` is the **external tracker bridge** documented here. The two share a verb and nothing else.

Flow-Next runs fully without this. It costs a bidirectional round-trip per lifecycle event you enable, plus a conflict policy to hold an opinion about and a second place state can be wrong. Turn it on when other people need to read or edit status where they already work, or run `/flow-next:tracker-sync` manually and leave the bridge off in between. Spec-only is a first-class mode, not a degraded one.

## Projection, not coordination

The `.flow/specs/<id>.md` spec is the **single source of truth** and the quality layer. The tracker is a **co-editable mirror** for teams that must live in it. The bridge is **projection**, not **coordination**:

* The tracker **mirrors** the spec. Body, status, and comments all sync **two-way** - a vague PM-authored issue can be pulled in, fleshed out in Flow-Next, and synced back.
* A push never silently overwrites a tracker-side edit (6.1.0+). When the issue body changed since the last sync, an attended run asks whether to reconcile or overwrite, and an unattended run queues the decision ([Edits made on the tracker](https://flow-next.dev/integrations/tracker-operations/#edits-made-on-the-tracker)).
* The tracker **never drives flow state or spawns agents**. There is no board-status-flips-fire-an-agent control plane. The spec stays where work is authored, enriched, and executed.

“Not coordination” means the tracker is not a control plane - it does **not** mean one-way. The contrast is OpenAI Symphony, where Linear is the canonical finite-state machine that spawns agents off a thin per-issue `WORKFLOW.md`. Flow-Next’s pitch is “Symphony, but with real specs, re-anchoring, and receipts” - the spec carries the weight; the tracker is a downstream window. A Symphony-style board-triggered per-spec executor is a **separable future addition**, explicitly out of scope here.

```mermaid
flowchart LR
  Spec[".flow/specs/<id>.md\nsource of truth"] -->|projection| Issue["Tracker issue\nco-editable mirror"]
  Issue -->|reconcile body / status / comments| Spec
  Issue -. never drives .-> Agent["Agent loop"]
  Spec --> Agent
```

## Setup: the discovery ceremony

**Configuring the bridge is its own one-time step, separate from [`/flow-next:setup`](https://flow-next.dev/skills/setup/).** Setup creates `.flow/` and project docs and **never touches tracker config** - that keeps the zero-dep base install clean for the (many) users who run no project-management software. The bridge is set up by running **`/flow-next:tracker-sync`**, whose discovery ceremony writes the config. When `/flow-next:setup` finishes it **proposes running `/flow-next:tracker-sync` as an optional next step**, so the bridge is discoverable without being imposed.

The bridge is **off until explicitly enabled** (`tracker.enabled` defaults `false`, `tracker.type` defaults `null`). The discovery ceremony **detects → surfaces → asks → never assumes**, and writes config **only on confirmation**, with provenance. No signal means nothing is written.

Five probed signals:

| Signal                 | Probe                                                                                                                                                                                                  | Means                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| Linear MCP registered  | host MCP/tool list contains a Linear server                                                                                                                                                            | external create or discovery can be offered; flowctl resumes from the returned identity |
| `LINEAR_API_KEY`       | `[ -n "$LINEAR_API_KEY" ]`                                                                                                                                                                             | Linear can be resolved for deterministic GraphQL operations                             |
| GitHub auth            | `gh auth status` exits 0                                                                                                                                                                               | GitHub can be resolved                                                                  |
| GitLab auth / token    | `glab auth status` exits 0, or `GITLAB_TOKEN` / `CI_JOB_TOKEN` set                                                                                                                                     | GitLab can be resolved, including self-managed hosts                                    |
| Jira host + credential | a Jira base URL is configured (Cloud `*.atlassian.net` or a self-hosted `JIRA_BASE_URL`) and a credential is present (`JIRA_EMAIL` + `JIRA_API_TOKEN` on Cloud, or `JIRA_PAT` on Data Center / Server) | Jira can be resolved                                                                    |

Resolution is **env > config > ASK** (the same precedence as `flowctl review-backend`): if env or config already identifies the destination, the ceremony does not re-ask. On confirmation the skill writes via `flowctl config set tracker.…`, runs `flowctl tracker resolve`, and verifies with `flowctl sync active --json` (must report `active: true`). The bridge is active when raw `tracker.enabled == true` **or** raw `tracker.type ∈ {linear, github, gitlab, jira}`.

For a **GitLab** tracker the ceremony also writes `tracker.perTracker.project` (the group/sub-group/project path, e.g. `group/subgroup/project`, the GitLab analog of GitHub’s `repo`) and, for self-managed hosts, `tracker.perTracker.host`. GitLab works from an existing `glab auth login` session or a `GITLAB_TOKEN` / `CI_JOB_TOKEN` already present, with no Flow-Next-specific provisioning; the spec-first floor applies when neither is present.

For a **Jira** tracker the ceremony detects the deployment shape once and persists it: `tracker.perTracker.baseUrl` (the site, e.g. `https://acme.atlassian.net` or a self-hosted `https://jira.acme.com`), `tracker.perTracker.projectKey` (the project the bridge scopes to), `tracker.perTracker.authScheme` (`cloud-basic` for Jira Cloud’s `email:API_TOKEN` HTTP-basic, or `bearer-pat` for a Data Center / Server PAT), `tracker.perTracker.apiVersion` (`2` for both deployment families), and `tracker.perTracker.statusMap` (the normalized-status to Jira-workflow-status map the transitions API drives). Jira works from a standard Jira credential read from the environment each run and never stored; it is **never** an OAuth, Connect, or Forge app. Credentials live in env (`JIRA_EMAIL` + `JIRA_API_TOKEN` on Cloud, `JIRA_PAT` on DC/Server); only the non-secret deployment shape is persisted. A self-hosted instance behind a private CA can opt out of TLS verification with `tracker.perTracker.sslVerify=false` (or `JIRA_SSL_VERIFY=false`).

Since Flow-Next 1.12.0 the ceremony ends with **one optional, skippable readiness question**: *which tracker workflow state means “ready for work”?* - a Linear workflow-state name (discovered from the team’s states, a “Ready”-looking name recommended), a GitHub label (suggested `ready`, pre-created idempotently), a GitLab label (suggested `ready`, pre-created idempotently; GitLab has no rich workflow, so readiness is a label like GitHub), or a Jira workflow-status name (matched like Linear, used raw in the promoted-lane filter - never through `statusMap`). The answer is stored as `tracker.readyState`; skipping writes nothing and the readiness gate stays dormant. See [Readiness projection](https://flow-next.dev/integrations/tracker-operations/#readiness-projection) below.

## Three entry flows: no fixed starting point

There is no required starting point. All three flows attach sync state **on link**:

1. **Author-in-flow-then-push (flow-first).** A `fn-NN` spec already exists. Push creates the tracker issue, then `flowctl sync set-tracker-id` attaches the issue UUID plus `--identifier WOR-17` and `--url`. The `fn-NN` id is kept; the tracker key becomes a resolvable alias.
2. **Link-existing-issue (tracker-first) - “grab issue X and spec it.”** Fetch the issue, create the spec **keyed by the tracker key** and linked in the same call (`flowctl spec create --tracker-first --tracker-identifier <key-or-ref> --tracker-id <id> --tracker-url <url>`), seed the merge base from the current issue body, and treat the first pass as pull-only. **All four trackers support tracker-first:**

* Linear `WOR-17` / Jira `PROJ-123` are native `KEY-N` and mint directly (`wor-17-slug` / `proj-123-slug`).
* GitHub `#123` and GitLab `<project>#456` are not literal `KEY-N` (no alpha key / path + `#`), so flowctl mints **synthetic keys** while `tracker.type` matches: `#123` → `gh-123-slug`, `<project>#456` → `gl-456-slug` (project-scoped `iid`, never the opaque global id). Bare `gh-123` / `gl-456` resolve as aliases.
* Flow-first remains available on every tracker (create `fn-NN`, then `set-tracker-id` with the issue ref as a display alias).
* One Jira edge: a Data Center / Server **custom key** that can’t slugify into a canonical id (an underscore key like `MY_PROJECT-7`, or a long alphanumeric one) links flow-first / display-only - the key is stored as a shown alias, not a resolvable spec handle.

3. **Create-first (fresh idea - issue before any local spec).** When `tracker.specIds=tracker` and no issue exists yet, tracker-sync creates the issue from title + body (no local spec id), returns `{id, identifier, url}`, then the caller mints with that identity (linked at once) and seeds the merge base. A retry after partial failure **links**, never re-creates.

## Grain: one spec maps to one issue

* **One flow spec maps to one tracker issue.** The tracker UUID is the durable dedupe key (`flowctl sync set-tracker-id`); `flowctl sync check-collisions` flags any UUID shared by two specs.
* **Tasks stay flow-local by default** - they are never auto-created as tracker sub-issues. An optional checklist-in-body render (tasks as a body checklist, not sub-issues) is a body-format concern that is off by default.

## Hybrid id model

The two id schemes **coexist**; resolution is case-insensitive. **Ids never change - there is no rename-on-push.** Mixed stores (`fn-N-slug` next to `KEY-N-slug` / `gh-N-slug`) are permanent and expected.

|                    | Tracker-first (canonical)                                                                                     | Flow-first (alias)                                                                                                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| canonical spec id  | `wor-17-slug` / `gh-123-slug` / `gl-456-slug`                                                                 | `fn-NN-slug` (unchanged)                                                                                                                                                                                                    |
| canonical task ids | `wor-17-slug.M` / `gh-123-slug.M`                                                                             | `fn-NN-slug.M`                                                                                                                                                                                                              |
| branch             | same as canonical id                                                                                          | `fn-NN-slug`                                                                                                                                                                                                                |
| bare aliases       | `wor-17` / `gh-123` / `gl-456` (and `.M` task forms) resolve to the full slug id                              | `WOR-17` (stored in `tracker.identifier`) resolves to `fn-NN-slug`. A GitHub/GitLab ref like `#123` is **display-only** - stored and shown, never a resolvable handle; only the synthetic `gh-123` / `gl-456` form resolves |
| create / link      | `flowctl spec create --tracker-first --tracker-identifier <key-or-ref> --tracker-id <id> --tracker-url <url>` | `flowctl sync set-tracker-id fn-NN-slug <uuid> --identifier <key> --url <url>`                                                                                                                                              |

The user-facing version of this model, without the bridge internals, is [Spec and task ids](https://flow-next.dev/reference/spec-schema/#spec-and-task-ids).

### Synthetic keys (GitHub / GitLab)

GitHub and GitLab do not ship a native `KEY-N` display form. While `tracker.type` is `github` / `gitlab`, flowctl synthesizes a resolvable key from the issue number:

| `tracker.type` | Native identifier | Minted spec id                                                                            |
| -------------- | ----------------- | ----------------------------------------------------------------------------------------- |
| `linear`       | `WOR-17`          | `wor-17-slug` (native key, unchanged)                                                     |
| `jira`         | `PROJ-123`        | `proj-123-slug` (native key, unchanged)                                                   |
| `github`       | `#123`            | `gh-123-slug` (synthetic `gh`)                                                            |
| `gitlab`       | `<project>#456`   | `gl-456-slug` (synthetic `gl`; uses project-scoped **`iid`**, never the opaque global id) |

**Guards (not type-gating alone):** while `tracker.type` is `github`/`gitlab`, the matching prefix (`gh`/`gl`) is **contextually reserved** for synthesis - an explicit native `GH-123` identifier is rejected at mint/link. Before minting, a **preflight** of the existing store refuses a colliding canonical id or resolvable alias. A Linear/Jira repo natively keyed `GH` is unaffected (type is not `github`/`gitlab`). **Re-pointing `tracker.type`** (or re-pointing GitLab at a different project) is a documented hazard: previously minted ids keep their meaning; preflight stops a new mint from colliding with them.

### `tracker.specIds`: team default gate

| Value                                                                     | Behavior                                                                                                                   |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `flow` (default; also the fail-closed read for a malformed on-disk value) | Spec-creating skills mint `fn-N-slug` (today’s behavior)                                                                   |
| `tracker`                                                                 | With an active bridge, skills mint tracker-keyed ids: named issue → `--tracker-first`; fresh idea → create-first then mint |

```bash
flowctl config set tracker.specIds tracker    # or flow
```

Write side: only `flow` or `tracker` are accepted (invalid CLI writes are rejected). The leaf is **unset-detectable** (not materialized at init) so `/flow-next:setup` can ask once when a tracker is configured and the key is still absent; once set either way, setup never re-asks. An inactive bridge or unresolved destination degrades silently to flow-first. Explicit user override always wins.

Network cost is conditional: when the matching `tracker.perEvent.*` touchpoint is already active, tracker-first reorders an existing remote write; when those leaves are off (their default), tracker-first introduces an earlier remote write that flow-first would not make.

### Duplicate ordinals

A duplicate native `fn-N` ordinal whose full ids are distinct (e.g. two `fn-122-…` specs) is **untidy, not broken**. `flowctl validate --all` reports it as a top-level **warning**, not a root error. Bare `fn-N` resolution disambiguates rather than guessing - lists candidates and requires the full id. **Ids never change; do not renumber.**

### Other hybrid rules

* **Resolution is case-insensitive.** `flowctl show wor-17`, `work gh-123`, `plan gl-456`, tasks `wor-17.M` all resolve. `tracker.identifier` stores the **display form** (`WOR-17` / `#123` / `group/project#456`); the canonical id derives from the lowercase key.
* **`fn` is the only globally reserved prefix.** Synthetic `gh`/`gl` are reserved only while `tracker.type` matches. Native `fn-N` allocation counts `fn-*` only - a `wor-9999` never bumps the next `fn`.
* **Native `fn-N` allocation is a union scan** over the working tree, every registered git worktree’s `.flow/specs/`, and every ref (monotonic max-ever-allocated). Fail-open on git problems. That shrinks the parallel-agent collision window; separate unfetched clones can still collide - that is what `tracker.specIds=tracker` is for.
* **One tracker team / workspace per repo.** The bridge assumes a single team key so a bare `wor-17` resolves unambiguously.
* **No rename-on-push.** Existing spec/task ids, branches, and dep edges are never mutated on link; the tracker key is added as a resolvable handle, not a replacement. `flowctl spec set-title` on a tracker-linked spec updates the title only - it does not re-slug the id, branch, or files.

## Chart projection (optional)

When the bridge is active **and** `tracker.charts` is the literal `on`, chart lifecycle projects through the same facade as specs:

| Local object    | Tracker shape                                                      |
| --------------- | ------------------------------------------------------------------ |
| Chart           | Parent issue - status, counts, latest resolution, frontier summary |
| Decision (D-ID) | Child issue - type, attendance, status, blocking, safe evidence    |

Contracts that stay load-bearing:

* **Local is canonical.** Chart mutations always commit under `.flow/charts/` first. Remote failure never rolls back local state; `tracker_projection.skipped` names why projection did not run (`tracker.charts_off`, `bridge_inactive`, provider degradation).
* **Projection-only rollups.** Parent counts / frontier / status are mirrors, not control-plane inputs. The tracker never drives chart claims or resolves attended decisions.
* **Safe evidence only.** Assets and answers project as kind/reference/display/revision summaries - never secrets or literal destructive command strings.
* **Reconcile receipts.** Event markers + aggregate receipts dedupe retries; re-invoking the same chart command or the next mutation converges without duplicate issues/comments.
* **URL re-entry is local ledger only.** `flowctl chart locate` resolves a stored tracker URL or identifier through the local provenance ledger - no remote search, no title matching. Parent URL → local chart status/frontier; open decision URL → that D-ID; historical/unsupported URLs fail or show history and offer the local chart-id path without mutation. See [Chart - tracker re-entry](https://flow-next.dev/skills/chart/#tracker-projection--url-re-entry).

```bash
flowctl config set tracker.charts on
flowctl chart locate "https://linear.app/.../stored-chart-or-decision-url" --json
```

## Linear Diffs: review the PR inside the issue

[Linear Diffs](https://linear.app/docs/diffs) (GA May 2026) renders a GitHub PR’s diff, file changes, checks, and inline review threads directly on the Linear issue, and lets you approve, request changes, or merge from inside Linear. Flow-Next makes your PRs **Diffs-ready automatically** when `tracker.type == linear`:

![A Flow-Next PR rendered as a Linear Diff inside its issue - the codex-delegation diff shown on the right, the issue context and review controls on the left.](https://flow-next.dev/_astro/linear-diffs.Fpacr5KE_130F3m.webp)

A Flow-Next PR (the spec's make-pr output) rendered as a Linear Diff inside its issue - full code diff, checks, and review controls without leaving Linear. Click to zoom.

* **What Flow-Next does.** `make-pr` puts a **non-closing** `Ref WOR-N` line in the PR body so Linear’s GitHub integration auto-links the PR to the issue on the identifier - which is exactly what makes the diff render inside the issue. On the GraphQL transport it also creates a rich PR attachment (`attachmentLinkURL`) for status sync. **Non-closing (`Ref`, not `Fixes`) is deliberate:** the PR links and renders as a diff but does **not** auto-complete the Linear issue on merge. Since **2.1.2**, **merge evidence owns the `Done` transition** - only `land.merged` (gated on the GitHub `MERGED` probe) writes `Done`; spec-completion-review posts a verdict comment but never completes the issue. The linkage is unconditional once the bridge is active - there is no separate `makePr` opt-in, which is also why make-pr can move the issue to **In Review** on this same unconditional path.
* **What you must enable** (one-time, Linear-side - Flow-Next cannot set these for you): the Linear **GitHub integration with code access** to the repo, your **personal GitHub connection**, and **“Enable code reviews”** in Linear settings. Without them the PR still links and status still syncs; only the rendered diff view needs them.
* **GitHub / GitLab tracker.** No Linear Diffs. The PR is cross-linked natively (`Refs #N` on GitHub; `Ref <project>#<iid>` on GitLab) and review happens on the host. Like the Linear branch, the cross-reference is non-closing (`Refs`, not `Fixes`) so merge does not bypass spec-completion-review.
* **Jira tracker.** No Linear Diffs and no native git cross-reference (Jira does not auto-linkify a PR, and Smart-Commit keys need a DVCS connector that may be absent). Instead make-pr writes the PR↔issue link as a Jira **remote link** - idempotent on a deterministic `globalId`, rendered in the issue’s web-links panel regardless of any connector - falling back to a URL comment if the remote-link write fails. The link is informational, never status-driving, so merge still routes through the merge-evidence gate.

Day-to-day mechanics (lifecycle touchpoints, who wins a conflict, readiness and dependency projection, the transport contract, and the Jira specifics) are in [Tracker operations](https://flow-next.dev/integrations/tracker-operations/).
