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
//! The `tracker` plugin — balls' one remote-talker (§0/§12/§13).
//!
//! Base balls is local-only: it commits task-file changes to the balls branch
//! and never touches a remote. Everything beyond that is a plugin, and the
//! tracker is the plugin that owns the remote. It is a SEPARATE binary, invoked
//! subprocess-uniform (`<bin> <op> <phase>`, §6) with the §7 wire on stdin and
//! no return channel — in-repo only as a default capability + reference impl.
//!
//! Its whole job is git acts on the state branch:
//! - [`remote_ops::sync`] — `sync/pre`: the reconcile over every unpublished
//! seal — fetch, rebase the store checkout onto the remote tip, push (§13;
//! bl-3616 §3). A same-ball conflict is the contention signal, named.
//! - [`remote_ops::push`] — `*/post`: publish the sealed balls branch (§12); a
//! non-ff reject runs the same reconcile once, over the one in-flight seal.
//! - [`prime::prime`] — `prime/pre`: settle the store name, clone an established
//! remote branch into a local ref, or stop SILENTLY when stealth (§12).
//! - [`prime::prime_post`] — `prime/post`: settle content — fetch-ff an
//! established remote then push, or found an absent branch by pushing (§12).
//!
//! The wire's [`Binding`] is everything it needs — `remote` + `tasks_branch`
//! name the store upstream DIRECTLY, with no trail to walk (§12). When the binding
//! carries no explicit `remote` (core resolves only `--remote`/XDG, the
//! config tiers — §0 keeps it local-only), the tracker discovers the project-repo
//! `origin` as its single fallback ([`effective_remote`], resolved once at the
//! [`handle`] dispatch point). Each handler no-ops in a stealth repo — no explicit
//! remote AND no discoverable origin, or a binding DECLARED stealth (the landing
//! `task_remote` sentinel, written by `bl prime --stealth` and re-derived by core
//! on every op, bl-9df0) — the structural opt-out (§12).
mod drift;
mod git;
mod payload;
mod prime;
mod remote_ops;
#[cfg(test)]
mod fixtures;
pub use payload::Binding;
use serde::Serialize;
use std::io::{self, Read, Write};
use std::path::Path;
/// The host-resolved environment the binary edge hands the tracker: the XDG
/// roots that locate this checkout's clone bundle (§1), and the chain of store
/// checkouts the `bl`s in this invocation tree hold open (`$BALLS_HELD_STORES`,
/// §6). No env reads in the lib — the edge resolves them once (the bl-bfa8
/// rule) and passes them in.
pub struct Env {
pub xdg: crate::layout::Xdg,
/// `$BALLS_HELD_STORES` split like `$PATH`: every `bl` in the invocation
/// tree, outermost first, ending with the one that spawned this plugin —
/// see [`Env::nested`] for the read that turns it into a publish decision.
pub held: Vec<std::path::PathBuf>,
}
impl Env {
/// Assemble from the raw boundary values, so the chain PARSE lives in the
/// library where unit tests reach every branch (the bl-bfa8 rule, as
/// [`crate::edge::Edge::resolve`] does it for core). An absent variable is
/// an empty chain — it FAILS OPEN, publishing: a plugin run by hand, or by a
/// core too old to set the variable, must not silently stop federating.
#[must_use]
pub fn resolve(xdg: crate::layout::Xdg, held: Option<std::ffi::OsString>) -> Self {
Self { xdg, held: held.map(|h| std::env::split_paths(&h).collect()).unwrap_or_default() }
}
/// Whether an ENCLOSING `bl` holds `store` open — the §12 rung that decides
/// [`remote_ops::push`] (bl-1266), store-scoped since bl-aac7.
///
/// The read, stated once: core exports the chain with its OWN store appended
/// LAST (`crate::plugin::held_chain`), so the final entry is always the
/// spawning op's — the wire's `binding.store` — and the enclosing set is
/// everything before it. `store` in that prefix reads exactly "an op above
/// the one that invoked me has this anvil open", which is the condition
/// under which its seal is not yet this op's to publish; a nested `bl -C`
/// on a store nobody above holds is NOT nested here and publishes (the
/// bl-1266 H1 fill). Depth is no longer consulted — it is the §6 recursion
/// cap alone.
#[must_use]
pub fn nested(&self, store: &str) -> bool {
let enclosing = &self.held[..self.held.len().saturating_sub(1)];
enclosing.iter().any(|h| h == Path::new(store))
}
}
/// The ops the tracker handles, for the §6 `protocol` self-description: the
/// deliverable verbs — `comment` among them: an `update` specialization with
/// its own hook key, so wiring `comment.post` must be admitted (bl-cca0) —
/// plus `import` (it pushes on their `post` — imported records sync like any
/// mutate, §16), `sync`/`prime`, `install` (it fetches
/// the center's config on `install/pre`, §13), and the reads `show`/`list`
/// (the drift render, bl-439d).
const OPS: &[&str] = &[
"create", "claim", "unclaim", "update", "comment", "close", "import", "sync", "prime", "install", "show", "list",
];
/// The §6 self-description emitted by `tracker protocol`. balls never persists
/// it; it is read at install time to validate a binding.
#[derive(Serialize)]
struct SelfDescription {
protocol: u32,
ops: &'static [&'static str],
}
/// The tracker entrypoint: dispatch `args` (`protocol`, or `<op> <phase>` with
/// the §7 payload on `input`), returning the process exit code. A handler error
/// is logged to stderr and becomes exit `1` — the §6 "non-zero aborts the op".
pub fn run(args: &[String], input: &mut impl Read, out: &mut impl Write, env: &Env) -> i32 {
match dispatch(args, input, out, env) {
Ok(()) => 0,
Err(e) => {
eprintln!("tracker: {e}");
1
}
}
}
fn dispatch(args: &[String], input: &mut impl Read, out: &mut impl Write, env: &Env) -> io::Result<()> {
match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
// Beside `protocol` — a question about the binary, not about an op. A
// sibling states its own version and no other's ([`crate::version`]).
["--version" | "-V"] => writeln!(out, "{}", crate::version::plugin_line("bl-tracker")),
["protocol"] => protocol(out),
[op, phase] => handle(op, phase, input, out, env),
_ => Err(io::Error::other("usage: tracker --version | tracker protocol | tracker <op> <phase>")),
}
}
/// Emit the §6 `{ protocol, ops }` self-description as JSON.
fn protocol(out: &mut impl Write) -> io::Result<()> {
let desc = SelfDescription { protocol: crate::message::PROTOCOL, ops: OPS };
serde_json::to_writer(&mut *out, &desc).map_err(io::Error::other)?;
out.write_all(b"\n")
}
/// Route one `<op> <phase>` to its handler. The tracker acts in six slots —
/// `sync/pre`, `prime/pre` (settle name + clone-in), `prime/post` (settle content
/// — the reconcile, bl-0a23/bl-21ab), `install/pre` (the §13 config fetch), any
/// deliverable verb's `post` (the push, then the op-ball drift line on stderr,
/// bl-439d), and the `show`/`list` reads (the drift line folded into the render)
/// — and no-ops everywhere else. `prime/post` is matched out explicitly BEFORE
/// the `sync`/`prime`/`install` catch-all so it reaches its own content handler;
/// that catch-all then keeps `sync`/`install` from triggering the generic `post`
/// push: in particular `install` adopts config INTO the local landing (a fetch),
/// and must NEVER push the landing back out (publishing is a separate direction,
/// §6/§13).
fn handle(op: &str, phase: &str, input: &mut impl Read, out: &mut impl Write, env: &Env) -> io::Result<()> {
let payload::Input { mut binding, id } = payload::read_input(input)?;
binding.remote = effective_remote(&binding);
match (op, phase) {
("sync", "pre") => remote_ops::sync(&binding, env),
("prime", "pre") => prime::prime(&binding, env),
("prime", "post") => prime::prime_post(&binding, env),
("install", "pre") => remote_ops::fetch_config(&binding),
("sync" | "prime" | "install", _) => Ok(()),
("show" | "list", "read") => drift::render(op, &binding, id.as_deref(), out),
(_, "post") => {
remote_ops::push(&binding, env)?;
// The op's own drift, in the op's scope (bl-3616 Q5): quiet at zero
// (a mandatory push), a count when the seal stayed local (opt-in
// wiring, an unreachable remote), nothing in stealth or when an
// enclosing op will publish for this one (the count would be its).
if binding.remote.is_some() && !env.nested(&binding.store) {
if let Some(line) = drift::op_line(Path::new(&binding.store), id.as_deref()) {
eprintln!("tracker: {line}");
}
}
Ok(())
}
_ => Ok(()),
}
}
/// The effective store remote for this op (§12): the EXPLICIT remote core already
/// resolved (`--remote`/XDG `remote`, on the binding), else the
/// auto-discovered project-repo `origin`. Implicit `origin` discovery is the
/// TRACKER's alone — core stays local-only (§0) and hands a `remote: None`
/// binding when no explicit tier is set. Resolved ONCE here at the [`handle`]
/// dispatch point and written back onto `binding.remote`, so every handler shares
/// this one fallback and reads `binding.remote` as before — the stealth gate
/// ("no remote ⇒ no-op") thus means "no explicit remote AND no discoverable
/// origin", with no per-handler re-probe. A binding carrying `stealth` — the
/// landing `task_remote` sentinel core derives on EVERY op (§12, bl-9df0) — is
/// DECLARED stealth: resolve no
/// remote at all, so even a discoverable `origin` is never founded or pushed.
fn effective_remote(b: &Binding) -> Option<String> {
if b.stealth {
return None;
}
b.remote.clone().or_else(|| origin_of(Path::new(&b.invocation_path)))
}
/// The auto-discovered store remote — `git remote get-url origin` on the PROJECT
/// repo (the `invocation_path`, the clone the user works in, whose `origin` is the
/// real upstream the code rides and where `balls/tasks` sits alongside it). A
/// LOCAL config read, no network. NEVER the landing: the landing is local-only
/// (§2 install-transport, founded by a bare `git init`) and carries no origin.
/// Absent origin (the stealth case, or a non-repo path) ⇒ `None`.
fn origin_of(project: &Path) -> Option<String> {
git::git(project, &["remote", "get-url", "origin"]).ok()
}
#[cfg(test)]
#[path = "tracker_tests.rs"]
mod tests;