Skip to content

Tracker sync

/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.

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.
  • 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.

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

Configuring the bridge is its own one-time step, separate from /flow-next:setup. Setup installs flowctl 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:

SignalProbeMeans
Linear MCP registeredhost MCP/tool list contains a Linear serverexternal 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 authgh auth status exits 0GitHub can be resolved
GitLab auth / tokenglab auth status exits 0, or GITLAB_TOKEN / CI_JOB_TOKEN setGitLab can be resolved, including self-managed hosts
Jira host + credentiala 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 below.

Three entry flows: no fixed starting point

Section titled “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 (flowctl spec create --tracker-first --tracker-identifier <key-or-ref>), 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: #123gh-123-slug, <project>#456gl-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.
  1. 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 → attaches → seeds the merge base. A retry after partial failure links, never re-creates.
  • 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.

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 idwor-17-slug / gh-123-slug / gl-456-slugfn-NN-slug (unchanged)
canonical task idswor-17-slug.M / gh-123-slug.Mfn-NN-slug.M
branchsame as canonical idfn-NN-slug
bare aliaseswor-17 / gh-123 / gl-456 (and .M task forms) resolve to the full slug idWOR-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 / linkflowctl spec create --tracker-first --tracker-identifier <key-or-ref>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.

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.typeNative identifierMinted spec id
linearWOR-17wor-17-slug (native key, unchanged)
jiraPROJ-123proj-123-slug (native key, unchanged)
github#123gh-123-slug (synthetic gh)
gitlab<project>#456gl-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.

ValueBehavior
flow (default; also the fail-closed read for a malformed on-disk value)Spec-creating skills mint fn-N-slug (today’s behavior)
trackerWith an active bridge, skills mint tracker-keyed ids: named issue → --tracker-first; fresh idea → create-first then mint
Terminal window
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.

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.

  • 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.

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

Local objectTracker shape
ChartParent 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.
Terminal window
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

Section titled “Linear Diffs: review the PR inside the issue”

Linear 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 (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.