sprawling-runtime 0.0.10

One run of a sprawling resident, from dispatch to freeze: turns, tools, replay and the sandbox that executes code.
Documentation
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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
// 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

//! Run modes and admission. A run sits in exactly one mode; the
//! catalog lists only that one (progressive disclosure — the other
//! mode's text stays out of the window).
//!
//! One exception, and it is one line long: [`dev_entry`] tells a run
//! that this city's own code is changeable, and names the evidence a
//! change can be asked to carry. Without it an agent working in this
//! city never learns that the city's own code and SPECs are changeable
//! at all, or under what discipline — and a capability nobody is told
//! about is one nobody uses.
//!
//! The half that decides is [`admits`]: it says whether what a run
//! produced may land, given the run policy it was dispatched under
//! (`crates/runtime/spec/Mode.lean` §8-54). The evidence arrives as plain answers rather
//! than as an instrument's type, because the instruments live in
//! citysim, outside this crate, and the question here is not how
//! evidence was gathered but whether enough of it exists.

use crate::catalog::CatalogEntry;
use kernel::{AdmissionRequirement, AxCode, AxError, LandingPolicy, Mode, RunPolicy};
use std::sync::{Arc, PoisonError, RwLock};

/// The name of the catalog row that opens the developer discipline.
pub const DEV_ENTRY: &str = "dev";

/// The row that tells a run this city is changeable, and nothing more.
///
/// One line in the resident segment, and the whole discipline behind an
/// expansion. That split is the point: most sessions never change this
/// city, so they pay a line; a session that is about to change it asks
/// once and gets the evidence a change can be asked for, the reading
/// order and what to do next. The same progressive disclosure the tool
/// rows use, applied to the one capability an agent would otherwise
/// never learn it has.
#[must_use]
pub fn dev_entry() -> CatalogEntry {
    CatalogEntry {
        name: DEV_ENTRY.to_owned(),
        disclosure: "when the work is to change this city's own code, SPECs or tools, open this \
                     entry before you touch anything"
            .to_owned(),
        expansion: "This city is built from small components, each with its SPEC beside it at \
                    `crates/<crate>/<crate>-SPEC.md`. Read that SPEC, then the code, then the \
                    tests next to the code - in that order, and before you change any of them. \
                    Where the implementation would differ from the SPEC, the SPEC changes first \
                    and says why.\n\nThe User chooses what a change must prove before it is \
                    merged:\n- tested: the asset's own tests ran and passed.\n- contract_kept: \
                    the asset's observable contract did not move.\n- double_validated: the change \
                    holds on held-in and on held-out evidence.\nAnd where it lands: \
                    ordinary work takes the building's own road, while an experiment works in a \
                    tree of its own and nothing in it is merged.\n\nYour next step: say which \
                    evidence this work needs and why, and wait for the User to dispatch it \
                    with that requirement. Choosing a requirement does not provide the evidence."
            .to_owned(),
        // Text this build holds, not a document on a shelf: there is
        // nothing behind it that could change while nobody is looking.
        hash: None,
        package: None,
    }
}

/// The catalog row for this mode: disclosure one-liner plus the
/// expansion text. The chat row is the whole of what chat mode does:
/// one line telling the resident that the person is talking with it.
#[must_use]
pub fn catalog_entry(mode: Mode) -> CatalogEntry {
    let (disclosure, expansion) = match mode {
        Mode::Chat => (
            "chat mode: focus on conversing with the User; answer what they said, in their language",
            "Reply in the conversation. Start work, plans or dispatches only when the User asks for them.",
        ),
        Mode::Work => (
            "work mode: carry out the task towards the stated goal; report when the goal is met",
            "When the task asks for a plan first, write it into Roadmap.md with the `plan` tool \
             before you change anything, then work through it. Report what you did and the \
             evidence that the goal is met.",
        ),
    };
    CatalogEntry {
        name: format!("mode:{}", mode.as_str()),
        disclosure: disclosure.to_owned(),
        expansion: expansion.to_owned(),
        hash: None,
        package: None,
    }
}

/// The resident tools a run in `mode` may run (`crates/runtime/Spec.lean`
/// §8-60). The request carries the union of every mode's core
/// ([`in_some_core`]), and [`mode_admits`] refuses at the call a tool the
/// mode in force does not run; every other admitted tool waits in the
/// dormant index. A name here travels only where the building admitted
/// it: the hall registers no `exec`, so `exec` is in no tier there.
///
/// The two doors of the truncation lock are in every mode, because a
/// dormant capability is reachable only through them. Chat answers the
/// person and reads what it is asked about; work also edits and runs
/// commands, which is what carrying out a task is made of. `status` is in
/// both because the city prompt sends every resident to it for its
/// situation.
#[must_use]
pub fn core_tools(mode: Mode) -> &'static [&'static str] {
    match mode {
        Mode::Chat => &[
            crate::tools::CallTool::NAME,
            crate::tools::DescribeTool::NAME,
            crate::tools::ReadTool::NAME,
            crate::tools::SearchTool::NAME,
            crate::tools::StatusTool::NAME,
        ],
        Mode::Work => &[
            crate::tools::CallTool::NAME,
            crate::tools::DescribeTool::NAME,
            crate::tools::EditTool::NAME,
            kernel::ToolName::EXEC,
            crate::tools::ReadTool::NAME,
            crate::tools::SearchTool::NAME,
            crate::tools::StatusTool::NAME,
        ],
    }
}

/// Whether `name` travels as a tool in every request of a session: it is
/// in the core of some mode. The tool array is this union in every mode
/// (runtime D25), so a change of mode mid-session leaves its bytes
/// alone; [`mode_admits`] is where the mode in force still decides.
#[must_use]
pub fn in_some_core(name: &str) -> bool {
    Mode::ALL
        .iter()
        .any(|mode| core_tools(*mode).contains(&name))
}

/// The gate a resident tool asks at its call: a tool that some mode
/// carries and the mode in force does not is refused, before it does
/// anything (runtime D25). A tool outside every core is the catalog's
/// to reach through `call`, and this gate does not judge it.
///
/// # Errors
/// `E_GATE_DENIED`, naming the tool and the mode, when the mode in
/// force does not run it.
pub fn mode_admits(policy: &RunPolicy, name: &str) -> Result<(), AxError> {
    if !in_some_core(name) || core_tools(policy.mode).contains(&name) {
        return Ok(());
    }
    Err(AxError::failure(
        AxCode::GateDenied,
        format!("run {name}"),
        format!("{name} does not run in {} mode", policy.mode.as_str()),
    )
    .with_recovery("answer in the conversation, or ask the User to switch this run to work mode"))
}

/// The run policy in force for one run (`crates/runtime/spec/PolicyTake.lean`
/// §8-62, runtime D28).
///
/// One writer and many readers. A change that reaches the run at any
/// safe point waits in the cell, and the run's driver loop puts the
/// last one in force at `SafePoint::BeforeWave` and nowhere else; the
/// write gates hold a [`PolicyReader`] that they ask once per write. So
/// every call of one wave is judged under one policy, the one in force
/// when the wave began.
#[derive(Debug)]
pub struct PolicyCell {
    shared: Arc<RwLock<RunPolicy>>,
    arrived: Option<RunPolicy>,
}

impl PolicyCell {
    /// The cell a run opens with, holding `run_started.policy`.
    #[must_use]
    pub fn new(start: RunPolicy) -> PolicyCell {
        PolicyCell {
            shared: Arc::new(RwLock::new(start)),
            arrived: None,
        }
    }

    /// A read-only view of the policy in force, for a write gate.
    #[must_use]
    pub fn reader(&self) -> PolicyReader {
        PolicyReader {
            shared: Arc::clone(&self.shared),
        }
    }

    /// A `run_policy_changed` reached the run at a safe point. It waits
    /// for the next `BeforeWave`; a later arrival overrides it.
    pub(crate) fn arrive(&mut self, policy: RunPolicy) {
        self.arrived = Some(policy);
    }

    /// At `SafePoint::BeforeWave`: puts the last arrival in force and
    /// hands it back so the driver can append the note that tells the
    /// model; `None` when nothing arrived, and then nothing is appended.
    pub(crate) fn take_at_wave(&mut self) -> Option<RunPolicy> {
        let taken = self.arrived.take()?;
        // A poisoned lock still holds a whole policy: the only write is
        // the assignment of a `Copy` value, which cannot stop halfway.
        *self.shared.write().unwrap_or_else(PoisonError::into_inner) = taken;
        Some(taken)
    }
}

/// What a write gate reads: the policy in force, never a change that
/// has arrived and not yet been taken.
#[derive(Debug, Clone)]
pub struct PolicyReader {
    shared: Arc<RwLock<RunPolicy>>,
}

impl PolicyReader {
    /// The policy in force now; asked once per write.
    #[must_use]
    pub fn now(&self) -> RunPolicy {
        *self.shared.read().unwrap_or_else(PoisonError::into_inner)
    }
}

/// The note appended after a wave's results when a change was taken. It
/// is the only place a run's write limit is spelled to the model, so the
/// frozen prefix and the tool descriptions stay byte for byte as they
/// were sent (§8-62).
#[must_use]
pub(crate) fn policy_note(policy: &RunPolicy) -> String {
    format!(
        "The User changed this run's policy; from the next tool call on: mode {}, write {}, \
         admission {}, landing {}.",
        policy.mode.as_str(),
        policy.write.as_str(),
        policy.admit.as_str(),
        policy.landing.as_str()
    )
}

/// What a run has to show for itself.
///
/// `None` is not `Some(false)`: a suite that was never run and a suite
/// that failed are different facts, and a requirement that treated them
/// alike would let "we did not check" pass as "we checked and it was
/// fine".
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Produced {
    /// The asset's own tests ran and passed.
    pub tests_passed: Option<bool>,
    /// The observable contract moved under the renovation.
    pub contract_moved: bool,
    /// Held-in evidence: it did not get worse on what it was built from.
    pub held_in: Option<bool>,
    /// Held-out evidence: it stands up away from what it was built from.
    pub held_out: Option<bool>,
}

/// Whether this may land, and if not, what to do about it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Admission {
    Lands,
    Refused {
        because: &'static str,
        alternative: &'static str,
    },
}

/// Whether what a run produced may be merged, under the policy it was
/// dispatched with (`crates/runtime/spec/Mode.lean` §8-54).
///
/// The landing policy is asked first: an experiment lands nothing
/// however well it went. Then the evidence requirement, exhaustively,
/// so a new requirement has to say what it demands. The mode takes no
/// part: talk and work produce things that meet the same merge.
#[must_use]
pub fn admits(policy: &RunPolicy, produced: &Produced) -> Admission {
    match policy.landing {
        LandingPolicy::Experiment => Admission::Refused {
            because: "nothing produced in an experiment lands",
            alternative: "write what you learned into Memo.md, then ask for the work again \
                          with the ordinary landing",
        },
        LandingPolicy::Ordinary => admits_evidence(policy.admit, produced),
    }
}

/// The evidence half of [`admits`].
///
/// `Standing` adds nothing: the building's own checks were made where
/// they are made. `Tested` wants the asset proven by its own tests.
/// `ContractKept` wants the contract to have stayed where it was,
/// because a renovation that moves it is not a renovation.
/// `DoubleValidated` wants both halves of the double validation: it is
/// asked for a change to how the city behaves, so confidence is not the
/// currency — evidence is.
fn admits_evidence(required: AdmissionRequirement, produced: &Produced) -> Admission {
    match required {
        AdmissionRequirement::Standing => Admission::Lands,
        AdmissionRequirement::Tested => match produced.tests_passed {
            Some(true) => Admission::Lands,
            Some(false) => Admission::Refused {
                because: "the asset's own tests did not pass",
                alternative: "fix the asset or the test, then offer it again",
            },
            None => Admission::Refused {
                because: "the asset has no tests of its own",
                alternative: "write the test that would fail if this asset broke",
            },
        },
        AdmissionRequirement::ContractKept => {
            if produced.contract_moved {
                Admission::Refused {
                    because: "the asset's observable contract moved",
                    alternative: "keep the contract and renovate behind it, or ask for the work \
                                  again under double_validated with held-out evidence",
                }
            } else {
                Admission::Lands
            }
        }
        AdmissionRequirement::DoubleValidated => match (produced.held_in, produced.held_out) {
            (Some(true), Some(true)) => Admission::Lands,
            (Some(false), _) => Admission::Refused {
                because: "it got worse on the held-in set",
                alternative: "a change that costs more than it buys is not adopted; revise it",
            },
            (_, Some(false)) => Admission::Refused {
                because: "it did not stand up on the held-out set",
                alternative: "a gain that only shows where it was built is a gain in fitting, \
                              not in ability",
            },
            _ => Admission::Refused {
                because: "one half of the double validation is missing",
                alternative: "run the suite on both sets; an unmeasured change is not adopted on \
                              confidence",
            },
        },
    }
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    reason = "test code"
)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    /// One thing a run meets, as `crates/runtime/spec/PolicyTake.lean`
    /// names it: a change reaching the run, a `BeforeWave`, or one call of
    /// the wave asking a write gate.
    #[derive(Debug, Clone, Copy)]
    enum Step {
        Change(RunPolicy),
        Wave,
        Call,
    }

    fn any_policy() -> impl Strategy<Value = RunPolicy> {
        (
            prop::sample::select(Mode::ALL.to_vec()),
            prop::sample::select(kernel::WriteLimit::ALL.to_vec()),
            prop::sample::select(AdmissionRequirement::ALL.to_vec()),
            prop::sample::select(LandingPolicy::ALL.to_vec()),
        )
            .prop_map(|(mode, write, admit, landing)| RunPolicy {
                mode,
                write,
                admit,
                landing,
            })
    }

    fn any_step() -> impl Strategy<Value = Step> {
        prop_oneof![
            any_policy().prop_map(Step::Change),
            Just(Step::Wave),
            Just(Step::Call),
        ]
    }

    /// The model's two properties over every trace: a wave's calls all
    /// read the policy in force when the wave began, and a `BeforeWave`
    /// takes the last change that arrived before it, and only then.
    fn walk(start: RunPolicy, trace: &[Step]) -> Result<(), TestCaseError> {
        let mut cell = PolicyCell::new(start);
        let gate = cell.reader();
        let mut in_force = start;
        let mut last_arrival = None;
        for step in trace {
            match *step {
                Step::Change(policy) => {
                    cell.arrive(policy);
                    last_arrival = Some(policy);
                }
                Step::Wave => {
                    let taken = cell.take_at_wave();
                    prop_assert_eq!(taken, last_arrival.take());
                    in_force = taken.unwrap_or(in_force);
                }
                Step::Call => prop_assert_eq!(gate.now(), in_force),
            }
        }
        Ok(())
    }

    proptest! {
        #[test]
        fn policy_take_holds_on_every_trace(
            start in any_policy(),
            trace in prop::collection::vec(any_step(), 0..24),
        ) {
            walk(start, &trace)?;
        }
    }

    /// The model's counterexample to reading the mailslot
    /// (`eager_reading_splits_a_wave`), kept as the trace that bit.
    #[test]
    fn a_change_inside_a_wave_leaves_that_wave_alone() {
        let start = RunPolicy::of(Mode::Work);
        let tighter = RunPolicy {
            write: kernel::WriteLimit::Create,
            ..start
        };
        walk(start, &[Step::Call, Step::Change(tighter), Step::Call]).unwrap();
    }

    /// The tool list is the same in every mode, so the mode in force is
    /// asked at the call: a chat run creates nothing (runtime D25).
    #[test]
    fn a_chat_run_is_refused_an_edit_and_the_disk_is_untouched() {
        let tmp = tempfile::tempdir().unwrap();
        let work = kernel::Address::parse("work").unwrap();
        std::fs::create_dir_all(tmp.path().join("work")).unwrap();
        let tool = crate::tools::EditTool::new(
            tmp.path(),
            work.clone(),
            kernel::WriteDomain::new(vec![work]).unwrap(),
            crate::PolicyCell::new(kernel::RunPolicy::of(kernel::Mode::Chat)).reader(),
        )
        .unwrap();
        let refused = kernel::Tool::invoke(&tool, &edit_call());
        assert_eq!(
            refused.err().map(|err| *err.code()),
            Some(AxCode::GateDenied)
        );
        assert!(!tmp.path().join("work/new.md").exists());
    }

    fn edit_call() -> kernel::ToolCall {
        let args = serde_json::json!({
            "path": "work/new.md", "base_version": "new", "old": "", "new": "text",
        });
        kernel::ToolCall {
            id: "call-1".to_owned(),
            name: kernel::ToolName::parse("edit").unwrap(),
            args: kernel::Payload::of(&args).unwrap(),
        }
    }

    fn work(admit: AdmissionRequirement) -> RunPolicy {
        RunPolicy {
            admit,
            ..RunPolicy::of(Mode::Work)
        }
    }

    /// A requirement chosen is not evidence held: work that asked for
    /// its own tests and ran none does not land.
    #[test]
    fn a_work_run_without_the_evidence_it_chose_does_not_land() {
        let Admission::Refused { because, .. } =
            admits(&work(AdmissionRequirement::Tested), &Produced::default())
        else {
            panic!("work that chose `tested` and ran no test landed");
        };
        assert!(because.contains("no tests"), "{because}");
    }

    /// A chat run is told one thing, to talk with the person.
    #[test]
    fn chat_is_one_line_about_the_conversation() {
        let entry = catalog_entry(Mode::Chat);
        assert_eq!(entry.name, "mode:chat");
        assert!(entry.disclosure.contains("convers"), "{}", entry.disclosure);
        assert!(!entry.disclosure.contains('\n'), "one line");
        assert!(
            !entry.expansion.is_empty(),
            "a read of the entry says something"
        );
        assert_eq!(catalog_entry(Mode::Work).name, "mode:work");
    }

    /// The building's own checks are made where they are made; the
    /// standing requirement adds none, in either mode.
    #[test]
    fn standing_adds_nothing_in_either_mode() {
        for mode in Mode::ALL {
            assert_eq!(
                admits(&RunPolicy::of(mode), &Produced::default()),
                Admission::Lands
            );
        }
    }

    #[test]
    fn tested_wants_the_asset_proven_and_says_which_way_it_failed() {
        let policy = work(AdmissionRequirement::Tested);
        let failed = Produced {
            tests_passed: Some(false),
            ..Produced::default()
        };
        let Admission::Refused { because, .. } = admits(&policy, &failed) else {
            panic!("a failing asset does not land");
        };
        assert!(
            because.contains("did not pass"),
            "not the untested sentence"
        );
        let passed = Produced {
            tests_passed: Some(true),
            ..Produced::default()
        };
        assert_eq!(admits(&policy, &passed), Admission::Lands);
    }

    #[test]
    fn contract_kept_refuses_a_renovation_that_moved_the_contract() {
        let policy = work(AdmissionRequirement::ContractKept);
        assert_eq!(admits(&policy, &Produced::default()), Admission::Lands);
        let moved = Produced {
            contract_moved: true,
            ..Produced::default()
        };
        let Admission::Refused { alternative, .. } = admits(&policy, &moved) else {
            panic!("a moved contract is not a renovation");
        };
        assert!(
            alternative.contains("double_validated"),
            "the refusal names the requirement that would take it"
        );
    }

    #[test]
    fn double_validated_takes_nothing_on_confidence() {
        let policy = work(AdmissionRequirement::DoubleValidated);
        let both = Produced {
            held_in: Some(true),
            held_out: Some(true),
            ..Produced::default()
        };
        assert_eq!(admits(&policy, &both), Admission::Lands);
        for missing in [
            Produced {
                held_in: Some(true),
                ..Produced::default()
            },
            Produced {
                held_out: Some(true),
                ..Produced::default()
            },
            Produced::default(),
        ] {
            assert!(
                matches!(admits(&policy, &missing), Admission::Refused { .. }),
                "half of a double validation is not a double validation"
            );
        }
        let regressed = Produced {
            held_in: Some(true),
            held_out: Some(false),
            ..Produced::default()
        };
        let Admission::Refused { because, .. } = admits(&policy, &regressed) else {
            panic!("a change that only holds where it was built does not land");
        };
        assert!(because.contains("held-out"));
    }

    /// An experiment lands nothing, whatever it proved and whatever it
    /// was allowed to write.
    #[test]
    fn an_experiment_lands_nothing_however_well_it_went() {
        let excellent = Produced {
            tests_passed: Some(true),
            contract_moved: false,
            held_in: Some(true),
            held_out: Some(true),
        };
        for admit in AdmissionRequirement::ALL {
            let policy = RunPolicy {
                landing: LandingPolicy::Experiment,
                write: kernel::WriteLimit::Create,
                ..work(admit)
            };
            assert!(matches!(
                admits(&policy, &excellent),
                Admission::Refused { .. }
            ));
        }
    }
}