Linear sync never converges: tbd sync reports success with work pending, and push/pull alternate forever on agreeing data
- Dominant language
- TypeScript
- Stars
- 79
- Forks
- 9
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 42
Description
`tbd 0.8.1`, provider `linear`, 136 linked beads in one repository (team `FIN`).
Three defects, found while reconciling a large mirror. They compound: the first hides the second, and the third makes the direction unreadable.
## 1. `tbd sync` reports success while the tracker surface has pending work
`tbd sync` documents itself as covering docs, issues **and** enabled trackers, reporting per-surface failures at the end. In practice it prints only the docs and issues lines and returns `✓ Already in sync`, while the Linear surface still has 13 items queued. Nothing is printed about Linear at all — not a success line, not a skip, not a failure.
```
$ tbd sync
✓ Docs up to date
✓ Synced: sent 1 new, 1 updated
$ tbd --dry-run integration sync # immediately after
linear: would pull 13, warnings 5
```
An operator following the documented session-closing protocol (`tbd sync`, confirm, done) will believe the tracker is reconciled when it is not. This is the most damaging of the three, because it is silent.
## 2. A converged pair never settles — the direction alternates forever
Running the integration surface explicitly does not converge. Each run reverses the direction of the *same* 13 items:
```
$ tbd --dry-run integration sync
linear: would pull 13, warnings 5
$ tbd integration sync
linear: pull 13, warnings 5
$ tbd --dry-run integration sync # immediately after the pull
linear: would push 13, warnings 5
```
Push and pull trade places indefinitely; the count stays at 13. `nothing to do` is never reached.
**The 13 items are not actually divergent.** Verified independently against tbd's own bookkeeping in `.tbd/data-sync/bridge/linear/`:
| Check | Result |
| --- | --- |
| Link records present | 136 / 136 |
| `base` (title, status, priority) vs current bead | **0 differ** |
| `base.slot` vs `refinement_slot` | **0 differ** |
| `remote_updated_at` vs live Linear `updatedAt` | **0 differ** (compared as instants) |
| Pending intents in `bridge/linear/intents/` | **0** |
| Beads carrying comments | **0** |
And independently against the Linear API — all 136 pairs agree on title, status, priority, and archived state; managed `⟦tbd⟧` blocks are well-formed (exactly one open and one close marker) and their content matches the bead.
So every field the reconciler tracks agrees, its own recorded base agrees, and the remote timestamp it stored matches the remote exactly — yet 13 pairs are reported dirty on every run, in whichever direction was not taken last.
Relevant config (`field_sync`): `title/description/status/priority: merge`, `labels/assignee: local`, `comments: two_way`, `tie_break: newest`. `inbound.mode: report`, `outbound.kinds: [epic]`.
A plausible shape, given the perfect alternation: a write bumps the counterpart's modification time, the next run reads that as "the other side changed", and `tie_break: newest` hands the decision to whichever side was written last — a loop with no fixed point. I have not confirmed that mechanism; the measurements above are what I can stand behind.
## 3. `--pull` reports a push
Direction flags are documented as `--push` outbound-only, `--pull` inbound-only. The inbound-only dry run announces a push:
```
$ tbd --dry-run integration sync --pull
linear: would push 13, warnings 5
```
Bare and `--pull` print identical summaries, so the direction word appears to be unrelated to the direction actually planned. Separately, `--push` reports in a different vocabulary and a different magnitude on the same state — `would create 0, would update 136` (i.e. every linked bead) versus the bare run's `13`. With three mutually inconsistent numbers for one state, an operator cannot tell what a sync will do.
## Why this matters together
Symptom 1 means the tracker silently drifts. Symptom 2 means an operator who notices and runs the surface by hand still never sees it finish. Symptom 3 means the output cannot be used to work out which way the data is about to move — and given #244's point that a wrong close writes real `completedAt` data into Linear, "which way is this about to move" is not an idle question.
Happy to run further diagnostics against this repository; it is a large, live mirror and reproduces every time.
Related: #244 (terminal-state model). Distinct from it — this is the reconciler failing to reach a fixed point on data that already agrees.
Contributor guide
No contributing guide indexed for this repository
Research direction
Reproduce the three behaviors with `tbd sync`, `tbd --dry-run integration sync`, and the `--pull` variant against the Linear mirror. Inspect the recorded state under `.tbd/data-sync/bridge/linear/` and the `field_sync` configuration, then trace integration sync planning and reporting. Done means the Linear surface is reported, agreeing data reaches `nothing to do`, and direction flags produce consistent summaries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, distributed-systems
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100