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.
| Event | Config key | Default op | Fires when |
|---|---|---|---|
| capture | tracker.perEvent.capture | reconcile | a spec is captured |
| interview | tracker.perEvent.interview | reconcile | a spec is refined |
| plan | tracker.perEvent.plan | reconcile | a spec is decomposed into tasks |
| work (first claim) | tracker.perEvent.work.firstClaim | push | the first task of a spec is claimed |
| work (done) | tracker.perEvent.work.done | comment | a task completes |
| make-pr | tracker.perEvent.makePr | comment | a PR is opened - also moves the issue to In Review when an open PR exists (2.1.2+) |
| resolve-pr | tracker.perEvent.resolvePr | comment | PR threads are resolved |
| completion review | tracker.perEvent.completionReview | comment | a spec-completion review runs - verdict + R-ID coverage comment, never Done (2.1.2+) |
| land (merged) | tracker.perEvent.land.merged | push | a 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.
MISSING after retro-fire: recovery
Section titled “MISSING after retro-fire: recovery”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:
- Read the structured result from the event receipt. The
classdistinguishesauth,unresolved,stale_id,rate_limited,transport,capability,conflict, andexternal_action_required. - 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 matchingflowctl tracker syncfacade call. - Verify:
flowctl sync check <spec-id> --events <event> --since <retro-fire-time>now printsOK:<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):
| Phase | Evidence | Tracker state |
|---|---|---|
| Work underway | task in_progress | In Progress |
| make-pr found an open PR | open PR for the branch | In Review |
| PR merged | GitHub MERGED | Done (terminal) |
All tasks done + completion ship, no merged PR | no / open / closed-unmerged PR | never 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.
Reconciliation: who wins
Section titled “Reconciliation: who wins”- Body - an agentic host-agent semantic 3-way merge against the
lastSyncedAtmerge-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 istracker.conflictTiebreak(flow-wins | tracker-wins | always-ask, defaultalways-ask).flow-winsuses the normal provider-neutral status write and still requires merged-PR evidence forDone.tracker-winsfolds a terminal tracker into the local spec without a provider write and emits the existingpulledreceipt. 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.
Readiness projection
Section titled “Readiness projection”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 readyon a tracker-connected repo is overwritten by the next sync; the team blesses work on the board. This is also why/flow-next:captureand/flow-next:interviewstop offering their local mark-ready prompt oncereadyStateis configured. - Match semantics. Linear: case-insensitive trimmed match on the workflow-state name (names, not
state.type- a custom “Ready” state is typicallytype=unstarted, so type alone cannot distinguish Todo from Ready). GitHub / GitLab: thereadyStatelabel - present on the issue meansready=true, absent meansready=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 throughstatusMap. - Change-only receipts. The projection applies via the idempotent
spec ready/unreadytoggles 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 +
noopreceipt, leaves the flag untouched, and the rest of the sync continues - one bad knob never aborts the run, and a stalereadyStatemust not silently un-ready every linked spec. - Orthogonal to status. The projection never feeds the who-wins ladder, never advances
lastSyncedAtby 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:pilotselects ready specs, and its two-strike don’t-thrash guard runs a localspec 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 thestrike 2/2verdict 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.
Dependency projection
Section titled “Dependency projection”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_epicsedge 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 asub_issueof the blocker and returnsdegraded.kind: "hierarchy"withdegraded.form: "sub_issues". GitLab uses a native directional link whencapabilities.blockedByis true; otherwise it records a directionlessrelates_tolink 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
depRelationsledger, 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
donedependency keeps its relation on the board (the audit trail) but never re-gatesready=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.
Sync-state schema
Section titled “Sync-state schema”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:
| Field | Meaning |
|---|---|
id | tracker UUID - the durable dedupe key |
identifier | display key, e.g. WOR-17 |
url | issue URL |
lastSyncedAt | ISO timestamp of the last real reconciliation (advances on a real reconcile, never on a no-op pull or echo) |
baseHashFlow / baseHashTracker | content hashes of each merge-base side (echo fence) |
mergeBaseFlow / mergeBaseTracker | the 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.
Deterministic tracker transport
Section titled “Deterministic tracker transport”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, andattach-get. - Lifecycle verbs:
create,create-first,persist-external,status,relate, andsync-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: the enterprise provider
Section titled “Jira: the enterprise provider”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_TOKENauth; 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.statusMapmaps 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 stablestatusCategory.key == "done"signal, not the renamable status name. Donemeans merged. A locally-donespec with no merged PR maps toin-review, and only aMERGEDPR probe lets terminaldonereach the board. (See Status lifecycle.)- Native blocks links.
depends_on_epicsedges project as native JiraBlocksissue 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-openenumerates open issues at the exacttracker.readyState, using bounded pagination and honesttruncatedmetadata. - Backlog dependency reads.
flowctl tracker wire relation-listaccepts the durable/display locator returned bylist-open, returns provider-neutral directedblocksrows 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-stateslists the project’s statuses (id, name, statusCategory key) with acompleteflag, so “does every configured status id still name a live status?” is answerable withouttracker resolve- which repairs and writes the mapping. Detection never writes. - Tracker-only question parking.
flowctl tracker wire questionreceives 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 #Nauto-linkify nor Linear’s Diffs attachment, so/flow-next:make-prprojects the PR↔issue link as a Jira remote link (idempotent on a deterministicglobalId), falling back to a URL comment if the remote-link write is unavailable.
Ralph-safe: conflicts queue
Section titled “Ralph-safe: conflicts queue”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.
flowctl surface
Section titled “flowctl surface”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).
See also
Section titled “See also”- Spec & task ids - the hybrid id model: when a spec is
fn-NN,wor-17,gh-123, orgl-456. - Collaboration - projection-not-coordination positioning, the tracker-first PM flow, and the team recommendation for
tracker.specIds. - Configuration -
flowctl syncsubcommands and thetracker.*config keys.