1pub mod board;
37pub mod store;
38
39#[cfg(test)]
40mod tests;
41
42use serde::{Deserialize, Serialize};
43
44use lean_ctx_protocol::UtcTimestamp;
45use lean_ctx_protocol::hive::tasks::{TaskActorKind, TaskActorV1, TaskState};
46
47pub use board::{Amendment, Board, LocalTask, Proposal, SharedRef, Submission, Transition};
48pub use store::Store;
49
50pub const LOCAL_VAULT: &str = "local";
52pub const DEFAULT_CLAIM_MINUTES: u32 = 60;
54pub const MIN_CLAIM_MINUTES: u32 = 5;
55pub const MAX_CLAIM_MINUTES: u32 = 240;
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Actor {
61 Agent { id: String, label: Option<String> },
63 Person { label: String },
65}
66
67impl Actor {
68 pub fn agent(id: impl Into<String>) -> Self {
69 Self::Agent {
70 id: id.into(),
71 label: None,
72 }
73 }
74
75 pub fn person(label: impl Into<String>) -> Self {
76 Self::Person {
77 label: label.into(),
78 }
79 }
80
81 pub fn agent_id(&self) -> Option<&str> {
83 match self {
84 Self::Agent { id, .. } => Some(id),
85 Self::Person { .. } => None,
86 }
87 }
88
89 pub fn view(&self) -> TaskActorV1 {
92 match self {
93 Self::Agent { id, label } => TaskActorV1 {
94 kind: TaskActorKind::Instance,
95 label: label.clone().unwrap_or_else(|| id.clone()),
96 agent: Some(id.clone()),
97 owner: None,
98 model: None,
99 mine: false,
100 profile_id: None,
101 },
102 Self::Person { label } => TaskActorV1 {
103 kind: TaskActorKind::Person,
104 label: label.clone(),
105 agent: None,
106 owner: None,
107 model: None,
108 mine: false,
109 profile_id: None,
110 },
111 }
112 }
113
114 fn is(&self, actor: &TaskActorV1) -> bool {
116 match self {
117 Self::Agent { id, .. } => actor.agent.as_deref() == Some(id.as_str()),
118 Self::Person { .. } => actor.kind == TaskActorKind::Person,
119 }
120 }
121
122 fn is_exactly(&self, actor: &TaskActorV1) -> bool {
125 match self {
126 Self::Agent { .. } => self.is(actor),
127 Self::Person { label } => actor.kind == TaskActorKind::Person && actor.label == *label,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum TaskEvent {
136 Proposed,
137 Accepted,
138 Declined,
139 Claimed,
140 Updated,
141 Released,
142 Submitted,
143 Failed,
144 Verified,
145 Rejected,
146 Stopped,
147 Amended,
149 Shared,
151 Fields,
153}
154
155impl TaskEvent {
156 pub fn as_str(self) -> &'static str {
157 match self {
158 Self::Proposed => "proposed",
159 Self::Accepted => "accepted",
160 Self::Declined => "declined",
161 Self::Claimed => "claimed",
162 Self::Updated => "updated",
163 Self::Released => "released",
164 Self::Submitted => "submitted",
165 Self::Failed => "failed",
166 Self::Verified => "verified",
167 Self::Rejected => "rejected",
168 Self::Stopped => "stopped",
169 Self::Amended => "amended",
170 Self::Shared => "shared",
171 Self::Fields => "fields",
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct Change {
179 pub seq: u64,
181 pub number: u64,
182 pub event: TaskEvent,
183 pub state: TaskState,
184 pub worker: Option<String>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum TaskError {
193 NotFound(u64),
194 InvalidState {
196 number: u64,
197 reason: &'static str,
198 },
199 Forbidden {
201 number: u64,
202 reason: &'static str,
203 },
204 Invalid(String),
206 Store(String),
208}
209
210impl TaskError {
211 pub fn code(&self) -> &'static str {
214 match self {
215 Self::NotFound(_) => "not_found",
216 Self::InvalidState { .. } => "invalid_state",
217 Self::Forbidden { .. } => "forbidden",
218 Self::Invalid(_) => "invalid_argument",
219 Self::Store(_) => "unavailable",
220 }
221 }
222
223 pub fn reason(&self) -> Option<&'static str> {
225 match self {
226 Self::InvalidState { reason, .. } | Self::Forbidden { reason, .. } => Some(reason),
227 _ => None,
228 }
229 }
230}
231
232impl std::fmt::Display for TaskError {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 match self {
235 Self::NotFound(n) => write!(f, "task #{n} does not exist"),
236 Self::InvalidState { number, reason } | Self::Forbidden { number, reason } => {
237 write!(f, "task #{number}: {}", describe(reason))
238 }
239 Self::Invalid(message) | Self::Store(message) => f.write_str(message),
240 }
241 }
242}
243
244impl std::error::Error for TaskError {}
245
246fn describe(reason: &str) -> &'static str {
247 match reason {
248 "task_claimed" => "another agent holds its claim (task_claimed)",
249 "task_not_open" => "it is not open (task_not_open)",
250 "not_proposed" => "it is not a proposal (not_proposed)",
251 "not_claimed" => "it is not claimed (not_claimed)",
252 "task_closed" => "it is closed (task_closed)",
253 "not_submitted" => "no result waits for a check (not_submitted)",
254 "self_verify" => {
255 "the agent that submitted the result never checks it; a person or another agent does (self_verify)"
256 }
257 "not_holder" => "only the agent holding its claim does that (not_holder)",
258 "agents_claim" => "only an agent claims a task (agents_claim)",
259 "task_submitted" => "its result waits for a check; verify it instead (task_submitted)",
260 "agents_stop" => "only a person stops a task (agents_stop)",
261 "already_shared" => "it was already brought into a vault (already_shared)",
262 "too_many_tasks" => "the project holds too many unfinished tasks (too_many_tasks)",
263 "not_submitter" => {
264 "only the agent that submitted its result corrects it; a merge record also a person or a Queen session (not_submitter)"
265 }
266 "not_merger" => {
267 "the reviewer never records the merge; the submitter, a person or a Queen session does (not_merger)"
268 }
269 "not_amendable" => "it has no submitted or done result to amend (not_amendable)",
270 "record_merge_only" => {
271 "it is done: it only takes its merge record, --merge <sha> (record_merge_only)"
272 }
273 "not_merged_yet" => {
274 "its result waits for a check; a merge is recorded once it is done (not_merged_yet)"
275 }
276 "merge_recorded" => "its merge is already recorded (merge_recorded)",
277 _ => "refused",
278 }
279}
280
281pub fn timestamp(at: chrono::DateTime<chrono::Utc>) -> UtcTimestamp {
283 UtcTimestamp::new(at.format("%Y-%m-%dT%H:%M:%SZ").to_string())
284 .unwrap_or_else(|_| unreachable_timestamp())
285}
286
287#[cold]
288fn unreachable_timestamp() -> UtcTimestamp {
289 UtcTimestamp::new("1970-01-01T00:00:00Z").unwrap_or_else(|_| unreachable!())
291}