Skip to content

Tracker operations

This is the operations half of the tracker bridge. Set the bridge up there; run it from here.

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 - also moves the issue to In Review when an open PR exists (2.1.2+)
resolve-prtracker.perEvent.resolvePrcommentPR threads are resolved
completion reviewtracker.perEvent.completionReviewcommenta spec-completion review runs - verdict + R-ID coverage comment, never Done (2.1.2+)
land (merged)tracker.perEvent.land.mergedpusha PR merges - the only path to Done, gated on the GitHub MERGED probe; active by default when the bridge is active (2.1.2+)

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. A no-tracker repo or excluded event costs one value-check and no provider request. When active, the caller composes any semantic body or comment in a private temp file and invokes the flowctl tracker sync facade once.

Observable and forcing. Every facade call is event-tagged (work.firstClaim, work.done, capture, makePr, etc.), so .flow/sync-runs/ records which touchpoint it 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>. A MISSING event is retro-fired exactly once through the same facade, then re-checked against a fresh --since. The final summary carries Tracker sync: OK | MISSING:<event> → retro-fired → OK | MISSING:<event> (retro-fire failed: <reason>) | n/a (bridge inactive).

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 did not fire and the one bounded retro-fire could not recover it. The primary work is unaffected: tracker sync is best-effort and never blocks. To recover:

  1. Read the structured result from the event receipt. The class distinguishes auth, unresolved, stale_id, rate_limited, transport, capability, conflict, and external_action_required.
  2. Resolve the named condition, then re-fire the missed touchpoint through /flow-next:tracker-sync. The skill supplies recovery judgment; its runtime action is the matching flowctl tracker sync facade call.
  3. Verify: flowctl sync check <spec-id> --events <event> --since <retro-fire-time> now prints OK:<event>.

Status lifecycle: Done means merged (2.1.2)

Section titled “Status lifecycle: Done means merged (2.1.2)”

Done is a claim that the work shipped, so Flow-Next reserves it for merge-confirmed state and never infers it from local completion. The flow→tracker status map is a function of (spec status, completion_review_status, PR-merge-evidence):

PhaseEvidenceTracker state
Work underwaytask in_progressIn Progress
make-pr found an open PRopen PR for the branchIn Review
PR mergedGitHub MERGEDDone (terminal)
All tasks done + completion ship, no merged PRno / open / closed-unmerged PRnever Done - stays In Review (open) or In Progress (no PR); ambiguous → NEEDS_HUMAN

The merge-evidence gate is a per-write invariant - no touchpoint (and no manual /flow-next:tracker-sync reconcile) writes terminal Done without a MERGED probe result, so a premature “shipped” can’t reach the board (the bug that prompted this: a spec marked locally complete with no PR was wrongly pushed to Done). A manual reconcile can still recover Done once a merge exists. Pilot mirrors the rule: an all-done spec with no merged PR is never NO_WORK - it routes to make-pr, or reports DEFERRED_TO_LAND when an open PR exists.

  • 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). flow-wins uses the normal provider-neutral status write and still requires merged-PR evidence for Done. tracker-wins folds a terminal tracker into the local spec without a provider write and emits the existing pulled receipt. The mirror - a merged Flow spec while the tracker is active - cannot converge through raw spec status, so it stays an explicit conflict with no write and no sync-state advance.
  • 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 / GitLab: 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). GitLab has no rich workflow states, so readiness is a label identical to GitHub’s. Jira: a case-insensitive match on the workflow-status name (like Linear), used raw - never resolved through statusMap.
  • 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 (corrected in 3.28.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 and re-readies the spec. A projection-set ready never clears a strike: the board echo re-grants readiness with nobody acting, and clearing on it would re-dispatch the same failing spec every tick. The recovery is the verb, not the board - flowctl pilot strikes clear <spec-id> - and the strike 2/2 verdict names it. Moving the issue out of the ready state is still worth doing (it stops re-selection noise and keeps the board honest), but it is not what clears the strike: the board stays the control plane for readiness; strikes are pilot state. Full walkthrough on the pilot page.

When a spec declares cross-spec dependencies via depends_on_epics, every push or reconcile asks flowctl tracker relate to project those edges onto linked issues, so the board shows the real ordering instead of a flat list.

  • Direction and fidelity. A depends_on_epics edge means “this spec is blocked by that one.” Linear and Jira use native directional blocked-by relations. GitHub has no issue-level blocked-by relation, so flowctl makes the blocked issue a sub_issue of the blocker and returns degraded.kind: "hierarchy" with degraded.form: "sub_issues". GitLab uses a native directional link when capabilities.blockedBy is true; otherwise it records a directionless relates_to link for board visibility and preserves direction and provenance in the fenced <!-- flow:deps --> body block.
  • Read-before-write idempotency. Re-syncing creates no duplicate relations. Re-running is safe.
  • Provenance. Flow records projected edges in the per-spec depRelations ledger, plus the fenced marker where a fallback needs it. A relation flowctl cannot prove it created is never removed. A ledgered relation that a human removed is deferred, never silently recreated.
  • Missing link → warning, not a block. A dependency whose spec isn’t linked to an issue surfaces a named warning; the rest of the sync proceeds.
  • Completed blockers stay visible. A done dependency keeps its relation on the board (the audit trail) but never re-gates ready=true - readiness already treats done deps as satisfied.
  • Collision-safe. A ledgered edge that a tracker user removed is deferred, never silently recreated. Provider failures return a structured class and never block the primary work.

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.

At runtime, skills do not choose provider routes or execute provider APIs. They pass semantic inputs to flowctl tracker and branch on its structured result.

flowctl tracker resolve writes normalized destination metadata and a capability snapshot to tracker.resolved. All consuming verbs read that persisted block. Scoped timestamps make partial refreshes honest: refreshing destination.statusIds cannot make capabilities look fresh.

The command surface has three layers:

  • Wire verbs: read, update, comment-add, comment-list, comment-update, comment-delete, label, assign, list-open, list-states, relation-list, question, attach, and attach-get.
  • Lifecycle verbs: create, create-first, persist-external, status, relate, and sync-body.
  • Lifecycle facade: flowctl tracker sync <spec-id> --op <push|pull|reconcile|comment> --event <key>. This is what lifecycle callers use. It owns create-if-unlinked, title and body projection, comment markers and deduplication, provider mutations, and one aggregate receipt.

Every invocation returns one JSON envelope. Success is:

{"success":true,"data":{},"degraded":null,"probe":null}

Failure includes success:false, an exhaustive class, error, retryable, and typed details. Classes are invalid_input, inactive, unresolved, auth, rate_limited, transport, not_found, capability, conflict, stale_id, and external_action_required; their fixed exits are 2 through 12 in that order. Exit 0 is success. Branch on class, never provider error text. A confirmed capability loss appears as degraded; a failed capability probe appears separately as probe and keeps the prior capability.

Jira is the fourth tracker behind the same deterministic command contract. Its workflow model is richer than the normalized vocabulary, so it carries provider-specific metadata:

  • REST API, both deployment families. Jira Cloud and Data Center / Server both use API version 2 so issue bodies round-trip as plain strings. Cloud uses HTTP-basic email:API_TOKEN auth; Data Center / Server uses a Bearer PAT. A legacy configured version 3 is migrated to 2. Self-hosted instances behind a private CA can opt out of TLS verification.
  • No MCP runtime dependency. The official Atlassian Remote MCP does not expose the complete mutation surface the bridge needs. External MCP use can return through persist-external; deterministic runtime operations stay in flowctl.
  • Workflow-aware status via the transitions API. Jira forbids a direct status write - a change goes through the transitions API: read the transitions legal from the issue’s current status, resolve the one that lands the configured target, and POST it. A configurable tracker.perTracker.statusMap maps each normalized status to a Jira workflow status (by durable id, preferred, or name). A status with no map entry, or a target the project’s workflow can’t reach from the current state, is deferred with a receipt, never forced into an arbitrary lane. Terminal detection keys off the stable statusCategory.key == "done" signal, not the renamable status name.
  • Done means merged. A locally-done spec with no merged PR maps to in-review, and only a MERGED PR probe lets terminal done reach the board. (See Status lifecycle.)
  • Native blocks links. depends_on_epics edges project as native Jira Blocks issue links. Jira does not no-op a duplicate link, so flowctl reads before writing. A human-removed ledgered link is deferred, never silently re-created. The blocking link type is configurable and resolved once.
  • Plain-string body fidelity. API version 2 keeps issue bodies comparable to the markdown-side representation without an ADF translation layer.
  • Backlog enumeration via JQL. flowctl tracker wire list-open enumerates open issues at the exact tracker.readyState, using bounded pagination and honest truncated metadata.
  • Backlog dependency reads. flowctl tracker wire relation-list accepts the durable/display locator returned by list-open, returns provider-neutral directed blocks rows for pilot ordering, and fails closed when bounded pagination cannot prove the graph complete.
  • Read-only status enumeration (4.1.0). flowctl tracker wire list-states lists the project’s statuses (id, name, statusCategory key) with a complete flag, so “does every configured status id still name a live status?” is answerable without tracker resolve - which repairs and writes the mapping. Detection never writes.
  • Tracker-only question parking. flowctl tracker wire question receives the issue locator, four stable identity fields, and a private body file. It computes the canonical marker id, reads existing comments, and posts only when that id is absent.
  • PR link as a remote link. Jira has neither GitHub’s Refs #N auto-linkify nor Linear’s Diffs attachment, so /flow-next:make-pr projects the PR↔issue link as a Jira remote link (idempotent on a deterministic globalId), falling back to a URL comment if the remote-link write is unavailable.

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 semantic judgment. flowctl tracker owns deterministic provider reads and mutations. flowctl sync owns local bridge state: active, get-state, set-tracker-id, set-last-synced, set-merge-base, clear, list-unsynced, list-stale, check-collisions, the dependency ledger, receipt, check, and defer. See CLI Reference and 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, wor-17, gh-123, or gl-456.
  • Collaboration - projection-not-coordination positioning, the tracker-first PM flow, and the team recommendation for tracker.specIds.
  • Configuration - flowctl sync subcommands and the tracker.* config keys.