onepipeline 0.41.1

Execute a task DAG over oneagentgraph and onevcs, merging their event streams into one.
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
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
//! The post-launch verbs, as typed calls: what the binary does after a run is
//! launched, reachable without the binary.
//!
//! **The CLI is argument parsing over these calls.** One function per verb,
//! each answering the facts behind what `onepipeline <verb>` prints, and beside
//! each a renderer producing exactly the text the binary prints — so an
//! embedding program reads the facts and the binary reads the rendering, off
//! one call. A post-launch behaviour the binary has and one of these lacks is a
//! defect, and `tests/parity.rs` is the gate that holds the two to one another:
//! it drives the compiled binary and the call here over one run and holds
//! stdout and exit code byte for byte.
//!
//! Exit codes do not move. A typed result carries the outcome — a
//! [`Receipt`]'s state, [`Stopped::clean`], a [`WatchOutcome`] — and the binary
//! maps it to the code `docs/contract.md` assigns, through the `exit_code`
//! each result answers. What the binary reads from its environment — the
//! launching session, the runs root, the profile a reader named — is passed in
//! here rather than read inside, so a consumer that is not this binary is not
//! answered out of the binary's environment.
//!
//! The launch verbs — `start`, `plan check` and the hidden retained verbs — are
//! not here: a launch is what makes a run, and these are what a run is reached
//! with afterwards. The one exception is [`drive_run`], the body of the hidden
//! `drive-run` verb, published so an embedding binary can carry a hidden driver
//! verb of its own and be its own detached driver.

// llmlint: ignore-file[invalid_states_unrepresentable] a run id, a launching session and a
// project id are `String`s on every result here for the reason `src/ledger.rs` states of
// the records they are read off: each is a *serialized* field an older build wrote and a
// consumer parses, and `docs/contract.md` names no `RunId`, `Session` or `ProjectId` — the
// signatures it fixes for this module spell `&RunPaths`, `session: &str` and `run: String`,
// which the downstream nodes are written against, so a newtype here would be a public
// vocabulary the contract did not ask for. What is enforced is the boundary that matters:
// `resolved` is where an externally supplied run id is checked before it is joined onto
// anything, and `ledger::owned_by` is the one place ownership is decided. The shapes the
// contract fixes field by field — `Next`, `ChannelQueue`, `StopRequest`, `Stopped` — are
// answered at each, beside the field.

use std::path::{Path, PathBuf};

use serde_json::json;

use crate::agentgraph;
use crate::channel::{
    Author, ChannelState, Command, CommandOutcome, QueuedCommands, QueuedReply, Reply, Surface,
    SurfaceKind,
};
use crate::driver::{self, Submitted};
use crate::error::{Error, Result, EXIT_REFUSED, EXIT_SUCCESS};
use crate::event::Envelope;
use crate::filter::EventFilter;
use crate::journal::{self, Journal};
use crate::ledger::{self, RunPaths};
use crate::telemetry::RunTelemetry;
use crate::views::{self, Listing, Projects, RunView, Survey};

pub use crate::agents::{AgentRun, AgentScope, AgentSession, Agents};
pub use crate::driver::{Retained, Settlement};
pub use crate::journal::StopTeardown;
pub use crate::unwatched::{Unwatched, UnwatchedRun};
pub use crate::watch::{
    Ending as WatchEnding, Frame as WatchFrame, Lines as WatchLines, Monitored,
    Outcome as WatchOutcome, Request as WatchRequest,
};

/// The paths for a run that exists under `root`, or a refusal naming the root
/// searched.
///
/// A run id that navigates is refused before it is joined onto anything: it is
/// not a run this root holds, and reporting it as merely missing would leave a
/// caller believing the path they typed was looked for where they meant.
pub(crate) fn resolved(root: &Path, run: &str) -> Result<RunPaths> {
    if !ledger::is_valid_run_id(run) {
        return Err(Error::Invalid(format!(
            "'{run}' is not a run id: a run id names one directory under the runs root, \
             so it may not be a path"
        )));
    }
    let paths = RunPaths::under(root, run);
    if !paths.exists() {
        return Err(Error::NoSuchRun {
            run: run.to_string(),
            root: root.to_path_buf(),
        });
    }
    Ok(paths)
}

/// How a listing lays its runs out.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Grouping {
    /// By project, newest activity first, under a header line each. The
    /// default.
    Grouped,
    /// One row per run, by run id: `runs --flat`.
    Flat,
}

/// `onepipeline runs [--mine]`: every run under `root`, grouped by project.
///
/// `mine` narrows each group to the runs `session` owns; a group none of whose
/// runs it owns stays, empty, so a consumer can still tell a root holding other
/// sessions' work from one holding nothing — the listing says `no runs recorded`
/// for the first and names what it refused for the second. A bounded read: it
/// opens no run's merged event store for a run whose summary document is
/// current. [`Projects::flat`] is the ungrouped list.
pub fn runs(root: &Path, session: &str, mine: bool) -> Projects {
    let mut projects = Projects::of(&Listing::of(root));
    if mine {
        for group in &mut projects.groups {
            group
                .runs
                .retain(|summary| ledger::owned_by(&summary.session, session));
        }
    }
    projects
}

/// The text `onepipeline runs` prints, laid out either way, for a reader in
/// `session` — which is what decides the ownership marker on each row.
pub fn render_runs(projects: &Projects, grouping: Grouping, session: &str) -> String {
    views::runs_of(projects, grouping, session)
}

/// `onepipeline status [RUN]`: one run's detail, or every run's standing.
///
/// The two are two different reads, deliberately so. A named run is a
/// **detail** read that folds that run's merged store and reports what each of
/// its nodes is doing; no run named is a **listing**, which never folds — it
/// answers the run-level lines out of each run's bounded summary document.
#[allow(
    clippy::large_enum_variant,
    reason = "built once per call and matched on by a consumer, so the size of a folded \
              run costs nothing a box would save; boxing the variant would change the shape \
              of a public enum the contract fixes as `Status::Run(RunStatus)`"
)]
#[derive(Debug)]
pub enum Status {
    /// The run named, folded.
    Run(RunStatus),
    /// Every run under the root, grouped by project.
    Listing(Projects),
}

/// One run's folded detail.
#[derive(Debug)]
pub struct RunStatus {
    /// The run, read once.
    pub view: RunView,
}

/// `onepipeline status [RUN]`.
///
/// # Errors
///
/// A run named that is not under `root`, or one whose store cannot be read.
pub fn status(root: &Path, run: Option<&str>) -> Result<Status> {
    Ok(match run {
        Some(run) => Status::Run(RunStatus {
            view: RunView::open(&resolved(root, run)?)?,
        }),
        None => Status::Listing(Projects::of(&Listing::of(root))),
    })
}

/// The text `onepipeline status [RUN]` prints.
pub fn render_status(status: &Status) -> String {
    match status {
        Status::Run(detail) => views::status_of(&detail.view),
        Status::Listing(projects) => views::status_listed(projects),
    }
}

/// `onepipeline host`: every live dispatch on this host, read off every run
/// under the root.
#[derive(Debug)]
pub struct Host {
    /// Every run the root holds, folded, and the roots that refused.
    pub survey: Survey,
}

/// `onepipeline host`.
pub fn host(root: &Path) -> Host {
    Host {
        survey: Survey::of(root),
    }
}

/// The text `onepipeline host` prints.
pub fn render_host(host: &Host) -> String {
    views::host(&host.survey)
}

/// `onepipeline goals [RUN]`: what each run is for, and how far it has got.
///
/// The goal lines come off each run's projected plan, so the runs are folded;
/// given no run, the grouping says which run goes under which project's header
/// and in what order, and is read off the bounded listing beside the fold.
#[derive(Debug)]
pub struct Goals {
    /// The runs read, folded, and the roots that refused.
    pub survey: Survey,
    /// How a listing groups them — `None` when one run was named.
    pub projects: Option<Projects>,
}

/// `onepipeline goals [RUN]`.
///
/// # Errors
///
/// A run named that is not under `root`, or one whose store cannot be read.
pub fn goals(root: &Path, run: Option<&str>) -> Result<Goals> {
    Ok(match run {
        Some(run) => Goals {
            survey: Survey::of_one(RunView::open(&resolved(root, run)?)?),
            projects: None,
        },
        None => Goals {
            survey: Survey::of(root),
            projects: Some(Projects::of(&Listing::of(root))),
        },
    })
}

/// The text `onepipeline goals [RUN]` prints.
pub fn render_goals(goals: &Goals) -> String {
    match &goals.projects {
        Some(projects) => views::goals_grouped(&goals.survey, projects),
        None => views::goals(&goals.survey),
    }
}

/// `onepipeline results RUN`: per-node outcomes, with each node's own evidence.
#[derive(Debug)]
pub struct Results {
    /// The run, read once.
    pub view: RunView,
}

/// `onepipeline results RUN`.
///
/// # Errors
///
/// A run whose store cannot be read.
pub fn results(paths: &RunPaths) -> Result<Results> {
    Ok(Results {
        view: RunView::open(paths)?,
    })
}

/// The text `onepipeline results RUN` prints.
pub fn render_results(results: &Results) -> String {
    views::results(&results.view)
}

/// `onepipeline transcript RUN [NODE]`: a dispatched turn's tools and reasoning,
/// from the evidence it retained.
#[derive(Debug)]
pub struct Transcript {
    /// The run, read once.
    pub view: RunView,
    /// The one node asked for, or every node with a record.
    pub node: Option<String>,
}

/// `onepipeline transcript RUN [NODE]`.
///
/// # Errors
///
/// A node this run never dispatched is refused rather than answered with an
/// empty transcript: the two read alike, and only one of them means the reader
/// typed a name that is not in this run.
pub fn transcript(paths: &RunPaths, node: Option<&str>) -> Result<Transcript> {
    let view = RunView::open(paths)?;
    if let Some(node) = node {
        if views::nodes_with_agent_records(&view, Some(node)).is_empty() {
            let recorded = views::nodes_with_agent_records(&view, None);
            return Err(Error::Refused(format!(
                "run '{}' has recorded nothing for node '{node}'; it has records for: {}",
                paths.run,
                if recorded.is_empty() {
                    "nothing yet".to_string()
                } else {
                    recorded.join(", ")
                }
            )));
        }
    }
    Ok(Transcript {
        view,
        node: node.map(str::to_owned),
    })
}

/// The text `onepipeline transcript RUN [NODE]` prints.
pub fn render_transcript(transcript: &Transcript) -> String {
    views::transcript(&transcript.view, transcript.node.as_deref())
}

/// `onepipeline agents RUN [NODE]`: every oneharness session a run's launches
/// wrote — every one, or the ones a node's dispatches wrote — read off the run's
/// own pointer file and nothing else.
///
/// One entry per session, grouped by `history_session`, carrying the three
/// fields a reader opens it to its transcript through: `history_dir`,
/// `history_project`, `history_session`. See [`crate::agents`] for what is
/// stamped and why the store is never opened here.
///
/// # Errors
///
/// A pointer file that exists and cannot be read. A run with no pointer file —
/// one an earlier build launched, or one that has dispatched nothing yet — is
/// an empty list rather than an error.
pub fn agents(paths: &RunPaths, scope: AgentScope<'_>) -> Result<Agents> {
    Agents::of_run(paths, scope)
}

/// `onepipeline agents --project PROJECT`: the union of [`agents`] over every
/// run under `root` whose summary names `project`, in the listing's order.
///
/// # Errors
///
/// A project no run under `root` was launched from, naming the ones that were;
/// and a pointer file that exists and cannot be read.
pub fn project_agents(root: &Path, project: &str) -> Result<Agents> {
    let listing = Listing::of(root);
    let mut agents = Agents::default();
    let mut found = false;
    for summary in listing
        .summaries
        .iter()
        .filter(|summary| summary.project == project)
    {
        found = true;
        agents.absorb(
            &RunPaths::under(root, &summary.run_id).oneharness_sessions(),
            AgentScope::Run,
        )?;
    }
    if !found {
        let mut launched: Vec<&str> = listing
            .summaries
            .iter()
            .map(|summary| summary.project.as_str())
            .filter(|project| !project.is_empty())
            .collect();
        launched.sort_unstable();
        launched.dedup();
        return Err(Error::Refused(format!(
            "no run under {} was launched from project '{project}'; runs were launched from: {}",
            root.display(),
            if launched.is_empty() {
                "no project at all".to_string()
            } else {
                launched.join(", ")
            }
        )));
    }
    Ok(agents)
}

/// The text `onepipeline agents` prints.
pub fn render_agents(agents: &Agents) -> String {
    crate::agents::render(agents)
}

/// `onepipeline telemetry [RUN]`: each run's timing and usage, one document per
/// run — the run named, or every run under the root, oldest id first.
///
/// Through [`telemetry::of_run`](crate::telemetry::of_run), the fold over a
/// view the caller holds; this is that fold over the views this reads.
///
/// # Errors
///
/// A run named that is not under `root`, or one whose store cannot be read.
pub fn telemetry(root: &Path, run: Option<&str>) -> Result<Vec<RunTelemetry>> {
    let survey = match run {
        Some(run) => Survey::of_one(RunView::open(&resolved(root, run)?)?),
        None => Survey::of(root),
    };
    Ok(survey
        .views
        .iter()
        .map(|view| crate::telemetry::of_run(&view.paths, &view.events))
        .collect())
}

/// The text `onepipeline telemetry [RUN] [--breakdown]` prints: one JSON line
/// per run, or each run's breakdown.
///
/// # Errors
///
/// A document that cannot be serialised, which none this crate folds is.
pub fn render_telemetry(measured: &[RunTelemetry], breakdown: bool) -> Result<String> {
    let mut out = String::new();
    for telemetry in measured {
        if breakdown {
            out.push_str(&render_telemetry_breakdown(telemetry));
        } else {
            out.push_str(
                &serde_json::to_string(telemetry)
                    .map_err(|e| Error::Invalid(format!("telemetry: {e}")))?,
            );
            out.push('\n');
        }
    }
    Ok(out)
}

/// One run's breakdown, as `telemetry --breakdown` prints it.
pub fn render_telemetry_breakdown(telemetry: &RunTelemetry) -> String {
    crate::telemetry::render_breakdown(telemetry)
}

/// `onepipeline monitor RUN [--cursor C]`: one pass over the merged stream, from
/// the cursor or from the start, shown through `filter`.
///
/// # Errors
///
/// A run whose store cannot be read, or a cursor this run cannot place.
pub fn monitor(paths: &RunPaths, filter: &EventFilter, cursor: Option<&str>) -> Result<Monitored> {
    let view = RunView::open(paths)?;
    crate::watch::monitored(paths, view, filter, cursor)
}

/// The text `onepipeline monitor` prints: the event lines, the run's trailer,
/// and the resume line last.
pub fn render_monitor(monitored: &Monitored) -> String {
    monitored.render()
}

/// `onepipeline watch RUN`: block until the run needs a supervisor, or until
/// the wait runs out, handing every frame to `sink` as it happens.
///
/// It registers the calling process as the run's watcher for the length of the
/// wait and removes the record on a clean return, exactly as the verb does; a
/// sink that refuses a frame ends the wait with that refusal.
///
/// # Errors
///
/// A cursor this run cannot place, a condition it cannot return on, a run that
/// cannot be read, or the sink's own refusal.
pub fn watch(
    paths: &RunPaths,
    request: &WatchRequest,
    sink: &mut dyn FnMut(WatchFrame<'_>) -> Result<()>,
) -> Result<WatchOutcome> {
    crate::watch::watch(paths, request, sink)
}

/// The two lines the binary prints for one frame: the human one on standard
/// error, the machine one on standard output.
///
/// # Errors
///
/// A record that cannot be rendered, which none this crate builds is.
pub fn render_watch_frame(frame: &WatchFrame<'_>) -> Result<WatchLines> {
    WatchLines::of(frame)
}

/// `onepipeline unwatched --session ID`: which of `session`'s runs under `root`
/// has nothing watching it.
///
/// # Errors
///
/// A runs root that exists and cannot be read.
pub fn unwatched(root: &Path, session: &str) -> Result<Unwatched> {
    crate::unwatched::unwatched(root, session)
}

/// The text `onepipeline unwatched` prints on standard output: one line per
/// reported run, and nothing when there is nothing to report. What could not be
/// resolved is [`Unwatched::unresolved`], which the binary prints on standard
/// error.
pub fn render_unwatched(unwatched: &Unwatched) -> String {
    unwatched
        .reported
        .iter()
        .map(UnwatchedRun::line)
        .collect::<Vec<_>>()
        .concat()
}

/// What `onepipeline next` answered.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NextStatus {
    /// Nothing was waiting, and the run has settled.
    Finished,
    /// Nothing was waiting, and the run is still being driven.
    Running,
    /// A surface was claimed, and it is the one on the answer.
    Surface,
}

/// One read of the channel: the surface claimed, if one was, and the run's
/// events shown through the reader's profile.
// llmlint: ignore[invalid_states_unrepresentable] the three fields are the three keys the line has always carried — `{"status", "surface", "events"}` — and the shape the contract fixes as `Next { status, surface, events }`, which the downstream nodes read by name; the one place a `Next` is built is `next` below, where the status and the surface are decided by one `match` on the claim, so no caller assembles a `Surface` status beside no surface.
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Next {
    /// Whether a surface was claimed, and if not, whether the run is over.
    pub status: NextStatus,
    /// The surface claimed, consumed by this read.
    pub surface: Option<Surface>,
    /// The run's merged store, shaped through the profile.
    pub events: Vec<Envelope>,
}

/// `onepipeline next RUN` — the channel's only consumer.
///
/// Rendering is not reading: `monitor` shows a pending surface without
/// consuming it, and this is what advances the queue, journals
/// `planner-surfaced`, and restarts the check-in clock of every member the run's
/// observer graph declares resettable.
///
/// # Errors
///
/// A run whose store cannot be read, or a channel that refuses the claim.
pub fn next(paths: &RunPaths, filter: &EventFilter) -> Result<Next> {
    let view = RunView::open(paths)?;
    let events: Vec<Envelope> = views::shaped(&view, filter).into_iter().cloned().collect();
    let channel = ChannelState::new(paths);

    let Some(surface) = channel.claim()? else {
        let settled = view.liveness().is_undriven();
        return Ok(Next {
            status: if settled {
                NextStatus::Finished
            } else {
                NextStatus::Running
            },
            surface: None,
            events,
        });
    };

    let mut journal = Journal::open(paths);
    journal.emit(
        journal::PipelineKind::PlannerSurfaced,
        journal::labels(&paths.run, surface.workstream.as_deref()),
        journal::payload(&[
            ("kind", json!(surface.kind)),
            ("message", json!(surface.message)),
            ("source", json!(surface.source)),
            ("blocking", json!(surface.blocking)),
            // When the text was true, beside the text: a surface is written in
            // the present tense and this record's own stamp is the reading.
            // Divergence 66 is why it is the queued instant and not an age.
            ("queued_at", json!(surface.queued_at)),
        ]),
    )?;

    // Consumption is what restarts the check-in clock — the whole reset
    // contract. Which clocks is the observer graph's own to say: every member it
    // declared `resettable`, and the engine names none. Addressed by the
    // **graph** run's id, which is what the sibling minted and the only id its
    // signals answer to; this run's id names a run `oneagentgraph` has never
    // heard of. A run that launched no observer graph has no clock to restart
    // and nothing to report. A failure to reach the sibling is reported and does
    // not fail the read: the planner has the surface either way.
    if view.launch.observer_graph().is_some() {
        if let Err(error) = agentgraph::recorded_graph_run(&view.launch.graph_run, &paths.run)
            .and_then(|graph_run| agentgraph::reset_resettable(&graph_run))
        {
            eprintln!("onepipeline: could not restart the check-in clock: {error}");
        }
    }

    // The surface is delivered whatever the profile said. A profile shapes the
    // **event view** and nothing else: which surfaces exist, and the unread
    // accounting over them, belong to the channel, so a blocking surface reaches
    // its planner under the narrowest profile a run has.
    Ok(Next {
        status: NextStatus::Surface,
        surface: Some(surface),
        events,
    })
}

/// The line `onepipeline next` prints: one JSON object.
pub fn render_next(next: &Next) -> String {
    json!({"status": next.status, "surface": next.surface, "events": next.events}).to_string()
}

/// The run's channel as it stands: every surface it has raised and where each
/// is, the replies the planner has written, and the edit envelopes on the
/// durable command queue with the reconciler's answers to them.
///
/// A reading, never a record: nothing here consumes, claims or answers. What
/// `next` hands out and what the views count as unread are decided from the same
/// queues this reads.
// llmlint: ignore[invalid_states_unrepresentable] the fields are the channel's own queues as the bus's layout keeps them — the surfaces log, its projection's `waiting` and its pending slot, and the three other logs — read as one snapshot by `channel` below, which is the one place a `ChannelQueue` is built; the shape is the one the planner ruled on for the downstream nodes (`ChannelQueue { surfaces, waiting, held, replies, commands, outcomes }`), and a reading of six logs that folded them into one structure would be a projection of this crate's own beside the bus's, which is what the channel paragraph of the contract forbids.
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct ChannelQueue {
    /// Every surface the run has raised, in the order it raised them.
    pub surfaces: Vec<Surface>,
    /// The surfaces nobody has read yet, oldest first.
    pub waiting: Vec<Surface>,
    /// Whatever the pending slot holds: the surface a planner consumed and has
    /// not answered, abandoned or not.
    pub held: Option<Surface>,
    /// Every reply the planner has written, in order.
    pub replies: Vec<QueuedReply>,
    /// The edit envelopes the reconciler has not claimed yet.
    pub commands: Vec<QueuedCommands>,
    /// The reconciler's answer to every envelope it has answered, in order.
    pub outcomes: Vec<CommandOutcome>,
}

impl ChannelQueue {
    /// The surface waiting for an answer, if one is: the held surface, unless
    /// nobody is waiting on it any more.
    pub fn pending(&self) -> Option<&Surface> {
        self.held.as_ref().filter(|surface| !surface.abandoned)
    }

    /// The held surface nobody is waiting on any more, if that is what the
    /// slot holds.
    pub fn abandoned(&self) -> Option<&Surface> {
        self.held.as_ref().filter(|surface| surface.abandoned)
    }

    /// The surfaces that have been read and answered: every one raised that is
    /// neither waiting nor held.
    pub fn answered(&self) -> Vec<&Surface> {
        self.surfaces
            .iter()
            .filter(|surface| {
                !self.waiting.iter().any(|waiting| waiting.id == surface.id)
                    && self.held.as_ref().is_none_or(|held| held.id != surface.id)
            })
            .collect()
    }
}

/// `onepipeline channel queue RUN`: read the run's channel.
///
/// # Errors
///
/// A run that is not there.
pub fn channel(paths: &RunPaths) -> Result<ChannelQueue> {
    if !paths.exists() {
        return Err(Error::NoSuchRun {
            run: paths.run.clone(),
            root: paths.dir.parent().unwrap_or(Path::new(".")).to_path_buf(),
        });
    }
    let channel = ChannelState::new(paths);
    let queue = channel.queue();
    Ok(ChannelQueue {
        surfaces: channel.every_surface(),
        waiting: queue.waiting,
        held: queue.pending,
        replies: channel.replies(),
        commands: channel.claimable_commands(),
        outcomes: channel.outcomes(),
    })
}

/// The line `onepipeline channel queue` prints: one JSON object.
///
/// # Errors
///
/// A record that cannot be serialised, which none the channel holds is.
pub fn render_channel(queue: &ChannelQueue) -> Result<String> {
    serde_json::to_string(queue).map_err(|e| Error::Invalid(format!("channel: {e}")))
}

/// What became of an envelope's verdict half.
///
/// **Two variants and not three**: what happens to a verdict that reaches a
/// receipt is not a variable — it was queued, on every path a submission can
/// take — so the only thing left to say is whether the envelope carried one. The
/// other thing that can happen to a verdict is a refusal, which is an error and
/// never a receipt.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerdictHalf {
    /// The envelope carried none, so the receipt names none.
    NotCarried,
    /// Carried, and on the reply queue for whichever reader claims it.
    OnTheQueue,
}

impl VerdictHalf {
    /// The word the receipt writes, and nothing for the half it never carried.
    ///
    /// `delivered` for the same reason `state` has always spelled it so:
    /// **delivery on this channel is acceptance**, a planner writing when it has
    /// something to say with nothing obliged to be listening at that moment. So
    /// the two keys cannot disagree about one half. Which question it answers is
    /// bound as `ChannelState::answer` states and entry 63 of
    /// `docs/contract-divergences.md` records, and which listener then reads it
    /// is not something a receipt written at submission could answer.
    fn word(self) -> Option<&'static str> {
        match self {
            Self::NotCarried => None,
            Self::OnTheQueue => Some("delivered"),
        }
    }
}

/// What one submitted envelope became: one variant per outcome a submission can
/// reach, so the receipt cannot be built out of parts that contradict each other.
///
/// Which words `state` and `commands` spell, and whether there is a channel
/// identifier to name at all, are decided by the same choice rather than carried
/// as three fields a caller assembles.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplyOutcome {
    /// A commandless verdict, queued for whichever reader the run owes one.
    Answered {
        /// Its id in the channel.
        reply: u64,
        /// Whether the envelope carried a verdict at all: an envelope carrying
        /// neither half is answered here too, and names neither.
        verdict: VerdictHalf,
    },
    /// Every command applied by this process, which took the run's ownership
    /// lock because nothing was driving it — so there is no queue and no
    /// identifier to name.
    AppliedHere {
        /// The verdict half that rode along, if one did.
        verdict: VerdictHalf,
    },
    /// Every command applied by the run's own reconciler, over the durable
    /// queue.
    AppliedByRun {
        /// The envelope's id in the command queue.
        reply: u64,
        /// The verdict half that rode along, if one did.
        verdict: VerdictHalf,
    },
    /// Accepted and durable, and not reconciled within the reply timeout. Still
    /// queued: **not** an instruction to send it again.
    Queued {
        /// The envelope's id in the command queue.
        reply: u64,
        /// The verdict half that rode along, if one did.
        verdict: VerdictHalf,
    },
}

/// `onepipeline reply`'s answer, whose shape entry 64 of
/// `docs/contract-divergences.md` states.
///
/// The receipt is a transport's answer and stays exactly what it was; what
/// rides beside it — what a queued envelope is waiting for, and a landing
/// settled with no release stated for it — is [`advice`](Self::advice), which the
/// binary prints on standard error and a consumer shows however it shows advice.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Receipt {
    /// What the envelope became.
    pub outcome: ReplyOutcome,
    /// What the person who sent it should know beside the receipt, each a
    /// sentence of its own, in the order the binary says them.
    pub advice: Vec<String>,
}

impl Receipt {
    /// The identifier in the channel. The wire spells the local apply's absence
    /// `0`, which is what this receipt has always answered there and is not a
    /// second identifier.
    pub fn reply(&self) -> u64 {
        match self.outcome {
            ReplyOutcome::Answered { reply, .. }
            | ReplyOutcome::AppliedByRun { reply, .. }
            | ReplyOutcome::Queued { reply, .. } => reply,
            ReplyOutcome::AppliedHere { .. } => 0,
        }
    }

    /// The word `state` has spelled since before the two halves were named.
    pub fn state(&self) -> &'static str {
        match self.outcome {
            ReplyOutcome::Answered { .. } => "delivered",
            ReplyOutcome::AppliedHere { .. } | ReplyOutcome::AppliedByRun { .. } => "applied",
            ReplyOutcome::Queued { .. } => "queued",
        }
    }

    /// The same word again where there were commands to have one, under a name
    /// saying whose it is, and nothing where the envelope carried none.
    pub fn commands(&self) -> Option<&'static str> {
        match self.outcome {
            ReplyOutcome::Answered { .. } => None,
            _ => Some(self.state()),
        }
    }

    /// The verdict half, whichever outcome the commands reached.
    pub fn verdict(&self) -> VerdictHalf {
        match self.outcome {
            ReplyOutcome::Answered { verdict, .. }
            | ReplyOutcome::AppliedHere { verdict }
            | ReplyOutcome::AppliedByRun { verdict, .. }
            | ReplyOutcome::Queued { verdict, .. } => verdict,
        }
    }

    /// The status the binary exits with: [`EXIT_SUCCESS`] for every receipt,
    /// a queued envelope included — entry 67 of `docs/contract-divergences.md`
    /// records why the two outcomes share it and are told apart by `state`. A
    /// refusal is an error and never a receipt, and exits [`EXIT_REFUSED`].
    pub const fn exit_code(&self) -> i32 {
        EXIT_SUCCESS
    }
}

/// Written by hand rather than derived, because every key is read off the one
/// variant and a derive would need them stored as fields that could disagree.
/// The advice is not on the wire: the receipt is the record entry 64 states.
impl serde::Serialize for Receipt {
    fn serialize<S: serde::Serializer>(
        &self,
        serializer: S,
    ) -> std::result::Result<S::Ok, S::Error> {
        use serde::ser::SerializeMap;
        let mut receipt = serializer.serialize_map(None)?;
        receipt.serialize_entry("reply", &self.reply())?;
        receipt.serialize_entry("state", self.state())?;
        if let Some(verdict) = self.verdict().word() {
            receipt.serialize_entry("verdict", verdict)?;
        }
        if let Some(commands) = self.commands() {
            receipt.serialize_entry("commands", commands)?;
        }
        receipt.end()
    }
}

/// The line `onepipeline reply` and `onepipeline attest` print: the receipt, as
/// entry 64 states it. The advice beside it is the caller's to say.
///
/// # Errors
///
/// A receipt that cannot be serialised, which none is.
pub fn render_receipt(receipt: &Receipt) -> Result<String> {
    serde_json::to_string(receipt).map_err(|e| Error::Invalid(format!("receipt: {e}")))
}

/// `onepipeline reply RUN [--correlation C]`, over the envelope's **bytes**.
///
/// Parsed and validated here, through the whole submission path the verb runs:
/// the declared author and its grants, the completion grant, the node validator
/// and the envelope reviewer the launch retained and the reviewer's bar, a local
/// apply when nothing drives the run and the durable queue with the reply
/// timeout otherwise. A refusal anywhere is an error, exiting [`EXIT_REFUSED`],
/// and never a receipt.
///
/// # Errors
///
/// A malformed envelope — read a second time, leniently, to say when a retired
/// plan field is why — and every refusal the channel, the validators and the
/// reconciler can make.
pub fn reply(
    paths: &RunPaths,
    correlation: Option<&onemessagebus::Correlation>,
    envelope_json: &str,
) -> Result<Receipt> {
    let text = envelope_json.trim();
    // A reply this schema refuses is read a second time, leniently, to see
    // whether a retired plan field is why — an `add` carrying one is the same
    // planner mistake as a task carrying one, and deserves the same answer.
    let envelope: Reply = serde_json::from_str(text).map_err(|e| {
        let why = serde_json::from_str::<serde_json::Value>(text)
            .ok()
            .as_ref()
            .and_then(crate::plan::retired_field_refusal)
            .unwrap_or_else(|| e.to_string());
        Error::Refused(format!("the reply is malformed: {why}"))
    })?;
    submit(paths, correlation, &envelope)
}

/// `onepipeline attest RUN REF` — the shorthand for a reply carrying one
/// `attest`.
///
/// # Errors
///
/// Every refusal [`reply`] can make of the envelope this builds.
pub fn attest(paths: &RunPaths, reference: &str) -> Result<Receipt> {
    submit(
        paths,
        None,
        &Reply {
            version: Some(crate::channel::REPLY_ENVELOPE_VERSION),
            // The person who took the action, through the planner's own channel:
            // `attest` is not an op an observer may issue at all.
            author: Author::planner(),
            commands: vec![Command::Attest {
                reference: reference.to_owned(),
            }],
            ..Reply::default()
        },
    )
}

/// Validate a reply, queue it, and report which of the four true things happened.
///
/// The object [`render_receipt`] prints is stated in entry **64** of
/// `docs/contract-divergences.md` and nowhere else;
/// `channel::the_reply_receipt_names_each_half_the_envelope_carried` gates this
/// code against it.
fn submit(
    paths: &RunPaths,
    correlation: Option<&onemessagebus::Correlation>,
    envelope: &Reply,
) -> Result<Receipt> {
    let verdict = if envelope.carries_verdict() {
        VerdictHalf::OnTheQueue
    } else {
        VerdictHalf::NotCarried
    };
    let mut advice = Vec::new();
    let outcome = match driver::submit_envelope(paths, correlation, envelope)? {
        Submitted::Answered { reply } => ReplyOutcome::Answered { reply, verdict },
        // This process applied them itself, so there is no queue and no id in it.
        Submitted::AppliedHere { .. } => ReplyOutcome::AppliedHere { verdict },
        Submitted::AppliedByRun { reply } => ReplyOutcome::AppliedByRun { reply, verdict },
        // llmlint: ignore-block[cli_output_contract] the two outcomes this status shares are
        // told apart on stdout, by the receipt's `state`; the status answers whether the
        // envelope was accepted, which sharing it is the whole point of. Divergence 67.
        Submitted::Queued { reply } => {
            // The status cannot say what is left to happen, so the words do.
            advice.push(format!(
                "onepipeline: the edits are on run '{}'s durable command queue and have \
                 not been reconciled yet, so something has to drive the run for them to \
                 take effect: they are applied by the driver holding it, or by \
                 `onepipeline adopt {}` if nothing is driving it. They are not to be sent \
                 again — a second copy is a second edit.",
                paths.run, paths.run
            ));
            ReplyOutcome::Queued { reply, verdict }
        } // llmlint: ignore-end[cli_output_contract]
    };
    // Beside the receipt rather than in it — the receipt is a transport's answer and
    // stays exactly what it was — and only once the settle has been applied: a
    // landing settled with no release stated for it, which no probe answer will ever
    // release, is said to the person who settled it now rather than discovered later
    // from a dependent that is still waiting.
    if matches!(
        outcome,
        ReplyOutcome::AppliedHere { .. } | ReplyOutcome::AppliedByRun { .. }
    ) && envelope.commands.iter().any(|command| {
        matches!(
            command,
            Command::Settle {
                landing: Some(_),
                release: None,
                ..
            }
        )
    }) {
        // The settle is applied whatever this read finds, so a run that cannot be
        // read back now costs the advice and never the exit status the receipt has.
        // llmlint: ignore-block[changed_behavior_has_e2e] a run whose store cannot be
        // read in the instant after this process applied an edit to it is not a state an
        // invocation can put a run into; the read that succeeds is driven end to end by
        // `tests/e2e/adoption.rs`.
        match RunView::open(paths) {
            Ok(view) => advice.extend(crate::release::hold_warnings_for_stated_landings(
                &view.state,
                &envelope.commands,
            )),
            Err(unread) => advice.push(format!(
                "onepipeline: the settle was applied, and whether its landing has a release \
                 baseline could not be asked, because the run could not be read back: {unread}"
            )),
        } // llmlint: ignore-end[changed_behavior_has_e2e]
    }
    Ok(Receipt { outcome, advice })
}

/// What `onepipeline surface` queued.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Surfaced {
    /// The surface's id in the channel.
    pub surface: u64,
}

/// `onepipeline surface RUN --kind KIND --message TEXT`: raise a surface to the
/// planner.
///
/// What it is about, and what raised it, are two facts: a check-in is the
/// scheduled member's own, and a finding raised here is advice like any other.
/// Neither is a request, so neither blocks a subtree.
///
/// # Errors
///
/// A message with nothing in it once trimmed, and a channel that refuses the
/// push.
pub fn surface(paths: &RunPaths, kind: SurfaceKind, message: String) -> Result<Surfaced> {
    let message = message.trim().to_owned();
    if message.is_empty() {
        return Err(Error::Refused(
            "a surface carries what it has to say and this one carried nothing".to_owned(),
        ));
    }
    let source = if kind.as_str() == SurfaceKind::CHECK_IN {
        crate::channel::source::CHECK_IN
    } else {
        crate::channel::source::PROPOSAL
    };
    let queued = ChannelState::new(paths).push(Surface {
        id: 0,
        kind: kind.as_str().to_string(),
        message,
        source: source.to_string(),
        // Neither is a request: a check-in update and a finding raised at this
        // verb are reports, and never hold a subtree back waiting for a
        // decision. A finding that means to stop one says so through the
        // envelope's `finding` op, which carries `blocking`.
        blocking: false,
        queued_at: crate::sys::now_millis(),
        abandoned: false,
        // Raised by a call that answers its caller and is gone, so there is no
        // listener to name and none to come back: nothing abandons this and
        // nothing adopts it.
        asker: None,
        workstream: None,
        correlation: None,
    })?;
    let mut journal = Journal::open(paths);
    journal.emit(
        journal::PipelineKind::PlannerSurfaceQueued,
        journal::labels(&paths.run, None),
        journal::payload(&[
            ("kind", json!(queued.kind)),
            ("message", json!(queued.message)),
            ("source", json!(queued.source)),
            ("blocking", json!(false)),
        ]),
    )?;
    Ok(Surfaced { surface: queued.id })
}

/// The line `onepipeline surface` prints.
pub fn render_surfaced(surfaced: &Surfaced) -> String {
    json!({"surface": surfaced.surface, "state": "queued"}).to_string()
}

/// Who is stopping a run, and whether they may stop one they do not own.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StopRequest<'a> {
    /// The session acting. Passed in, never read from the environment here:
    /// the binary reads `ONEPIPELINE_LAUNCHER_SESSION` and passes it.
    pub session: &'a str,
    /// Stop a run another session owns, naming the owner.
    // llmlint: ignore[invalid_states_unrepresentable] the flag the contract fixes for this
    // request — `StopRequest { session, force }`, which `ui-api` and `aio-adopt` are written
    // against — and the one the CLI parses (`--force`); the two answers it has are the two
    // the verb has, and a named enum over them would be that boolean under another name.
    pub force: bool,
}

/// What `onepipeline stop` did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stopped {
    /// The run.
    pub run: String,
    /// Who owned it, as the stop journalled it.
    pub owner: String,
    /// Whether the stop overrode another session's ownership.
    pub forced: bool,
    /// What the teardown established, as journalled on `run-stopped`.
    pub teardown: StopTeardown,
    /// Whether the run is stopped: every process it named was reached, or none
    /// was left to reach. A teardown that was not clean is journalled and is
    /// **not** a stop — the run is still running — and [`refusal`](Self::refusal)
    /// says so.
    // llmlint: ignore[invalid_states_unrepresentable] derivable from `teardown`, and the
    // contract fixes it as a field — `Stopped { run, owner, forced, teardown, clean }` is
    // what the downstream nodes read — so it is written from the teardown in the one place a
    // `Stopped` is built, `driver::stop_run`, and nothing else assembles one. `forced` is
    // independent of both: it says whose run was stopped, not how the teardown went.
    pub clean: bool,
}

impl Stopped {
    /// The status the binary exits with: [`EXIT_SUCCESS`] for a clean stop and
    /// [`EXIT_REFUSED`] otherwise.
    pub const fn exit_code(&self) -> i32 {
        if self.clean {
            EXIT_SUCCESS
        } else {
            EXIT_REFUSED
        }
    }

    /// Why this was not a stop, for a teardown that was not clean, in the words
    /// the binary refuses with. `None` for a clean stop.
    ///
    /// Each teardown says something different because it leaves the operator in
    /// a different place: a host that gave no answer is a stop to run again, and
    /// a tree that refused this user's signal is one to end as the user that
    /// owns it.
    pub fn refusal(&self) -> Option<String> {
        let run = &self.run;
        match self.teardown {
            StopTeardown::NotAttempted => Some(format!(
                "run '{run}' was not stopped: this host gave no answer its tree could be \
                 read from — no process listing, or nothing that says whether a pid it \
                 recorded is still the process it named, each said above — so the \
                 processes the run started could not be found, and ending its driver \
                 alone would have orphaned them. The run is untouched — run \
                 `onepipeline stop {run}` again once this host answers"
            )),
            StopTeardown::PartlySignalled => Some(format!(
                "run '{run}' was only partly stopped: part of its process tree was \
                 signalled and at least one process in it is still running — one this \
                 session could not signal, or one that took the ask and stayed. Find it \
                 in this host's process list and end it as the user that owns it"
            )),
            StopTeardown::IdentityDeclined => Some(format!(
                "run '{run}' was not stopped: live processes were found, but every recorded \
                 identity disagreed with the process now holding its pid, so none was safe \
                 to signal. This is distinct from a run with nothing left to stop; inspect \
                 the declined claims above and retry only after correcting the run records"
            )),
            // llmlint: ignore-block[changed_behavior_has_e2e] this arm has no journey and
            // cannot have one: reaching it takes a run every process of which refuses this
            // user's signal, and a process this user may not signal is not a thing for a
            // suite to go and make — the same reason `sys::established` is a fold driven
            // from the answers a round of signalling gives rather than from signals. What the
            // arm is built from is proved there, at
            // `a_teardown_refused_by_everything_it_aimed_at_reports_no_signal_at_all` and
            // `a_stop_that_could_signal_nothing_it_aimed_at_says_so`; every other outcome
            // this match renders is driven end to end in `tests/e2e/driver.rs`.
            StopTeardown::Refused => Some(format!(
                "run '{run}' was not stopped: its process tree was found and every \
                 process in it refused this session's signal, so nothing was signalled \
                 and all of it is still running. Running `onepipeline stop {run}` again \
                 as this user will be refused the same way — find the tree in this \
                 host's process list and end it as the user that owns it"
            )), // llmlint: ignore-end[changed_behavior_has_e2e]
            StopTeardown::Signalled | StopTeardown::NothingToStop | StopTeardown::Elsewhere => None,
        }
    }
}

/// `onepipeline stop RUN [--force]`: end a run and its whole dispatch tree.
///
/// Refuses a run another session owns with [`Error::NotOwned`] unless forced,
/// and a forced stop journals the owner it overrode. Every teardown that reached
/// the run is journalled as `run-stopped`; only a clean one releases what the run
/// claimed and fires the run's stop hook.
///
/// # Errors
///
/// [`Error::NotOwned`] for a run another session owns, and [`Error::Refused`]
/// where this build cannot establish what the run is running — nothing is
/// signalled and nothing is recorded on that path.
pub fn stop(paths: &RunPaths, request: StopRequest<'_>) -> Result<Stopped> {
    driver::stop_run(paths, request.session, request.force)
}

/// The line `onepipeline stop` prints for a clean stop. `teardown` qualifies
/// `stopped`: the ledger record is what stops a run, and it is written either
/// way.
pub fn render_stopped(stopped: &Stopped) -> String {
    json!({
        "run_id": stopped.run,
        "stopped": true,
        "owner": stopped.owner,
        journal::STOP_TEARDOWN: stopped.teardown,
    })
    .to_string()
}

/// The program a detached adoption retains as the run's driver, and its
/// arguments, exactly as they will be spawned — nothing is appended.
///
/// The binary passes itself with `["drive-run", RUN, "--adopt"]`; an embedding
/// program carrying a hidden driver verb of its own passes that.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Retain {
    /// The executable.
    pub program: PathBuf,
    /// Its arguments, in order.
    pub args: Vec<String>,
}

/// How a run is adopted.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Adopt {
    /// This process drives the run until it settles, as `adopt` does.
    Attached,
    /// A retained process drives it, as `adopt --detach` does: validated and
    /// displaced here, then `program args…` spawned in a process group of its
    /// own with stdin null and stdout and stderr on the run's driver log, and
    /// waited for until it has claimed the run.
    Detached(Retain),
}

/// What `onepipeline adopt` did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Adopted {
    /// This process drove the run to where it settled.
    Attached {
        /// The run.
        run: String,
        /// How it settled.
        settlement: Settlement,
    },
    /// A retained driver has claimed the run.
    Detached {
        /// The run.
        run: String,
        /// The driver's pid.
        pid: u32,
    },
}

impl Adopted {
    /// The status the binary exits with: the settlement's for an attached
    /// adoption, [`EXIT_SUCCESS`] for a detached one that claimed the run.
    pub fn exit_code(&self) -> i32 {
        match self {
            Self::Attached { settlement, .. } => settlement.exit_code(),
            Self::Detached { .. } => EXIT_SUCCESS,
        }
    }
}

/// `onepipeline adopt RUN [--attach|--detach]`: attach a fresh driver to a run
/// whose ledger is intact.
///
/// Both refuse for the same reasons and refuse here, before anything is
/// written: a run another session owns ([`Error::NotOwned`]), and a run
/// something is still driving. A run the liveness verdict has called undriven is
/// taken over, ending the parked driver politely first.
///
/// # Errors
///
/// Those refusals; a lock that cannot be taken; and, detached, a retained
/// driver that did not claim the run, with what it said in its log.
pub fn adopt(paths: &RunPaths, how: Adopt) -> Result<Adopted> {
    Ok(match how {
        Adopt::Attached => Adopted::Attached {
            run: paths.run.clone(),
            settlement: driver::adopt_attached(paths, &mut |_| {})?,
        },
        Adopt::Detached(retain) => Adopted::Detached {
            run: paths.run.clone(),
            pid: driver::adopt_detached(paths, &retain)?,
        },
    })
}

/// The line `onepipeline adopt` prints: the settlement of an attached
/// adoption, or the run, the driver's pid and the two verbs that reach it for a
/// detached one — one shape for both detaching verbs, so `--detach` means on
/// `adopt` exactly what it means on `start`.
pub fn render_adopted(adopted: &Adopted) -> String {
    match adopted {
        Adopted::Attached { run, settlement } => settlement.line(run),
        Adopted::Detached { run, pid } => driver::announce_launch(run, *pid),
    }
}

/// `onepipeline drive-run RUN [--adopt]` — the body of the hidden retained
/// driver verb, published so an embedding binary can be its own detached
/// driver.
///
/// The same loop an attached launch runs in-process: it takes the ownership
/// lock, claims the run in the launch record, launches the observer graph the
/// run declares inside its own process tree, and drives the run until it
/// settles, answering the code a driver exits with — `0` for a complete graph
/// and `1` for one that settled unfinished. [`Retained::Adopting`] records the
/// adoption under the lock first. Whoever carries the verb calls
/// `agentgraph::speaks_this_cli` on its own executable first, as the binary's
/// `run` does.
///
/// # Errors
///
/// A lock another driver holds, a ledger that cannot be read, and an observer
/// graph that refuses to start.
pub fn drive_run(paths: &RunPaths, retained: Retained) -> Result<i32> {
    driver::drive_run(paths, retained)
}