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.
Projection, not coordination
Section titled “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.
- 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
Setup: the discovery ceremony
Section titled “Setup: the discovery ceremony”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:
| 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 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:
- Author-in-flow-then-push (flow-first). A
fn-NNspec already exists. Push creates the tracker issue, thenflowctl sync set-tracker-idattaches the issue UUID plus--identifier WOR-17and--url. Thefn-NNid is kept; the tracker key becomes a resolvable alias. - 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/ JiraPROJ-123are nativeKEY-Nand mint directly (wor-17-slug/proj-123-slug). - GitHub
#123and GitLab<project>#456are not literalKEY-N(no alpha key / path +#), so flowctl mints synthetic keys whiletracker.typematches:#123→gh-123-slug,<project>#456→gl-456-slug(project-scopediid, never the opaque global id). Baregh-123/gl-456resolve as aliases. - Flow-first remains available on every tracker (create
fn-NN, thenset-tracker-idwith 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.
- Create-first (fresh idea - issue before any local spec). When
tracker.specIds=trackerand 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.
Grain: one spec maps to one issue
Section titled “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-collisionsflags 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
Section titled “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> | 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.
Synthetic keys (GitHub / GitLab)
Section titled “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
Section titled “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 |
flowctl config set tracker.specIds tracker # or flowWrite 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
Section titled “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
Section titled “Other hybrid rules”- Resolution is case-insensitive.
flowctl show wor-17,work gh-123,plan gl-456, taskswor-17.Mall resolve.tracker.identifierstores the display form (WOR-17/#123/group/project#456); the canonical id derives from the lowercase key. fnis the only globally reserved prefix. Syntheticgh/glare reserved only whiletracker.typematches. Nativefn-Nallocation countsfn-*only - awor-9999never bumps the nextfn.- Native
fn-Nallocation 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 whattracker.specIds=trackeris for. - One tracker team / workspace per repo. The bridge assumes a single team key so a bare
wor-17resolves 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-titleon a tracker-linked spec updates the title only - it does not re-slug the id, branch, or files.
Chart projection (optional)
Section titled “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.skippednames 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 locateresolves 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.
flowctl config set tracker.charts onflowctl chart locate "https://linear.app/.../stored-chart-or-decision-url" --jsonLinear 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:
- What Flow-Next does.
make-prputs a non-closingRef WOR-Nline 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, notFixes) 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 theDonetransition - onlyland.merged(gated on the GitHubMERGEDprobe) writesDone; spec-completion-review posts a verdict comment but never completes the issue. The linkage is unconditional once the bridge is active - there is no separatemakePropt-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 #Non GitHub;Ref <project>#<iid>on GitLab) and review happens on the host. Like the Linear branch, the cross-reference is non-closing (Refs, notFixes) 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.