1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
//! §12/§13 remote ops: `sync` and `push`, both over ONE reconcile (bl-3616 §3,
//! bl-21ab). Currency is OPTIMISTIC (mutate → push, bl-336a): there is no
//! pre-pull — a stale store surfaces atomically as the push's non-ff reject,
//! and the reject is where the reconcile runs: fetch, `git rebase` the local
//! seals onto the remote tip IN the store checkout (never `update-ref` plumbing
//! behind it — the checkout IS the branch, §8/bl-057a), push once more. Every
//! seal is one commit touching one `tasks/<id>.md`, so seals on DIFFERENT balls
//! rebase clean by construction and the op lands with no human in the loop;
//! only a SAME-ball race conflicts, and that is E5 with the ball named. `sync`
//! is the same reconcile over every unpublished seal, the one place a store
//! publishes when the tracker is not wired on `*.post`. Transport failure
//! fails OPEN in both (warn; the store stays ahead, drift renders it); only a
//! same-ball conflict or a reject the reconcile cannot clear (permissions, a
//! server hook) stays fail-closed. Both are no-ops in a stealth (no-remote)
//! repo: with no remote there is nothing to talk to (§12).
use git;
use Binding;
use Env;
use cratereject_option_like;
use io;
use Path;
// The reconcile's spoken outcomes — the same-ball refusal and the fail-open
// warning — live in a sibling so this file stays under the 300-line cap.
use ;
/// §13 `sync/pre`: the general rule — fetch the branch's UPSTREAM, **if any**,
/// then reconcile THAT branch. "If any" is read from the remote
/// ([`remote_has_branch`], the same ls-remote that decides prime's
/// adopt-vs-found): an upstream-less branch — the landing by construction (§4),
/// any local-only branch — yields a no-op *for free*, no name special-cased; an
/// unreachable remote fails OPEN ([`fail_open`]). The target is the branch the
/// binding NAMES, never whatever the store checkout happens to have checked
/// out: the store's own branch goes through [`reconcile`] (the working tree
/// moves with it, and its unpublished seals publish); any other branch is a
/// pure ref move via the `<branch>:<branch>` refspec (ff-only by git's own
/// default). A partial sync leaves the branch at the old or the new tip, never
/// wedged (§13 rollback) — a conflicted rebase is aborted before returning.
/// §12 `*/post`: publish the just-sealed balls branch to the remote — always to
/// an ESTABLISHED store (founding is `prime`'s alone, §12). A rejected push is
/// the optimistic contention check firing, and it is answered by ONE
/// [`reconcile`] — the post-reject pull on the contended path only, so the
/// happy path still pays no round-trip (§12's "deliberately NO pre-pull"
/// stands). There is exactly one local seal on this path, the in-flight op's:
/// a clean rebase means the contention was on a DIFFERENT ball (the common
/// case with many agents) and the op lands; a conflict means the SAME ball
/// changed on both sides — the rebase is aborted, the non-zero exit ABORTS the
/// op (core un-seals), and the sentence names the ball. NEVER a silent stealth
/// degrade, which would be split-brain (contrast `prime`'s founding-miss
/// fallback, where nothing existed to land on).
///
/// **An op does not publish an anvil an enclosing op holds open (bl-1266,
/// store-scoped since bl-aac7).** A plugin that shells `bl` on THIS store
/// (bl-chore's `claim.post` mint was the shipped case) inserts a whole op —
/// seal AND push — into the middle of its parent's post phase, so without this
/// the nested push publishes the PARENT's not-yet-final commit; a later
/// `claim.post` failure then un-seals only the LOCAL store (`git reset --hard`),
/// and the next `bl sync` fast-forwards the repudiated op straight back. Nothing
/// is lost by waiting: a push publishes a branch TIP, so the nested seal rides
/// the parent's own trailing push (the tracker sorts LAST, §14) — one push per op
/// TREE, still last, and §14's *"core never pushes, so there is nothing remote to
/// chase"* becomes a theorem instead of an accident of hook order. The predicate
/// is the held-store chain, not depth ([`Env::nested`]): a nested `bl -C` on a
/// store no enclosing op holds has no parent push to ride, so it publishes.
/// THE reconcile (bl-3616 §3 / bl-21ab): fetch the remote tip, `git rebase` the
/// store's unpublished seals onto it IN the checkout, push. Shared by the
/// per-op reject path ([`push`] — one seal) and [`sync`] (every seal). Four
/// outcomes, each stated once:
/// - the remote is UNREACHABLE — fail OPEN ([`fail_open`]): the store stays
/// ahead, nothing is lost, the next reachable op or `sync` publishes;
/// - the tip is not a store — the §16 migration window ([`warn_legacy`]),
/// work stays local, the legacy ref is never rewritten;
/// - the rebase CONFLICTS — the same ball changed on both sides. Abort the
/// rebase (the local seals are exactly as they were), name the ball, and
/// refuse ([`conflict`]) — the one contention that is semantically meaningful
/// and must not be auto-resolved: two claims of one ball IS claim contention
/// (the later loses), a close against a remote update is a human's call. No
/// field-wise merge, no CRDT (§7);
/// - clean — publish, unless an enclosing op holds this store open
/// (`Env::nested`, bl-1266); a push still rejected after a clean rebase is not
/// contention (a re-race — re-run — or a denied push), and stays fail-closed.
///
/// An unpublished seal's sha is scratch across this (bl-3616 §6.1): content,
/// trailers, timestamps and order survive the rebase, commit ids do not, and
/// nothing on `main` pins one — the seen-token is a BLOB sha, the journal
/// walks by path. The tracker sorts LAST (§14), so no plugin sees the
/// pre-rebase `commit` after this runs.
/// A push just landed: the remote tip IS this head — set the publication mark
/// (bl-439d) so the drift render reads current.
/// Is `remote`'s `branch` tip NOT a store — no `tasks/` tree at its root? That
/// is the §16 migration window (bl-868d): a hub still carrying the
/// PRE-greenfield legacy JSON store on the (colliding, §16) store-branch name,
/// awaiting the runbook's one-time human cutover. Such a tip is no upstream at
/// all — every store TIP carries `tasks/` by construction (§2, the founding
/// `.gitkeep`; the §16 cutover join keeps the greenfield tree) — so a failed
/// integrate/publish against it is the window, not contention: warn (the §12
/// diagnostic-never-authority pattern) and report `true` so the caller skips,
/// keeping work local and the legacy ref intact (cutover is the runbook's
/// explicit history join + fast-forward push, never a rewrite).
/// Identification must be POSITIVE: the tip is re-fetched here (`FETCH_HEAD`),
/// and any failure to read it reports `false` — the caller's own error stands.
pub
/// The §16 migration-window warning — one spelling, shared by every site that
/// positively identified a legacy (non-store) tip.
/// Is `remote`'s `branch` tip a greenfield STORE (`tasks/` at its root, §2)?
/// `None` = the tip could not be read at all (unreachable remote, absent
/// branch) — the caller decides; `Some(false)` is the §16 legacy window
/// [`not_yet_cut_over`] warns about; `Some(true)` is an ESTABLISHED store.
/// Fetch `remote`'s `branch` to `FETCH_HEAD` and read its shape: `Ok(true)` an
/// established store (`tasks/` at the tip), `Ok(false)` a legacy tip, `Err` the
/// fetch itself failed (unreachable remote, absent branch) — the transport
/// signal [`reconcile`] fails open on. Positive identification by re-fetch,
/// shared by every reject-interpretation site.
/// Does `remote` already carry `branch`? `git ls-remote --heads` is the one
/// round-trip that answers "an upstream, if any" — sync's no-op gate and
/// prime's adopt-vs-found / clone-vs-bootstrap signal (§12/§13).
pub
/// §6/§13 `install/pre`: fetch the center's config branch (`balls/config`,
/// [`crate::LANDING_BRANCH`]) into the LANDING repo so core can MATERIALIZE it
/// locally and copy it in. The tracker is balls' only remote-talker — core never
/// fetches (§0) — so `prime --install`'s remote read rides this hook. It leaves
/// the config at the landing's `FETCH_HEAD` (a git-standard ref, so no invented
/// core↔plugin convention); core reads it from the same checkout. This is a READ
/// only — config adoption is destructive on the LANDING, never a push to the
/// center (publishing is `install --to`, a separate direction). Stealth (no
/// remote) is a no-op, like every handler — and so is a present remote that
/// simply LACKS the ref (bl-45fd): bl never publishes the landing (§4
/// single-owner), so a stock hub carries no `balls/config`, and a purely local
/// install must not depend on remote state. The gate is sync's own
/// [`remote_has_branch`] ("an upstream, if any", §13); an adopt that really
/// needs the center's config fails at point-of-use (no `FETCH_HEAD`).