g1t/crates/contracts/src/work.rs

425 lines12,683 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Issues and pull requests replace intents and attempts1//! The work service: issues, pull requests, comments and sessions.
Work service in Rust, with RFC 3339 timestamps2//!
3//! Each `*Args` struct is the argument of the method of the same name,
4//! served at `POST /rpc/<method>`.
Issues and pull requests replace intents and attempts5//!
6//! Issues and pull requests share one sequence of numbers per repository,
7//! so `#12` names exactly one of them.
Work service in Rust, with RFC 3339 timestamps8
9use serde::{Deserialize, Serialize};
10
11use crate::repos::RepoPath;
12use crate::{User, Viewer};
13
Issues and pull requests replace intents and attempts14/// Labels every repository starts with. Any other label comes into being
15/// the first time it is put on an issue.
16pub const DEFAULT_LABELS: [&str; 5] = ["bug", "feature", "docs", "chore", "question"];
17
18/// `open` or `closed`: the filter on lists of issues and pull requests. An
19/// open pull request is a draft or one ready for review; a closed one was
20/// merged or closed without merging.
Work service in Rust, with RFC 3339 timestamps21#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
Issues and pull requests replace intents and attempts23pub enum State {
Work service in Rust, with RFC 3339 timestamps24 Open,
Issues and pull requests replace intents and attempts25 Closed,
26}
27
28/// Why an issue was closed.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum IssueReason {
32 /// The work was done. If a pull request did it, `resolved_by` names it.
33 Completed,
34 NotPlanned,
Work service in Rust, with RFC 3339 timestamps35}
36
Issues and pull requests replace intents and attempts37impl IssueReason {
38 pub fn as_str(self) -> &'static str {
39 match self {
40 IssueReason::Completed => "completed",
41 IssueReason::NotPlanned => "not_planned",
42 }
43 }
44}
45
46/// Something that should change in a repository: a bug, a feature, a
47/// question. Opened by a person, an agent or an integration. Pull requests
48/// are made against it; the one that is merged resolves it.
Work service in Rust, with RFC 3339 timestamps49#[derive(Clone, Debug, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
Issues and pull requests replace intents and attempts51pub struct Issue {
Work service in Rust, with RFC 3339 timestamps52 pub id: String,
53 pub repo_id: String,
Issues and pull requests replace intents and attempts54 /// Shown as `#12`.
Work service in Rust, with RFC 3339 timestamps55 pub number: u32,
56 pub title: String,
Issues and pull requests replace intents and attempts57 /// Markdown. Also what an agent is given to work from.
58 pub body: String,
59 pub labels: Vec<String>,
60 /// Commands that must pass for a pull request to be accepted.
Work service in Rust, with RFC 3339 timestamps61 pub checks: Vec<String>,
Issues and pull requests replace intents and attempts62 pub state: State,
63 /// Set when closed.
64 pub reason: Option<IssueReason>,
65 /// The number of the pull request whose merge closed this issue.
66 pub resolved_by: Option<u32>,
Work service in Rust, with RFC 3339 timestamps67 pub author: User,
68 /// RFC 3339.
69 pub created_at: String,
Issues and pull requests replace intents and attempts70 /// RFC 3339.
71 pub updated_at: String,
72 /// RFC 3339.
73 pub closed_at: Option<String>,
74 /// Pull requests made against this issue, in any state.
75 pub pull_count: u32,
76 pub comment_count: u32,
Work service in Rust, with RFC 3339 timestamps77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(rename_all = "lowercase")]
Issues and pull requests replace intents and attempts81pub enum PullStatus {
82 /// Still being worked on.
83 Draft,
84 /// Ready for review.
85 Open,
86 Merged,
87 /// Closed without merging.
88 Closed,
Work service in Rust, with RFC 3339 timestamps89}
90
Issues and pull requests replace intents and attempts91impl PullStatus {
Work service in Rust, with RFC 3339 timestamps92 pub fn as_str(self) -> &'static str {
93 match self {
Issues and pull requests replace intents and attempts94 PullStatus::Draft => "draft",
95 PullStatus::Open => "open",
96 PullStatus::Merged => "merged",
97 PullStatus::Closed => "closed",
Work service in Rust, with RFC 3339 timestamps98 }
99 }
100
Issues and pull requests replace intents and attempts101 /// Whether the pull request can still be changed or merged.
Work service in Rust, with RFC 3339 timestamps102 pub fn is_active(self) -> bool {
Issues and pull requests replace intents and attempts103 matches!(self, PullStatus::Draft | PullStatus::Open)
Work service in Rust, with RFC 3339 timestamps104 }
105}
106
107/// Where the agent runs: on g1t's sandboxes, or in someone's own session.
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "lowercase")]
Issues and pull requests replace intents and attempts110pub enum Runtime {
Work service in Rust, with RFC 3339 timestamps111 Hosted,
112 External,
113}
114
Pull requests from branches115/// A proposed change. It is made either in a fork created for it, which is
116/// how agents work, or on a branch pushed to the repository itself.
Work service in Rust, with RFC 3339 timestamps117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
Issues and pull requests replace intents and attempts119pub struct Pull {
Work service in Rust, with RFC 3339 timestamps120 pub id: String,
121 pub repo_id: String,
Issues and pull requests replace intents and attempts122 /// Shown as `#12`.
Work service in Rust, with RFC 3339 timestamps123 pub number: u32,
Issues and pull requests replace intents and attempts124 /// The number of the issue this is for, if any.
125 pub issue: Option<u32>,
126 pub title: String,
127 /// Markdown: what changed and why. Set when marked ready.
128 pub body: Option<String>,
Work service in Rust, with RFC 3339 timestamps129 /// A label for the agent doing the work, e.g. `claude-code`.
130 pub agent: String,
Issues and pull requests replace intents and attempts131 pub runtime: Runtime,
132 pub status: PullStatus,
Pull requests from branches133 /// The fork holding the change, unless it is on a branch.
134 pub fork: Option<RepoPath>,
Work service in Rust, with RFC 3339 timestamps135 /// The fork's repository id.
Pull requests from branches136 pub fork_repo_id: Option<String>,
137 /// The branch of the repository holding the change, unless it is in a
138 /// fork.
139 pub branch: Option<String>,
Work service in Rust, with RFC 3339 timestamps140 pub head_commit: Option<String>,
Issues and pull requests replace intents and attempts141 /// For a merged pull request, what the branch pointed to before the
142 /// merge. Comparing against it shows what the pull request changed.
143 pub merge_base: Option<String>,
144 /// Username of whoever merged it.
145 pub merged_by: Option<String>,
146 /// RFC 3339.
147 pub merged_at: Option<String>,
148 /// Set on a pull request closed because another one for the same issue
149 /// was merged: that one's number.
150 pub superseded_by: Option<u32>,
151 pub author: User,
Work service in Rust, with RFC 3339 timestamps152 /// RFC 3339.
153 pub created_at: String,
154 /// RFC 3339.
155 pub updated_at: String,
156}
157
Issues and pull requests replace intents and attempts158#[derive(Clone, Debug, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct Comment {
161 pub id: String,
162 pub author: User,
163 /// Markdown.
164 pub body: String,
165 /// RFC 3339.
166 pub created_at: String,
167}
168
Work service in Rust, with RFC 3339 timestamps169#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171pub enum SessionEntryKind {
172 Prompt,
173 Message,
174 ToolCall,
175 ToolResult,
176 Note,
177}
178
Issues and pull requests replace intents and attempts179/// One step of an agent's session: the "why" behind a pull request's commits.
Work service in Rust, with RFC 3339 timestamps180#[derive(Clone, Debug, Serialize, Deserialize)]
181pub struct SessionEntry {
182 pub seq: u32,
183 pub kind: SessionEntryKind,
184 pub text: String,
185 /// For tool calls and results.
186 pub tool: Option<String>,
187 /// The fork's head commit when this entry was recorded, if known.
188 pub commit: Option<String>,
189 /// RFC 3339.
190 pub at: String,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize)]
194pub struct NewSessionEntry {
195 pub kind: SessionEntryKind,
196 pub text: String,
197 #[serde(default)]
198 pub tool: Option<String>,
199 #[serde(default)]
200 pub commit: Option<String>,
201}
202
203#[derive(Clone, Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts204pub struct IssueDetail {
205 pub issue: Issue,
206 /// Every pull request made against it, oldest first.
207 pub pulls: Vec<Pull>,
208 pub comments: Vec<Comment>,
Work service in Rust, with RFC 3339 timestamps209}
210
211#[derive(Clone, Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts212pub struct PullDetail {
213 pub pull: Pull,
214 /// The issue it is for, if any.
215 pub issue: Option<Issue>,
216 pub comments: Vec<Comment>,
Work service in Rust, with RFC 3339 timestamps217}
218
Issues and pull requests replace intents and attempts219/// `open_issue`. Returns `Outcome<Issue>`.
Work service in Rust, with RFC 3339 timestamps220#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts221pub struct OpenIssueArgs {
Work service in Rust, with RFC 3339 timestamps222 pub actor: User,
223 pub repo: RepoPath,
224 pub title: String,
225 #[serde(default)]
Issues and pull requests replace intents and attempts226 pub body: String,
227 #[serde(default)]
228 pub labels: Vec<String>,
Work service in Rust, with RFC 3339 timestamps229 #[serde(default)]
230 pub checks: Vec<String>,
231}
232
Issues and pull requests replace intents and attempts233/// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`.
Work service in Rust, with RFC 3339 timestamps234#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts235pub struct ListIssuesArgs {
Work service in Rust, with RFC 3339 timestamps236 pub repo: RepoPath,
237 pub viewer: Viewer,
238 #[serde(default)]
Issues and pull requests replace intents and attempts239 pub state: Option<State>,
240 /// Only issues carrying this label.
241 #[serde(default)]
242 pub label: Option<String>,
Work service in Rust, with RFC 3339 timestamps243}
244
Issues and pull requests replace intents and attempts245/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
Work service in Rust, with RFC 3339 timestamps246#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts247pub struct ListPullsArgs {
248 pub repo: RepoPath,
249 pub viewer: Viewer,
250 #[serde(default)]
251 pub state: Option<State>,
252}
253
254/// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`),
255/// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels`
256/// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two
257/// ignore `number`.
258#[derive(Debug, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase")]
260pub struct ViewArgs {
Work service in Rust, with RFC 3339 timestamps261 pub repo: RepoPath,
Issues and pull requests replace intents and attempts262 #[serde(default)]
Work service in Rust, with RFC 3339 timestamps263 pub number: u32,
264 pub viewer: Viewer,
Issues and pull requests replace intents and attempts265 /// For `read_session`: only entries after this sequence number.
266 #[serde(default)]
267 pub after_seq: u32,
268}
269
270/// How many issues and pull requests are open on a repository.
271#[derive(Debug, Serialize, Deserialize)]
272pub struct Counts {
273 pub issues: u32,
274 pub pulls: u32,
275}
276
277/// `update_issue`: changes whichever fields are given. The author or a
278/// member of the workspace may. Returns `Outcome<Issue>`.
279#[derive(Debug, Serialize, Deserialize)]
280pub struct UpdateIssueArgs {
281 pub actor: User,
282 pub repo: RepoPath,
283 pub number: u32,
284 #[serde(default)]
285 pub title: Option<String>,
286 #[serde(default)]
287 pub body: Option<String>,
288 #[serde(default)]
289 pub labels: Option<Vec<String>>,
Work service in Rust, with RFC 3339 timestamps290}
291
Issues and pull requests replace intents and attempts292/// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
Work service in Rust, with RFC 3339 timestamps293#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts294pub struct IssueActionArgs {
Work service in Rust, with RFC 3339 timestamps295 pub actor: User,
Issues and pull requests replace intents and attempts296 pub repo: RepoPath,
297 pub number: u32,
298 /// For `close_issue`; `completed` if left out.
299 #[serde(default)]
300 pub reason: Option<IssueReason>,
Work service in Rust, with RFC 3339 timestamps301}
302
Issues and pull requests replace intents and attempts303/// `add_comment`, on an issue or a pull request. Returns `Outcome<Comment>`.
Work service in Rust, with RFC 3339 timestamps304#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts305pub struct AddCommentArgs {
Work service in Rust, with RFC 3339 timestamps306 pub actor: User,
Issues and pull requests replace intents and attempts307 pub repo: RepoPath,
308 pub number: u32,
309 pub body: String,
Work service in Rust, with RFC 3339 timestamps310}
311
Pull requests from branches312/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
313/// request to push to. With it, opens a pull request, ready for review,
314/// for a branch already pushed to the repo. Returns `Outcome<Pull>`.
Work service in Rust, with RFC 3339 timestamps315#[derive(Debug, Serialize, Deserialize)]
Issues and pull requests replace intents and attempts316pub struct OpenPullArgs {
317 pub actor: User,
318 pub repo: RepoPath,
319 /// The issue this is for.
Work service in Rust, with RFC 3339 timestamps320 #[serde(default)]
Issues and pull requests replace intents and attempts321 pub issue: Option<u32>,
322 /// Defaults to the issue's title; required without an issue.
323 #[serde(default)]
324 pub title: String,
Pull requests from branches325 /// What changed and why. Usually set later, when a draft is marked ready.
326 #[serde(default)]
327 pub body: String,
328 /// A branch of the repository that already holds the change.
329 #[serde(default)]
330 pub branch: Option<String>,
Issues and pull requests replace intents and attempts331 #[serde(default)]
332 pub agent: String,
333 pub runtime: Runtime,
Work service in Rust, with RFC 3339 timestamps334}
335
Issues and pull requests replace intents and attempts336/// `ready_pull`, `close_pull` and `merge_pull`. Each returns `Outcome<Pull>`.
Work service in Rust, with RFC 3339 timestamps337#[derive(Debug, Serialize, Deserialize)]
338#[serde(rename_all = "camelCase")]
Issues and pull requests replace intents and attempts339pub struct PullActionArgs {
Work service in Rust, with RFC 3339 timestamps340 pub actor: User,
Issues and pull requests replace intents and attempts341 pub repo: RepoPath,
342 pub number: u32,
343 /// For `ready_pull`: what changed and why.
Work service in Rust, with RFC 3339 timestamps344 #[serde(default)]
345 pub summary: String,
Issues and pull requests replace intents and attempts346 /// For `merge_pull`: leave the issue open and the other pull requests
347 /// for it untouched, because this one is only part of the work.
348 #[serde(default)]
349 pub keep_issue_open: bool,
Work service in Rust, with RFC 3339 timestamps350}
351
Issues and pull requests replace intents and attempts352/// A pull request in progress, with where it lives.
353#[derive(Debug, Serialize, Deserialize)]
354pub struct ActivePull {
355 pub pull: Pull,
356 pub issue: Option<Issue>,
357}
358
359/// `list_active_pulls`: drafts and open pull requests the viewer started,
360/// most recently active first. Returns `Vec<ActivePull>`.
Work service in Rust, with RFC 3339 timestamps361#[derive(Debug, Serialize, Deserialize)]
362pub struct ViewerArgs {
363 pub viewer: Viewer,
364}
365
366/// `append_session`. Returns `Outcome<Appended>`.
367#[derive(Debug, Serialize, Deserialize)]
368pub struct AppendSessionArgs {
369 pub actor: User,
Issues and pull requests replace intents and attempts370 pub repo: RepoPath,
371 pub number: u32,
Work service in Rust, with RFC 3339 timestamps372 pub entries: Vec<NewSessionEntry>,
373}
374
375#[derive(Debug, Serialize, Deserialize)]
376pub struct Appended {
377 pub count: u32,
378}
Issues and pull requests replace intents and attempts379
380/// Lowercases, trims and de-duplicates labels, dropping empty ones.
381/// Returns `None` if there are too many or one is too long.
382pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
383 const MAX_LABELS: usize = 10;
384 const MAX_LABEL_CHARS: usize = 40;
385 let mut normalized: Vec<String> = Vec::new();
386 for label in labels {
387 let label = label
388 .split_whitespace()
389 .collect::<Vec<_>>()
390 .join(" ")
391 .to_lowercase();
392 if label.is_empty() || normalized.contains(&label) {
393 continue;
394 }
395 if label.chars().count() > MAX_LABEL_CHARS {
396 return None;
397 }
398 normalized.push(label);
399 }
400 (normalized.len() <= MAX_LABELS).then_some(normalized)
401}
402
403#[cfg(test)]
404mod tests {
405 use super::normalize_labels;
406
407 fn labels(names: &[&str]) -> Vec<String> {
408 names.iter().map(|name| (*name).to_owned()).collect()
409 }
410
411 #[test]
412 fn labels_are_lowercased_trimmed_and_unique() {
413 assert_eq!(
414 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
415 Some(labels(&["bug", "good first issue"]))
416 );
417 }
418
419 #[test]
420 fn too_long_or_too_many_labels_are_refused() {
421 assert_eq!(normalize_labels(&["x".repeat(41)]), None);
422 let many: Vec<String> = (0..11).map(|i| format!("label-{i}")).collect();
423 assert_eq!(normalize_labels(&many), None);
424 }
425}