Skip to content

Tracker Sync

/flow-next:tracker-sync projects a Flow-Next spec onto a tracker issue (Linear first, GitHub next) and reconciles body, status, and comments two-way. The skill owns the judgment — API calls, reconciliation, asking — while flowctl sync … provides the deterministic plumbing.

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.

Four probed signals:

SignalProbeMeans
Linear MCP registeredhost MCP/tool list contains a Linear serverinteractive Linear transport available (OAuth handled)
LINEAR_API_KEY[ -n "$LINEAR_API_KEY" ]headless Linear GraphQL transport available
GitHub authgh auth status exits 0headless GitHub transport available
Jira hosta *.atlassian.net host is configuredJira present — surfaced but out of scope (not offered)

Resolution is env > config > ASK (the same ladder as flowctl review-backend): if env or config already decides the transport, the ceremony does not re-ask. On confirmation the skill writes via flowctl config set tracker.… 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}.

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) or a GitHub label (suggested ready, pre-created idempotently). The answer is stored as tracker.readyState; skipping writes nothing and the readiness gate stays dormant. See Readiness projection below.

Two entry flows — no fixed starting point

Section titled “Two entry flows — no fixed starting point”

There is no required starting point. Both 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 WOR-17), seed the merge base from the current issue body, and treat the first pass as pull-only.

The two id schemes that result from these flows are explained in Spec & task ids.

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

State lives in the existing .flow/specs/<id>.json sidecar (not the markdown frontmatter — merge-base body snapshots would bloat the spec). The per-spec tracker block:

FieldMeaning
idtracker UUID — the durable dedupe key
identifierdisplay key, e.g. WOR-17
urlissue URL
lastSyncedAtISO timestamp of the last real reconciliation (advances on a real reconcile, never on a no-op pull or echo)
baseHashFlow / baseHashTrackercontent hashes of each merge-base side (echo fence)
mergeBaseFlow / mergeBaseTrackerthe body snapshots themselves — the common ancestor for the agentic 3-way merge

The merge base is a paired snapshot at one sync point: flowctl sync set-merge-base requires both the --flow/--flow-file and the --tracker/--tracker-file halves together. A partial write that pins one half to a stale sync point is rejected.

The skill is transport-blind — it calls a normalized interface (fetchIssue / writeIssue / listComments / postComment / readStatus / setStatus) and never sees a wire shape. Each adapter detects the best available transport and degrades gracefully:

AdapterLadderStatus fidelity
LinearMCP → GraphQL (LINEAR_API_KEY) → no-opfull workflow states
GitHubgh (single rung) → no-opreduced fidelity (open / closed)

When no transport is reachable, the run is a noop plus a receipt note — never a crash. The transport actually used (mcp / graphql / gh / none) is recorded on every receipt.

Lifecycle sync points (on by default — opt-out)

Section titled “Lifecycle sync points (on by default — opt-out)”

Sync is wired into seven lifecycle skills. When you hook the bridge up via the /flow-next:tracker-sync discovery ceremony, the whole pipeline activates by default — connecting a tracker means you want it kept in sync, so you don’t opt in event-by-event. You opt out instead: exclude events at ceremony time, or turn any off later with flowctl config set tracker.perEvent.<event> off. Leaf values: off | pull | push | reconcile | comment.

EventConfig keyDefault opFires when
capturetracker.perEvent.capturereconcilea spec is captured
interviewtracker.perEvent.interviewreconcilea spec is refined
plantracker.perEvent.planreconcilea spec is decomposed into tasks
work (first claim)tracker.perEvent.work.firstClaimpushthe first task of a spec is claimed
work (done)tracker.perEvent.work.donecommenta task completes
make-prtracker.perEvent.makePrcommenta PR is opened
resolve-prtracker.perEvent.resolvePrcommentPR threads are resolved
completion reviewtracker.perEvent.completionReviewreconcilea spec-completion review runs

The lifecycle skills value-check flowctl sync active and the specific perEvent leaf, short-circuiting cleanly when the bridge is off or an event was opted out — so a no-tracker repo (or an excluded event) costs a single value-check, no transport. The no-tracker path is the documented default and is shown unchanged everywhere else in these docs.

Observable + forcing (1.11.0). Every lifecycle dispatch is event-tagged: the tracker-sync skill writes its receipt with --event <perEvent-key> (work.firstClaim, work.done, capture, makePr, …), so .flow/sync-runs/ records which touchpoint each run served. At end-of-skill, work, capture, and make-pr run the read-only audit flowctl sync check <spec-id> --events <triggered-csv> --since <run-anchor> — independently of the touchpoints themselves, so a wholesale-skipped dispatch block is still caught. An event is MISSING iff it triggered this run AND its perEvent leaf is enabled AND the bridge is active AND no receipt with a matching event tag and timestamp ≥ --since exists (any receipt status clears — the check asserts the touchpoint ran; the receipt’s own status carries success/failure detail). A MISSING event is retro-fired exactly once — the skill re-dispatches the missed touchpoint via tracker-sync, then re-checks against a fresh --since — and the skill’s final summary carries a mandatory four-state Tracker sync: slot: OK | MISSING:<event> → retro-fired → OK | MISSING:<event> (retro-fire failed: <reason>) | n/a (bridge inactive). An explicit n/a proves the check ran; an absent slot is visible as a skipped check. With no tracker configured sync check exits silently in constant time — non-tracker repos see no change anywhere.

Activation is ceremony-gated, not flag-gated. The config schema default for every perEvent leaf stays off, so a bare tracker.enabled=true set by hand or a script — without running the discovery ceremony — fires no lifecycle-event sync (every perEvent event stays dormant). Only the ceremony’s explicit per-event writes (or your own config set) turn events on. This keeps the accidental-enable guard while making the intended path (run the ceremony) sync everything. The one thing not gated this way is make-pr’s PR↔issue link — it’s unconditional whenever the bridge is active (the exception documented just below), so a bare enabled=true plus a linked spec still adds a Ref line on the next make-pr. That linkage is cheap, conflict-free, and the whole point (Linear Diffs); it does not mutate the spec or fire the lifecycle touchpoints.

One exception — PR linkage is unconditional when the bridge is active. /flow-next:make-pr always links the new PR to its tracker issue when sync active reports true and the spec is linked — it does not require opting makePr in. Linking a PR to its issue is zero- or near-zero-cost hygiene and powers Linear Diffs (below), so there is no reason to gate it. The perEvent.makePr leaf still governs any extra make-pr sync, such as a status comment. make-pr additionally verifies the ref landed post-create: it fetches the LIVE PR body via gh pr view --json body and, when the Ref <identifier> line is absent (e.g. an agent hand-rolled gh pr create and bypassed the deterministic append), repairs it append-only via gh pr edit — mechanical, idempotent, fully non-fatal.

A Tracker sync: MISSING:<event> (retro-fire failed: <reason>) summary line means the touchpoint didn’t fire AND the one bounded retro-fire couldn’t recover it — typically no reachable transport (MCP server down, no LINEAR_API_KEY, gh unauthenticated). The primary work is unaffected: tracker sync is best-effort and never blocks, so the task is done / the PR is open. To recover by hand:

  1. Read the failure reason from the run’s receipts: ls -t .flow/sync-runs/sync-<spec-id>-*.json | head -3 — the status (noop / errored) and note fields on the event-tagged receipt say why it failed.
  2. Once transport returns, re-fire the missed touchpoint manually via the skill: /flow-next:tracker-sync push <spec-id> for status events (work.firstClaim, work.completionReview), or the matching op for comment events (comment <spec-id> for work.done / makePr).
  3. Verify: flowctl sync check <spec-id> --events <event> --since <retro-fire-time> now prints OK:<event>.

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 — Flow-Next’s spec-completion-review owns the Done transition. The linkage is unconditional once the bridge is active — there is no separate makePr opt-in.
  • 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 tracker. No Linear Diffs. The PR is cross-linked natively (Refs #N) in the same repo and review happens on GitHub. Like the Linear branch, the cross-reference is non-closing (Refs, not Fixes) so merge does not bypass spec-completion-review.
  • Body — an agentic host-agent semantic 3-way merge against the lastSyncedAt merge-base snapshot, translating between flow’s structured spec and the tracker’s free-form issue. Only genuine contradictions surface; confident merges proceed.
  • Status — a per-field who-wins ladder. The collision / deadlock case is evaluated before single-field terminal-wins rules (a tracker=done × flow=… deadlock falls to the tiebreak rather than being silently overwritten). Tiebreak is tracker.conflictTiebreak (flow-wins | tracker-wins | always-ask, default always-ask).
  • Comments and evidence — two-way append with dedup; neither side overwrites the other.

When tracker.readyState is configured (the optional ceremony question above), every operation that reads the issue (pull / reconcile) projects the configured tracker state onto the local spec ready flag — so readiness has a single local read path whether it is human-set or tracker-driven (Flow-Next 1.12.0+).

  • One-way pull, tracker authoritative. Readiness is projected tracker → local only — the local flag is never pushed to the tracker (no status write, no label add/remove). A local flowctl spec ready on a tracker-connected repo is overwritten by the next sync; the team blesses work on the board. This is also why /flow-next:capture and /flow-next:interview stop offering their local mark-ready prompt once readyState is configured.
  • Match semantics. Linear: case-insensitive trimmed match on the workflow-state name (names, not state.type — a custom “Ready” state is typically type=unstarted, so type alone cannot distinguish Todo from Ready). GitHub: the readyState label — present on the issue means ready=true, absent means ready=false (absence is a normal state; un-labeling is exactly how a GitHub user un-readies a spec).
  • Change-only receipts. The projection applies via the idempotent spec ready/unready toggles and emits an event-tagged receipt only when the local flag actually changes — silent on a no-op echo.
  • Stale-config degradation. A configured state name / label that no longer resolves on the tracker (renamed or deleted) produces a warn + noop receipt, leaves the flag untouched, and the rest of the sync continues — one bad knob never aborts the run, and a stale readyState must not silently un-ready every linked spec.
  • Orthogonal to status. The projection never feeds the who-wins ladder, never advances lastSyncedAt by itself, and never blocks. readyState: null (the default) skips it entirely — no calls, no receipts, no flag writes.
  • Pilot interplay (1.13.0+). /flow-next:pilot selects ready specs, and its two-strike don’t-thrash guard runs a local spec unready — advisory until the board reflects it, since the next pull projects the issue’s state back (re-readying the spec, which pilot reads as a human re-bless and clears strikes). When pilot strikes a spec out, move the issue out of the ready state on the board; re-blessing after a fix is the reverse move. Full walkthrough on the pilot page.

Every run emits a receipt (flowctl sync receipt --status …); genuine conflicts queue (flowctl sync defer …) rather than block. In autonomous / Ralph mode an always-ask tiebreak resolves to queue, not prompt — the same policy, with surface-dependent delivery. Deferred conflicts land in the review deferred-findings sink (.flow/review-deferred/<branch>.md), where a human already looks for deferred work — so tracker-sync never needs flowctl block and never stalls the loop. See Ralph guardrails.

The skill owns judgment; flowctl sync owns deterministic plumbing: sync active / get-state / set-tracker-id / set-last-synced / set-merge-base / clear / list-unsynced / list-stale / check-collisions / receipt (event-tagged via --event <perEvent-key>) / check (read-only lifecycle audit, OK/MISSING per event) / defer, plus the tracker.* config keys. See Configuration.

The sync-engine shape (discovery ceremony, per-item lastSyncedAt, surface-diffs-never-overwrite) is adapted from Ray Fernando’s running-bug-review-board issue-trackers.md (Apache-2.0).

  • Spec & task ids — the hybrid id model: when a spec is fn-NN and when it is wor-17.
  • Collaboration — projection-not-coordination positioning and the tracker-first PM flow.
  • Configurationflowctl sync subcommands and the tracker.* config keys.