sprawling 0.0.12

A local multi-agent harness for persistent agent teams: long-running project work, agent communication, and experiments in coordination and social simulation.
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! Every tool a person needs to develop this code (`crates/sprawling/spec/Doctor.lean`
//! §8-58).
//!
//! Data, with no branch in it. `table::REQUIREMENTS` lists these rows in
//! install order, because a later row's recipe starts a program an
//! earlier row installs: `cargo install` needs rustup, `elan toolchain
//! install` needs elan, `uv python install` needs uv. `just prereqs`
//! reads the same rows through `prereqs.tsv`, which `table::prereqs`
//! renders.
//!
//! `Required` means `just check` cannot run without it; `Optional` means
//! `just check` skips it when it is absent, or only a recipe a person
//! runs on purpose calls it, and the row says which recipe that is.

use wire::DoctorUnread;

use crate::doctor::family::{CHROMIUM_RECIPE, Family};
use crate::doctor::pin::LEAN_TOOLCHAIN as LEAN_PIN;
use crate::doctor::{Detection, Need, Pack, PerPlatform, Pin, Recipe, Requirement, Tier, Upstream};

use super::NOWHERE;

/// One command, spelled once and answered on all three platforms.
/// Every tool below that installs the same way everywhere uses it, so
/// the three columns are never one line copied three times.
const fn same_command(program: &'static str, args: &'static [&'static str]) -> PerPlatform<Recipe> {
    PerPlatform {
        windows: Recipe::Command { program, args },
        macos: Recipe::Command { program, args },
        linux: Recipe::Command { program, args },
    }
}

/// A program on the search path, asked its version with `--version`.
const fn program(name: &'static str) -> Detection {
    Detection::Program {
        program: name,
        version_arg: "--version",
        places: NOWHERE,
    }
}

/// A row of this tier; everything but the facts that differ per tool.
const fn row(
    name: &'static str,
    need: Need,
    enables: &'static str,
    detect: Detection,
    homepage: &'static str,
    recipe: PerPlatform<Recipe>,
) -> Requirement {
    Requirement {
        name,
        tier: Tier::Develop,
        need,
        enables,
        detect,
        homepage: Some(homepage),
        recipe,
        pin: Pin::Unpinned,
        upstream: Upstream::Unread(DoctorUnread::NoSource),
        pack: None,
    }
}

/// The three facts a row states beyond its detection and its recipe,
/// chained onto `row` so that each row says only the ones it has.
impl Requirement {
    const fn from(mut self, upstream: Upstream) -> Requirement {
        self.upstream = upstream;
        self
    }

    const fn pinned(mut self, pin: Pin) -> Requirement {
        self.pin = pin;
        self
    }

    /// A cargo tool this repository calls, installed from its crate.
    const fn packed(mut self, krate: &'static str) -> Requirement {
        self.pack = Some(Pack::RustTools);
        self.upstream = Upstream::Crate(krate);
        self
    }
}

pub(super) const GIT: Requirement = row(
    "git",
    Need::Required,
    "version control for this repository, and restoring a discarded file from its checkpoint",
    Detection::Program {
        program: "git",
        version_arg: "--version",
        // Where winget puts it, so the row answers before this process
        // has seen the search path the installer changed.
        places: PerPlatform {
            windows: &[r"C:\Program Files\Git\cmd\git.exe"],
            macos: &[],
            linux: &[],
        },
    },
    "https://git-scm.com/",
    PerPlatform {
        windows: Recipe::Command {
            program: "winget",
            args: &["install", "--id", "Git.Git", "-e", "--scope", "user"],
        },
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "git"],
        },
        linux: Recipe::Print("sudo apt install git"),
    },
)
.from(Upstream::GitHub("git-for-windows/git"));

/// The shell every recipe of this repository runs in: the `justfile`
/// opens with `set shell := ["bash", "-uc"]`. The listing asks for the
/// line only a real bash prints, so the WSL launcher a Windows search
/// path can resolve first is reported as absent, which is what `just`
/// would meet (`crates/sprawling/spec/Doctor.lean` §8-130).
pub(super) const BASH: Requirement = row(
    "bash",
    Need::Required,
    "`just`, which runs every recipe of this repository in bash",
    Detection::Listed {
        program: "bash",
        args: &["--version"],
        line: "GNU bash",
    },
    "https://www.gnu.org/software/bash/",
    PerPlatform {
        windows: Recipe::Manual(concat!(
            "run `just` from Git Bash, which the git row installs: its own bash comes first ",
            r"there, while elsewhere C:\Windows\System32\bash.exe can come first and starts ",
            "WSL rather than a shell",
        )),
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "bash"],
        },
        linux: Recipe::Print("sudo apt install bash"),
    },
);

pub(super) const RUSTUP: Requirement = row(
    "rustup",
    Need::Required,
    "cargo and the Rust toolchain rust-toolchain.toml pins, fetched on first use",
    program("rustup"),
    "https://rustup.rs/",
    PerPlatform {
        windows: Recipe::Command {
            program: "winget",
            args: &["install", "--id", "Rustlang.Rustup", "-e"],
        },
        macos: Recipe::Print("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"),
        linux: Recipe::Print("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"),
    },
)
.from(Upstream::RustupChannel);

/// The compiler itself. `rustup default stable` puts one on the search
/// path; inside this repository rustup then fetches the version
/// `rust-toolchain.toml` pins on first use.
pub(super) const RUST: Requirement = row(
    "rust",
    Need::Required,
    "the compiler, at the version rust-toolchain.toml pins, which rustup fetches on first use",
    program("rustc"),
    "https://www.rust-lang.org/",
    same_command("rustup", &["default", "stable"]),
)
.from(Upstream::RustChannel)
.pinned(Pin::RustToolchain);

pub(super) const RUSTFMT: Requirement = row(
    "rustfmt",
    Need::Required,
    "`cargo fmt --check`, the first step of `just check`",
    program("rustfmt"),
    "https://github.com/rust-lang/rustfmt",
    same_command("rustup", &["component", "add", "rustfmt"]),
)
.from(Upstream::Unread(DoctorUnread::WithToolchain));

pub(super) const CLIPPY: Requirement = row(
    "clippy",
    Need::Required,
    "`cargo clippy -D warnings`, which is where this workspace's lints bite",
    program("cargo-clippy"),
    "https://doc.rust-lang.org/clippy/",
    same_command("rustup", &["component", "add", "clippy"]),
)
.from(Upstream::Unread(DoctorUnread::WithToolchain));

pub(super) const JUST: Requirement = row(
    "just",
    Need::Required,
    "`just check`, the closing condition of every change here",
    program("just"),
    "https://just.systems/",
    PerPlatform {
        windows: Recipe::Command {
            program: "winget",
            args: &["install", "--id", "Casey.Just", "-e", "--scope", "user"],
        },
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "just"],
        },
        linux: Recipe::Command {
            program: "cargo",
            args: &["install", "just", "--locked"],
        },
    },
)
.from(Upstream::Crate("just"));

pub(super) const CARGO_NEXTEST: Requirement = row(
    "cargo-nextest",
    Need::Required,
    "`just test`, the test runner every check here calls",
    program("cargo-nextest"),
    "https://nexte.st/",
    same_command("cargo", &["install", "cargo-nextest", "--locked"]),
)
.packed("cargo-nextest");

pub(super) const CARGO_DENY: Requirement = row(
    "cargo-deny",
    Need::Optional,
    "`just deny`, the supply-chain read `just gates` ends with; CI runs it on every push",
    program("cargo-deny"),
    "https://embarkstudios.github.io/cargo-deny/",
    same_command("cargo", &["install", "cargo-deny", "--locked"]),
)
.packed("cargo-deny");

pub(super) const BUN: Requirement = row(
    "bun",
    Need::Required,
    "`just build-web` and the client's checks, which the artifact gates judge",
    program("bun"),
    "https://bun.sh/",
    PerPlatform {
        windows: Recipe::Command {
            program: "winget",
            args: &["install", "--id", "Oven-sh.Bun", "-e", "--scope", "user"],
        },
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "oven-sh/bun/bun"],
        },
        linux: Recipe::Print("curl -fsSL https://bun.sh/install | bash"),
    },
)
.from(Upstream::GitHub("oven-sh/bun"));

/// The browser the render gate measures the built client in. The same
/// family and recipe as the browser tool's Chromium row, under the tier
/// that needs it for another reason.
pub(super) const RENDER_BROWSER: Requirement = row(
    "render-browser",
    Need::Required,
    "`cargo xtask render`, which measures the built client in a headless Chromium browser",
    Detection::Family(Family::Chromium),
    "https://www.chromium.org/",
    CHROMIUM_RECIPE,
)
.from(Upstream::Unread(DoctorUnread::ManyBrands));

/// elan has no winget package, and its official installers are
/// scripts. On Windows this city runs `elan-init.ps1` without a prompt
/// once the person presses install, because a fresh machine has to
/// finish in one pass; the Linux line stays printed (`crates/sprawling/spec/Doctor.lean`
/// §8-58). `-DefaultToolchain none` leaves the Lean version to
/// the `lean` row, which installs the one `lean-toolchain` pins.
pub(super) const ELAN: Requirement = row(
    "elan",
    Need::Required,
    "the Lean toolchain manager `just models` and `just adversary` run through",
    program("elan"),
    "https://github.com/leanprover/elan",
    PerPlatform {
        windows: Recipe::Command {
            program: "powershell",
            args: &[
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
                "-Command",
                "& ([scriptblock]::Create((irm \
                 https://raw.githubusercontent.com/leanprover/elan/master/elan-init.ps1))) \
                 -NoPrompt 1 -DefaultToolchain none",
            ],
        },
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "elan-init"],
        },
        linux: Recipe::Print(
            "curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | sh",
        ),
    },
)
.from(Upstream::GitHub("leanprover/elan"));

/// What `elan toolchain install` is handed for one pin: the toolchain
/// `lean-toolchain` names, or `stable` when the build found no pin file,
/// because elan refuses an empty toolchain name (`crates/sprawling/spec/Doctor.lean` §8-162).
const fn lean_install(pin: &'static str) -> &'static str {
    if pin.is_empty() { "stable" } else { pin }
}

/// The toolchain this build installs for the `lean` row.
const LEAN_INSTALL: &str = lean_install(LEAN_PIN);

pub(super) const LEAN: Requirement = row(
    "lean",
    Need::Required,
    "`just models`, which proves every crate's Lean specification, at the version lean-toolchain pins",
    Detection::Listed {
        program: "elan",
        args: &["toolchain", "list"],
        line: LEAN_PIN,
    },
    "https://lean-lang.org/",
    same_command("elan", &["toolchain", "install", LEAN_INSTALL]),
)
.from(Upstream::GitHub("leanprover/lean4"))
.pinned(Pin::LeanToolchain);

/// The Zig version `crates/desktop/ffi/zig-version` pins, the one file the
/// leaf's build script and CI's install step read too (`crates/sprawling/spec/Doctor.lean`
/// §8-146).
const ZIG_PIN: &str = crate::doctor::pin::ZIG_VERSION;

/// winget's arguments for Zig at one pin, without `--version` when the
/// build found no pin file, because winget refuses an empty version
/// (`crates/sprawling/spec/Doctor.lean` §8-162). A macro rather than a `const fn`: a recipe's
/// arguments are a `'static` slice, which a `const fn` cannot build out of
/// its parameter, while a macro expands into a `const` item where the pin
/// is a constant.
macro_rules! winget_zig {
    ($pin:expr) => {
        if $pin.is_empty() {
            &["install", "--id", "zig.zig", "-e", "--scope", "user"]
        } else {
            &[
                "install",
                "--id",
                "zig.zig",
                "-e",
                "--version",
                $pin,
                "--scope",
                "user",
            ]
        }
    };
}

/// The compiler of the two Zig leaves. Required on every platform: the
/// console's renderer, `crates/console_ffi`, is compiled into every build
/// of this binary, and the desktop server's leaf into every Windows build
/// (`crates/sprawling/spec/Doctor.lean` §8-146).
pub(super) const ZIG: Requirement = row(
    "zig",
    Need::Required,
    "the Zig leaves: crates/console_ffi on every platform and crates/desktop/ffi on Windows, at the version crates/desktop/ffi/zig-version pins",
    Detection::Listed {
        program: "zig",
        args: &["version"],
        line: ZIG_PIN,
    },
    "https://ziglang.org/",
    PerPlatform {
        windows: Recipe::Command {
            program: "winget",
            args: winget_zig!(ZIG_PIN),
        },
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "zig"],
        },
        linux: Recipe::Manual(
            "install the Zig crates/desktop/ffi/zig-version names from https://ziglang.org/download/",
        ),
    },
);

pub(super) const UV: Requirement = row(
    "uv",
    Need::Optional,
    "the Python this repository's probes and maintenance scripts run on",
    program("uv"),
    "https://docs.astral.sh/uv/",
    PerPlatform {
        windows: Recipe::Command {
            program: "winget",
            args: &["install", "--id", "astral-sh.uv", "-e", "--scope", "user"],
        },
        macos: Recipe::Command {
            program: "brew",
            args: &["install", "uv"],
        },
        linux: Recipe::Print("curl -LsSf https://astral.sh/uv/install.sh | sh"),
    },
)
.from(Upstream::GitHub("astral-sh/uv"));

/// Present when uv can name an interpreter, whichever installed it.
pub(super) const PYTHON: Requirement = row(
    "python",
    Need::Optional,
    "Python for the probes and data transforms, installed and managed by uv",
    Detection::Listed {
        program: "uv",
        args: &["python", "find"],
        line: "",
    },
    "https://www.python.org/",
    same_command("uv", &["python", "install"]),
)
.from(Upstream::PythonOrg);

pub(super) const CARGO_MUTANTS: Requirement = row(
    "cargo-mutants",
    Need::Optional,
    "`just mutants`, which asks whether the tests notice a change in a module held to a mutation score",
    program("cargo-mutants"),
    "https://mutants.rs/",
    same_command("cargo", &["install", "cargo-mutants", "--locked"]),
)
.packed("cargo-mutants");

pub(super) const CARGO_FUZZ: Requirement = row(
    "cargo-fuzz",
    Need::Optional,
    "`just fuzz <target>`, which needs a nightly toolchain as well",
    program("cargo-fuzz"),
    "https://rust-fuzz.github.io/book/cargo-fuzz.html",
    same_command("cargo", &["install", "cargo-fuzz", "--locked"]),
)
.packed("cargo-fuzz");

pub(super) const KANI: Requirement = row(
    "kani",
    Need::Optional,
    "`just proof`, proving a property against real MIR, on Linux and nowhere else today",
    program("cargo-kani"),
    "https://model-checking.github.io/kani/",
    PerPlatform {
        windows: Recipe::Manual("kani runs on Linux; a WSL installation is where it goes here"),
        macos: Recipe::Manual("kani runs on Linux; this platform has no build today"),
        linux: Recipe::Print("cargo install --locked kani-verifier && cargo kani setup"),
    },
)
.packed("kani-verifier");

#[cfg(test)]
mod tests {
    use super::lean_install;

    /// A build from the crates.io archive finds none of the pin files, so
    /// both pins are empty there. The two rows then install a version
    /// nobody pinned rather than a command with an empty argument, which
    /// elan and winget both refuse (`crates/sprawling/spec/Doctor.lean` §8-162).
    #[test]
    fn a_build_without_pins_installs_lean_stable_and_any_zig() {
        const UNPINNED: &[&str] = winget_zig!("");
        const PINNED: &[&str] = winget_zig!("0.16.0");
        let any: &[&str] = &["install", "--id", "zig.zig", "-e", "--scope", "user"];
        let one: &[&str] = &[
            "install",
            "--id",
            "zig.zig",
            "-e",
            "--version",
            "0.16.0",
            "--scope",
            "user",
        ];
        assert_eq!(
            (
                lean_install(""),
                lean_install("leanprover/lean4:v4.33.1"),
                UNPINNED,
                PINNED
            ),
            ("stable", "leanprover/lean4:v4.33.1", any, one)
        );
    }
}