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}