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        // A seat judges; it never satisfies the stage's check itself, whatever
37        // that check happens to be. Its report is the only thing counted from
38        // it, so it is the only thing the brief asks for.
39        (WorkerRole::PanelReviewer, _) => {
40            "This stage passes only on your reported verdict; the work under review is not yours to change"
41        }
42        (WorkerRole::Stage, Check::Reported) => {
43            "This stage passes only on your reported verdict; nothing else you do can pass it"
44        }
45        (WorkerRole::Stage, Check::Actor(Actor::Builtin(Builtin::Commits))) => {
46            "Commit your work to the run branch"
47        }
48        (WorkerRole::Stage, Check::None) => {
49            "This stage passes on your own exit status: exit 0 only if the work succeeded"
50        }
51        // Every other check is an independent judge that runs after this
52        // worker. It is named as a judge and not by name: what it is belongs to
53        // the flow, and a worker is told about its own stage.
54        (WorkerRole::Stage, Check::Actor(_) | Check::Panel(_)) => {
55            "An independent check judges this stage's work after you exit"
56        }
57    };
58    vec![obligation.to_owned()]
59}
60
61/// How a stage's `result_check` is named in the brief's stage block.
62///
63/// The kind of judge, never its argv or its target: a worker is entitled to
64/// know what its own stage turns on, and not to the rest of the flow's shape.
65pub fn check_label(check: &Check) -> &'static str {
66    match check {
67        Check::None => "none",
68        Check::Reported => "reported",
69        Check::Panel(_) => "panel",
70        Check::Actor(Actor::Agent) => "agent",
71        Check::Actor(Actor::Exec { .. }) => "exec",
72        Check::Actor(Actor::Builtin(Builtin::Commits)) => "commits",
73        Check::Actor(Actor::Builtin(Builtin::Merge)) => "merge",
74        Check::Actor(Actor::Builtin(Builtin::Sync)) => "sync",
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use crate::flow::{Actor, Builtin, Check, Panel, Reviewer};
81
82    use super::{WorkerRole, check_label, definition_of_done};
83
84    fn panel() -> Panel {
85        Panel {
86            prompt: "prompts/panel.md".into(),
87            reviewers: vec![Reviewer {
88                target: "alpha".into(),
89                model: None,
90                effort: None,
91            }],
92            quorum: 1,
93        }
94    }
95
96    /// Every check a stage can carry, so the enumeration below is exhaustive
97    /// rather than a sample of the ones that were easy to think of.
98    fn every_check() -> Vec<Check> {
99        vec![
100            Check::None,
101            Check::Reported,
102            Check::Actor(Actor::Agent),
103            Check::Actor(Actor::Exec {
104                cmd: vec!["cargo".into(), "test".into()],
105            }),
106            Check::Actor(Actor::Builtin(Builtin::Commits)),
107            Check::Actor(Actor::Builtin(Builtin::Merge)),
108            Check::Actor(Actor::Builtin(Builtin::Sync)),
109            Check::Panel(panel()),
110        ]
111    }
112
113    /// The rule the old code made untestable: a worker is told to commit when,
114    /// and only when, a commit is what its stage's check reads. Everything else
115    /// — a reported stage, an exec check, any panel seat — is told what its own
116    /// stage turns on instead, and a reviewer that trusts the brief over its
117    /// prompt therefore does not start writing code.
118    #[test]
119    fn only_a_commits_checked_stage_worker_is_told_to_commit() {
120        for role in [WorkerRole::Stage, WorkerRole::PanelReviewer] {
121            for check in every_check() {
122                let text = definition_of_done(role, &check).join("\n").to_lowercase();
123                let expected = role == WorkerRole::Stage
124                    && check == Check::Actor(Actor::Builtin(Builtin::Commits));
125                assert_eq!(
126                    text.contains("commit"),
127                    expected,
128                    "role {role:?} with check {check:?} said: {text}"
129                );
130            }
131        }
132    }
133
134    /// A definition of done that says nothing is worse than none at all: it
135    /// reads as "no obligation" to an agent scanning its brief.
136    #[test]
137    fn every_role_and_check_states_exactly_one_obligation() {
138        for role in [WorkerRole::Stage, WorkerRole::PanelReviewer] {
139            for check in every_check() {
140                let obligation = definition_of_done(role, &check);
141                assert_eq!(obligation.len(), 1, "{role:?} / {check:?}");
142                assert!(!obligation[0].trim().is_empty(), "{role:?} / {check:?}");
143            }
144        }
145    }
146
147    /// The shipped default flow's builder is `result_check: { builtin:
148    /// commits }`, and what it is told is unchanged by this module existing.
149    #[test]
150    fn the_builders_definition_of_done_is_unchanged() {
151        assert_eq!(
152            definition_of_done(
153                WorkerRole::Stage,
154                &Check::Actor(Actor::Builtin(Builtin::Commits))
155            ),
156            vec!["Commit your work to the run branch".to_owned()]
157        );
158    }
159
160    /// A reported stage's worker is told what the stage turns on and nothing
161    /// about the run branch: that is the contradiction this ticket removes.
162    #[test]
163    fn a_reported_stage_turns_only_on_the_verdict() {
164        for role in [WorkerRole::Stage, WorkerRole::PanelReviewer] {
165            let text = definition_of_done(role, &Check::Reported).join("\n");
166            assert!(text.contains("reported verdict"), "{role:?}: {text}");
167        }
168    }
169
170    /// The label names the kind of judge, so a worker can tell a reported stage
171    /// from an exec-checked one without being handed the flow's commands.
172    #[test]
173    fn the_check_label_names_the_kind_and_not_the_command() {
174        assert_eq!(check_label(&Check::None), "none");
175        assert_eq!(check_label(&Check::Reported), "reported");
176        assert_eq!(check_label(&Check::Panel(panel())), "panel");
177        assert_eq!(
178            check_label(&Check::Actor(Actor::Builtin(Builtin::Commits))),
179            "commits"
180        );
181        let exec = Check::Actor(Actor::Exec {
182            cmd: vec!["cargo".into(), "test".into()],
183        });
184        assert_eq!(check_label(&exec), "exec");
185    }
186}