Skip to main content

sloop/worker/
brief.rs

1//! *What* a worker was assigned: the parts of the brief that are policy rather
2//! than a lookup of the run's own facts.
3//!
4//! One function authors the definition of done, over the executing stage's
5//! `Check` and the role of the worker reading it. That is the whole point of
6//! the module: while the text lived in a socket handler it was keyed on the
7//! run, so every caller was told to commit — including the reviewers Sloop's
8//! own prompts tell to change nothing. A stage's obligation is a property of
9//! the stage, and deriving it here is what lets a test enumerate every stage
10//! shape and every role at once.
11
12use crate::flow::{Actor, Builtin, Check};
13
14/// Which worker is reading the brief.
15///
16/// The distinction is not the stage's — a panel's seats and the action they
17/// judge run against the same stage, so `Check` alone cannot separate them.
18/// It comes from the credential the caller presented, which is the only thing
19/// that knows whether it was minted for the stage or for a seat on its panel.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum WorkerRole {
22    /// The stage's own worker: the process the stage's action runs.
23    Stage,
24    /// One seat on a panel judging the stage. It reads and reports; the work
25    /// under review is not its to change.
26    PanelReviewer,
27}
28
29/// What the executing stage turns on, as the brief states it.
30///
31/// Facts about this stage only. The mechanics of the CLI — which verb to call,
32/// how many times, what silence costs — belong to the prompt, and repeating
33/// them here is how the two channels came to contradict each other.
34pub fn definition_of_done(role: WorkerRole, check: &Check) -> Vec<String> {
35    let obligation = match (role, check) {
36        (WorkerRole::PanelReviewer, _) => {
37            "This stage passes only on your reported verdict; the work under review is not yours to change"
38        }
39        (WorkerRole::Stage, Check::Reported) => {
40            "This stage passes only on your reported verdict; nothing else you do can pass it"
41        }
42        (WorkerRole::Stage, Check::Actor(Actor::Builtin(Builtin::Commits))) => {
43            "Commit your work to the run branch"
44        }
45        (WorkerRole::Stage, Check::None) => {
46            "This stage passes on your own exit status: exit 0 only if the work succeeded"
47        }
48        (WorkerRole::Stage, Check::Actor(_) | Check::Panel(_)) => {
49            "An independent check judges this stage's work after you exit"
50        }
51    };
52    vec![obligation.to_owned()]
53}
54
55/// How a stage's `result_check` is named in the brief's stage block.
56///
57/// The kind of judge, never its argv or its target: a worker is entitled to
58/// know what its own stage turns on, and not to the rest of the flow's shape.
59pub fn check_label(check: &Check) -> &'static str {
60    match check {
61        Check::None => "none",
62        Check::Reported => "reported",
63        Check::Panel(_) => "panel",
64        Check::Actor(Actor::Agent) => "agent",
65        Check::Actor(Actor::Exec { .. }) => "exec",
66        Check::Actor(Actor::Builtin(Builtin::Commits)) => "commits",
67        Check::Actor(Actor::Builtin(Builtin::Merge)) => "merge",
68        Check::Actor(Actor::Builtin(Builtin::Sync)) => "sync",
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use crate::flow::{Actor, Builtin, Check, Panel, Reviewer};
75
76    use super::{WorkerRole, check_label, definition_of_done};
77
78    fn panel() -> Panel {
79        Panel {
80            prompt: "prompts/panel.md".into(),
81            reviewers: vec![Reviewer {
82                target: "alpha".into(),
83                model: None,
84                effort: None,
85            }],
86            quorum: 1,
87        }
88    }
89
90    /// Every check a stage can carry, so the enumeration below is exhaustive
91    /// rather than a sample of the ones that were easy to think of.
92    fn every_check() -> Vec<Check> {
93        vec![
94            Check::None,
95            Check::Reported,
96            Check::Actor(Actor::Agent),
97            Check::Actor(Actor::Exec {
98                cmd: vec!["cargo".into(), "test".into()],
99            }),
100            Check::Actor(Actor::Builtin(Builtin::Commits)),
101            Check::Actor(Actor::Builtin(Builtin::Merge)),
102            Check::Actor(Actor::Builtin(Builtin::Sync)),
103            Check::Panel(panel()),
104        ]
105    }
106
107    /// The rule the old code made untestable: a worker is told to commit when,
108    /// and only when, a commit is what its stage's check reads. Everything else
109    /// — a reported stage, an exec check, any panel seat — is told what its own
110    /// stage turns on instead, and a reviewer that trusts the brief over its
111    /// prompt therefore does not start writing code.
112    #[test]
113    fn only_a_commits_checked_stage_worker_is_told_to_commit() {
114        for role in [WorkerRole::Stage, WorkerRole::PanelReviewer] {
115            for check in every_check() {
116                let text = definition_of_done(role, &check).join("\n").to_lowercase();
117                let expected = role == WorkerRole::Stage
118                    && check == Check::Actor(Actor::Builtin(Builtin::Commits));
119                assert_eq!(
120                    text.contains("commit"),
121                    expected,
122                    "role {role:?} with check {check:?} said: {text}"
123                );
124            }
125        }
126    }
127
128    /// A definition of done that says nothing is worse than none at all: it
129    /// reads as "no obligation" to an agent scanning its brief.
130    #[test]
131    fn every_role_and_check_states_exactly_one_obligation() {
132        for role in [WorkerRole::Stage, WorkerRole::PanelReviewer] {
133            for check in every_check() {
134                let obligation = definition_of_done(role, &check);
135                assert_eq!(obligation.len(), 1, "{role:?} / {check:?}");
136                assert!(!obligation[0].trim().is_empty(), "{role:?} / {check:?}");
137            }
138        }
139    }
140
141    /// The shipped default flow's builder is `result_check: { builtin:
142    /// commits }`, and what it is told is unchanged by this module existing.
143    #[test]
144    fn the_builders_definition_of_done_is_unchanged() {
145        assert_eq!(
146            definition_of_done(
147                WorkerRole::Stage,
148                &Check::Actor(Actor::Builtin(Builtin::Commits))
149            ),
150            vec!["Commit your work to the run branch".to_owned()]
151        );
152    }
153
154    /// A reported stage's worker is told what the stage turns on and nothing
155    /// about the run branch: that is the contradiction this ticket removes.
156    #[test]
157    fn a_reported_stage_turns_only_on_the_verdict() {
158        for role in [WorkerRole::Stage, WorkerRole::PanelReviewer] {
159            let text = definition_of_done(role, &Check::Reported).join("\n");
160            assert!(text.contains("reported verdict"), "{role:?}: {text}");
161        }
162    }
163
164    /// The label names the kind of judge, so a worker can tell a reported stage
165    /// from an exec-checked one without being handed the flow's commands.
166    #[test]
167    fn the_check_label_names_the_kind_and_not_the_command() {
168        assert_eq!(check_label(&Check::None), "none");
169        assert_eq!(check_label(&Check::Reported), "reported");
170        assert_eq!(check_label(&Check::Panel(panel())), "panel");
171        assert_eq!(
172            check_label(&Check::Actor(Actor::Builtin(Builtin::Commits))),
173            "commits"
174        );
175        let exec = Check::Actor(Actor::Exec {
176            cmd: vec!["cargo".into(), "test".into()],
177        });
178        assert_eq!(check_label(&exec), "exec");
179    }
180}