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.
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 1 | //! `@g1t` in a comment, and the label rule. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 2 | //! |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 3 | //! A comment that mentions `@g1t` is recorded here as it is made, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 4 | //! with who wrote it and what they seem to want. The runner hears the |
| 5 | //! comment was made, takes the mention (once: a mention is one run at most) | |
| 6 | //! and does what it asks through the flows that already exist: assigning | |
| 7 | //! the issue, sending the author of a g1t pull request back, a review, or | |
| 8 | //! an answer in the thread. Whatever it does, or why it did nothing, is | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 9 | //! said back in the thread as `g1t`. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 10 | //! |
| 11 | //! The label rule is a repository's "when an issue gets this label, give it | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 12 | //! to g1t": the issue is queued for an agent, as a plan's issues are. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 13 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 14 | use g1t_contracts::access::{self, Capability}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 15 | use g1t_contracts::events::CommentCreated; |
| 16 | use g1t_contracts::repos::{GetByIdArgs, PathByIdArgs, Repo, RepoPath}; | |
| 17 | use g1t_contracts::time::rfc3339; | |
| 18 | use g1t_contracts::work::*; | |
| 19 | use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, new_id}; | |
| 20 | use g1t_kit::now_ms; | |
| 21 | use serde::{Deserialize, Serialize}; | |
| 22 | use worker::Result; | |
| 23 | use worker::wasm_bindgen::JsValue; | |
| 24 | ||
| 25 | use crate::Work; | |
| 26 | use crate::lifecycle::{POLICY_ACTOR_ID, POLICY_ACTOR_NAME, made_by_g1t}; | |
| 27 | use crate::reviews::{AGENT_ID, AGENT_NAME}; | |
| 28 | use crate::rows::{NumberRow, ValueRow}; | |
| 29 | ||
| 30 | /// How g1t's agent is mentioned. Matched without regard to case. | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 31 | pub(crate) const HANDLE: &str = "@g1t"; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 32 | /// How long a revision asked for in a comment may take before another step can. |
| 33 | const REVISION_MINUTES: u64 = 60; | |
| 34 | const MAX_REPLY_CHARS: usize = 20_000; | |
| 35 | const MAX_LABEL_CHARS: usize = 40; | |
| 36 | ||
| 37 | // --- Reading a comment ----------------------------------------------------- | |
| 38 | ||
| 39 | /// What a comment says in its own words: fenced code blocks, code spans | |
| 40 | /// and quoted lines are left out, so that quoting or showing a mention | |
| 41 | /// does not make one. | |
| 42 | pub(crate) fn spoken(body: &str) -> String { | |
| 43 | let mut out = String::new(); | |
| 44 | // The fence that opened the code block being skipped, if one is. | |
| 45 | let mut fence: Option<String> = None; | |
| 46 | for line in body.lines() { | |
| 47 | let trimmed = line.trim_start(); | |
| 48 | if let Some(marker) = &fence { | |
| 49 | if trimmed.starts_with(marker.as_str()) { | |
| 50 | fence = None; | |
| 51 | } | |
| 52 | out.push('\n'); | |
| 53 | continue; | |
| 54 | } | |
| 55 | if let Some(first) = trimmed.chars().next() | |
| 56 | && (first == '`' || first == '~') | |
| 57 | { | |
| 58 | let run = trimmed.chars().take_while(|c| *c == first).count(); | |
| 59 | if run >= 3 { | |
| 60 | fence = Some(std::iter::repeat_n(first, run).collect()); | |
| 61 | out.push('\n'); | |
| 62 | continue; | |
| 63 | } | |
| 64 | } | |
| 65 | if trimmed.starts_with('>') { | |
| 66 | out.push('\n'); | |
| 67 | continue; | |
| 68 | } | |
| 69 | out.push_str(&without_code_spans(line)); | |
| 70 | out.push('\n'); | |
| 71 | } | |
| 72 | out | |
| 73 | } | |
| 74 | ||
| 75 | /// A line with its code spans blanked: a run of backticks opens one, and | |
| 76 | /// the next run of the same length closes it. An unclosed run is text. | |
| 77 | fn without_code_spans(line: &str) -> String { | |
| 78 | let chars: Vec<char> = line.chars().collect(); | |
| 79 | let run_at = |at: usize| chars[at..].iter().take_while(|c| **c == '`').count(); | |
| 80 | let mut out = String::new(); | |
| 81 | let mut i = 0; | |
| 82 | while i < chars.len() { | |
| 83 | if chars[i] != '`' { | |
| 84 | out.push(chars[i]); | |
| 85 | i += 1; | |
| 86 | continue; | |
| 87 | } | |
| 88 | let open = run_at(i); | |
| 89 | let mut j = i + open; | |
| 90 | let mut closed = None; | |
| 91 | while j < chars.len() { | |
| 92 | if chars[j] == '`' { | |
| 93 | let run = run_at(j); | |
| 94 | if run == open { | |
| 95 | closed = Some(j + run); | |
| 96 | break; | |
| 97 | } | |
| 98 | j += run; | |
| 99 | } else { | |
| 100 | j += 1; | |
| 101 | } | |
| 102 | } | |
| 103 | match closed { | |
| 104 | Some(end) => { | |
| 105 | out.push(' '); | |
| 106 | i = end; | |
| 107 | } | |
| 108 | None => { | |
| 109 | out.extend(std::iter::repeat_n('`', open)); | |
| 110 | i += open; | |
| 111 | } | |
| 112 | } | |
| 113 | } | |
| 114 | out | |
| 115 | } | |
| 116 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 117 | /// Where `text` mentions `@g1t`, as byte ranges. Not in an email |
| 118 | /// address, a domain, a package scope or a longer name (`ops@g1t.sh`, | |
| 119 | /// `@g1t.dev`, `@g1t/contracts`, `@g1t-bot`). | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 120 | pub(crate) fn mentions_in(text: &str) -> Vec<(usize, usize)> { |
| 121 | let bytes = text.as_bytes(); | |
| 122 | let handle = HANDLE.as_bytes(); | |
| 123 | let mut found = Vec::new(); | |
| 124 | let mut at = 0; | |
| 125 | while at + handle.len() <= bytes.len() { | |
| 126 | if !bytes[at..at + handle.len()].eq_ignore_ascii_case(handle) { | |
| 127 | at += 1; | |
| 128 | continue; | |
| 129 | } | |
| 130 | let end = at + handle.len(); | |
| 131 | let before = text[..at].chars().next_back(); | |
| 132 | let mut after = text[end..].chars(); | |
| 133 | let starts_clean = | |
| 134 | before.is_none_or(|c| !(c.is_alphanumeric() || "._%+-/\\@`=".contains(c))); | |
| 135 | let ends_clean = match after.next() { | |
| 136 | None => true, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 137 | // A longer name, or a path such as the `@g1t/contracts` package. |
| 138 | Some(c) if c.is_alphanumeric() || "_-@/\\".contains(c) => false, | |
| 139 | // The end of a sentence, or a domain: `@g1t.dev`. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 140 | Some('.') => after.next().is_none_or(|c| !c.is_alphanumeric()), |
| 141 | Some(_) => true, | |
| 142 | }; | |
| 143 | if starts_clean && ends_clean { | |
| 144 | found.push((at, end)); | |
| 145 | at = end; | |
| 146 | } else { | |
| 147 | at += 1; | |
| 148 | } | |
| 149 | } | |
| 150 | found | |
| 151 | } | |
| 152 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 153 | /// Whether a comment mentions `@g1t` in its own words. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 154 | pub(crate) fn mentions_agent(body: &str) -> bool { |
| 155 | !mentions_in(&spoken(body)).is_empty() | |
| 156 | } | |
| 157 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 158 | /// What someone who mentions `@g1t` wants. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 159 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 160 | #[serde(rename_all = "lowercase")] | |
| 161 | pub(crate) enum Intent { | |
| 162 | /// Do something: take the issue, or change the pull request. | |
| 163 | Work, | |
| 164 | /// Answer in the thread, changing nothing. | |
| 165 | Question, | |
| 166 | /// Review the pull request. | |
| 167 | Review, | |
| 168 | } | |
| 169 | ||
| 170 | impl Intent { | |
| 171 | fn as_str(self) -> &'static str { | |
| 172 | match self { | |
| 173 | Intent::Work => "work", | |
| 174 | Intent::Question => "question", | |
| 175 | Intent::Review => "review", | |
| 176 | } | |
| 177 | } | |
| 178 | ||
| 179 | fn parse(text: &str) -> Self { | |
| 180 | match text { | |
| 181 | "question" => Intent::Question, | |
| 182 | "review" => Intent::Review, | |
| 183 | _ => Intent::Work, | |
| 184 | } | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | /// Words that start a request to do something. | |
| 189 | const WORK_VERBS: &[&str] = &[ | |
| 190 | "take", "fix", "implement", "do", "work", "handle", "pick", "start", "add", "update", | |
| 191 | "change", "make", "refactor", "write", "remove", "delete", "rename", "address", "try", | |
| 192 | "revise", "go", "build", "resolve", "move", "use", "rewrite", "split", "clean", "finish", | |
| 193 | "apply", "drop", "bump", "upgrade", "rebase", "merge", "test", | |
| 194 | ]; | |
| 195 | /// Words that start a question. | |
| 196 | const QUESTION_WORDS: &[&str] = &[ | |
| 197 | "what", "why", "how", "where", "when", "which", "who", "whose", "whom", "is", "are", "was", | |
| 198 | "were", "does", "did", "explain", "tell", "describe", "summarize", "summarise", "clarify", | |
| 199 | ]; | |
| 200 | /// Politeness before the request itself. | |
| 201 | const PREFIXES: &[&[&str]] = &[ | |
| 202 | &["please"], | |
| 203 | &["pls"], | |
| 204 | &["hey"], | |
| 205 | &["hi"], | |
| 206 | &["can", "you"], | |
| 207 | &["could", "you"], | |
| 208 | &["would", "you"], | |
| 209 | &["will", "you"], | |
| 210 | ]; | |
| 211 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 212 | /// What a comment mentioning `@g1t` asks for, read from what it says |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 213 | /// after the mention (or before, when nothing follows it). |
| 214 | pub(crate) fn intent(body: &str) -> Intent { | |
| 215 | let spoken = spoken(body); | |
| 216 | let found = mentions_in(&spoken); | |
| 217 | let request = match (found.first(), found.last()) { | |
| 218 | (Some(&(start, _)), Some(&(_, end))) => { | |
| 219 | let after = spoken[end..].trim(); | |
| 220 | if after.is_empty() { spoken[..start].trim().to_owned() } else { after.to_owned() } | |
| 221 | } | |
| 222 | _ => spoken.trim().to_owned(), | |
| 223 | }; | |
| 224 | // Other mentions of the agent in the request say nothing about it. | |
| 225 | let mut request = request.to_lowercase(); | |
| 226 | for (start, end) in mentions_in(&request).into_iter().rev() { | |
| 227 | request.replace_range(start..end, " "); | |
| 228 | } | |
| 229 | let words: Vec<&str> = request | |
| 230 | .split(|c: char| !(c.is_alphanumeric() || c == '\'')) | |
| 231 | .filter(|word| !word.is_empty()) | |
| 232 | .collect(); | |
| 233 | let mut rest: &[&str] = &words; | |
| 234 | 'strip: loop { | |
| 235 | for prefix in PREFIXES { | |
| 236 | if rest.starts_with(prefix) { | |
| 237 | rest = &rest[prefix.len()..]; | |
| 238 | continue 'strip; | |
| 239 | } | |
| 240 | } | |
| 241 | break; | |
| 242 | } | |
| 243 | if rest.iter().take(4).any(|word| *word == "review" || *word == "re-review") { | |
| 244 | return Intent::Review; | |
| 245 | } | |
| 246 | let first = rest.first().copied().unwrap_or_default(); | |
| 247 | if WORK_VERBS.contains(&first) { | |
| 248 | return Intent::Work; | |
| 249 | } | |
| 250 | // The question is the sentence the mention starts. | |
| 251 | let sentence = request.split_inclusive(['.', '!', '\n']).next().unwrap_or_default(); | |
| 252 | if QUESTION_WORDS.contains(&first) | |
| 253 | || words.first().is_some_and(|word| QUESTION_WORDS.contains(word)) | |
| 254 | || sentence.trim_end().ends_with('?') | |
| 255 | || request.trim_end().ends_with('?') | |
| 256 | { | |
| 257 | return Intent::Question; | |
| 258 | } | |
| 259 | Intent::Work | |
| 260 | } | |
| 261 | ||
| 262 | // --- Contracts ------------------------------------------------------------- | |
| 263 | // Mirrored in TypeScript by `packages/contracts/src/mentions.ts`. | |
| 264 | ||
| 265 | /// `take_mention`: claims the mention a comment made, once. Null when the | |
| 266 | /// comment made none, or it was already taken. Returns `Option<MentionJob>`. | |
| 267 | #[derive(Debug, Deserialize)] | |
| 268 | #[serde(rename_all = "camelCase")] | |
| 269 | pub(crate) struct TakeMentionArgs { | |
| 270 | comment_id: String, | |
| 271 | } | |
| 272 | ||
| 273 | /// What the runner needs to act on a mention. | |
| 274 | #[derive(Debug, Serialize)] | |
| 275 | #[serde(rename_all = "camelCase")] | |
| 276 | pub(crate) struct MentionJob { | |
| 277 | comment_id: String, | |
| 278 | /// Who wrote it, with the memberships they had then. | |
| 279 | actor: User, | |
| 280 | repo: RepoPath, | |
| 281 | number: u32, | |
| 282 | /// The comment as written. | |
| 283 | body: String, | |
| 284 | intent: Intent, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 285 | /// Whether they may put agents to work in the repository: the Write |
| 286 | /// role or higher (the name is from when that meant a member). | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 287 | member: bool, |
| 288 | default_branch: String, | |
| 289 | /// Set when the comment is on an issue: whether it is still open. | |
| 290 | issue_open: Option<bool>, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 291 | /// On an issue: the pull request g1t is already working on for it, if any. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 292 | working_pull: Option<u32>, |
| 293 | /// Set when the comment is on a pull request. | |
| 294 | pull: Option<MentionPull>, | |
| 295 | } | |
| 296 | ||
| 297 | #[derive(Debug, Serialize)] | |
| 298 | #[serde(rename_all = "camelCase")] | |
| 299 | pub(crate) struct MentionPull { | |
| 300 | id: String, | |
| 301 | status: PullStatus, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 302 | /// Made by g1t, which sees it through. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 303 | agent_authored: bool, |
| 304 | /// Where its change is: its fork, or the repository itself. | |
| 305 | source: RepoPath, | |
| 306 | /// The head is a branch of the repository itself, not a fork. | |
| 307 | in_repo: bool, | |
| 308 | branch: Option<String>, | |
| 309 | head_commit: Option<String>, | |
| 310 | files: Vec<String>, | |
| 311 | } | |
| 312 | ||
| 313 | /// `mention_revision`: sends the author of a g1t pull request back to | |
| 314 | /// address a comment that mentioned it. Returns `Outcome<LifecycleJob>`. | |
| 315 | #[derive(Debug, Deserialize)] | |
| 316 | #[serde(rename_all = "camelCase")] | |
| 317 | pub(crate) struct MentionRevisionArgs { | |
| 318 | comment_id: String, | |
| 319 | } | |
| 320 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 321 | /// `reply_mention`: g1t's answer to a mention, in its thread. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 322 | /// Returns `bool`: false when there was no such mention. |
| 323 | #[derive(Debug, Deserialize)] | |
| 324 | #[serde(rename_all = "camelCase")] | |
| 325 | pub(crate) struct ReplyMentionArgs { | |
| 326 | comment_id: String, | |
| 327 | body: String, | |
| 328 | } | |
| 329 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 330 | /// A repository's rules for putting g1t to work by itself. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 331 | #[derive(Debug, Default, Serialize, Deserialize)] |
| 332 | #[serde(rename_all = "camelCase")] | |
| 333 | pub(crate) struct AgentRules { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 334 | /// When an issue is given this label, g1t takes it. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 335 | label: Option<String>, |
| 336 | updated_by: Option<String>, | |
| 337 | updated_at: Option<String>, | |
| 338 | } | |
| 339 | ||
| 340 | /// `get_agent_rules`. Returns `Outcome<AgentRules>`. | |
| 341 | #[derive(Debug, Deserialize)] | |
| 342 | pub(crate) struct GetAgentRulesArgs { | |
| 343 | repo: RepoPath, | |
| 344 | viewer: Viewer, | |
| 345 | } | |
| 346 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 347 | /// `set_agent_rules`: needs the Maintain role. Returns `Outcome<AgentRules>`. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 348 | #[derive(Debug, Deserialize)] |
| 349 | pub(crate) struct SetAgentRulesArgs { | |
| 350 | actor: User, | |
| 351 | repo: RepoPath, | |
| 352 | #[serde(default)] | |
| 353 | label: Option<String>, | |
| 354 | } | |
| 355 | ||
| 356 | #[derive(Deserialize)] | |
| 357 | struct MentionRow { | |
| 358 | comment_id: String, | |
| 359 | repo_id: String, | |
| 360 | number: u32, | |
| 361 | pull_id: Option<String>, | |
| 362 | actor: String, | |
| 363 | body: String, | |
| 364 | intent: String, | |
| 365 | member: u8, | |
| 366 | } | |
| 367 | ||
| 368 | #[derive(Deserialize)] | |
| 369 | struct RulesRow { | |
| 370 | label: Option<String>, | |
| 371 | updated_by: String, | |
| 372 | updated_at: String, | |
| 373 | } | |
| 374 | ||
| 375 | #[derive(Deserialize)] | |
| 376 | struct RevisionsRow { | |
| 377 | revisions: u32, | |
| 378 | } | |
| 379 | ||
| 380 | /// A label as issues store them, or `None` for no rule. | |
| 381 | fn normalize_label(label: Option<&str>) -> std::result::Result<Option<String>, &'static str> { | |
| 382 | let Some(label) = label.map(str::trim).filter(|label| !label.is_empty()) else { | |
| 383 | return Ok(None); | |
| 384 | }; | |
| 385 | if label.chars().count() > MAX_LABEL_CHARS { | |
| 386 | return Err("A label is up to 40 characters."); | |
| 387 | } | |
| 388 | Ok(Some(label.to_lowercase())) | |
| 389 | } | |
| 390 | ||
| 391 | impl Work { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 392 | /// Records a comment's mention of `@g1t`, if it makes one, for |
| 393 | /// the runner to take when it hears of the comment. g1t mentioning | |
| 394 | /// itself is not recorded, so no agent can set another to work. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 395 | pub(crate) async fn note_mention( |
| 396 | &self, | |
| 397 | actor: &User, | |
| 398 | repo: &Repo, | |
| 399 | number: u32, | |
| 400 | comment: &Comment, | |
| 401 | pull_id: Option<&str>, | |
| 402 | ) -> Result<()> { | |
| 403 | if actor.kind == PrincipalKind::Agent | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 404 | || actor.is_system() |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 405 | || actor.id == AGENT_ID |
| 406 | || !mentions_agent(&comment.body) | |
| 407 | { | |
| 408 | return Ok(()); | |
| 409 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 410 | // Whether it may set the agent to work: mentioning spends compute. |
| 411 | let member = actor.verified && access::can(Some(actor), repo, Capability::Run); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 412 | self.db |
| 413 | .prepare( | |
| 414 | "INSERT OR IGNORE INTO agent_mentions | |
| 415 | (comment_id, repo_id, number, pull_id, actor, body, intent, member, created_at) | |
| 416 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", | |
| 417 | ) | |
| 418 | .bind(&[ | |
| 419 | comment.id.as_str().into(), | |
| 420 | repo.id.as_str().into(), | |
| 421 | number.into(), | |
| 422 | pull_id.map_or(JsValue::NULL, JsValue::from), | |
| 423 | serde_json::to_string(actor)?.into(), | |
| 424 | comment.body.as_str().into(), | |
| 425 | intent(&comment.body).as_str().into(), | |
| 426 | u32::from(member).into(), | |
| 427 | comment.created_at.as_str().into(), | |
| 428 | ])? | |
| 429 | .run() | |
| 430 | .await?; | |
| 431 | Ok(()) | |
| 432 | } | |
| 433 | ||
| 434 | pub(crate) async fn take_mention(&self, a: TakeMentionArgs) -> Result<Option<MentionJob>> { | |
| 435 | let Some(row) = self | |
| 436 | .db | |
| 437 | .prepare( | |
| 438 | "UPDATE agent_mentions SET status = 'taken', taken_at = ? | |
| 439 | WHERE comment_id = ? AND status = 'pending' | |
| 440 | RETURNING *", | |
| 441 | ) | |
| 442 | .bind(&[rfc3339(now_ms()).into(), a.comment_id.as_str().into()])? | |
| 443 | .first::<MentionRow>(None) | |
| 444 | .await? | |
| 445 | else { | |
| 446 | return Ok(None); | |
| 447 | }; | |
| 448 | let Ok(actor) = serde_json::from_str::<User>(&row.actor) else { | |
| 449 | return Ok(None); | |
| 450 | }; | |
| 451 | // Where the repository is now, as whoever commented: they could see it. | |
| 452 | let repo: Outcome<Repo> = g1t_kit::call( | |
| 453 | &self.repos, | |
| 454 | "get_by_id", | |
| 455 | &GetByIdArgs { | |
| 456 | id: row.repo_id.clone(), | |
| 457 | viewer: Some(actor.clone()), | |
| 458 | }, | |
| 459 | ) | |
| 460 | .await?; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 461 | let Outcome::Ok(repo) = crate::retired::unless_archived(repo) else { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 462 | return Ok(None); |
| 463 | }; | |
| 464 | let path = RepoPath { | |
| 465 | namespace: repo.namespace.clone(), | |
| 466 | name: repo.name.clone(), | |
| 467 | }; | |
| 468 | let pull = match &row.pull_id { | |
| 469 | Some(id) => self.pull_by_id(id).await?, | |
| 470 | None => None, | |
| 471 | }; | |
| 472 | let issue = match &pull { | |
| 473 | Some(_) => None, | |
| 474 | None => self.issue(&repo.id, row.number).await?, | |
| 475 | }; | |
| 476 | let issue_open = pull.is_none().then(|| issue.as_ref().is_some_and(|issue| issue.state == State::Open)); | |
| 477 | let working_pull = match &issue { | |
| 478 | Some(issue) => self | |
| 479 | .db | |
| 480 | .prepare( | |
| 481 | "SELECT number AS n FROM pulls | |
| 482 | WHERE issue_id = ? AND agent = ? AND status IN ('draft', 'open') | |
| 483 | ORDER BY number DESC LIMIT 1", | |
| 484 | ) | |
| 485 | .bind(&[issue.id.as_str().into(), AGENT_NAME.into()])? | |
| 486 | .first::<NumberRow>(None) | |
| 487 | .await? | |
| 488 | .map(|row| row.n), | |
| 489 | None => None, | |
| 490 | }; | |
| 491 | Ok(Some(MentionJob { | |
| 492 | comment_id: row.comment_id, | |
| 493 | actor, | |
| 494 | number: row.number, | |
| 495 | body: row.body, | |
| 496 | intent: Intent::parse(&row.intent), | |
| 497 | member: row.member != 0, | |
| 498 | default_branch: repo.default_branch.clone(), | |
| 499 | issue_open, | |
| 500 | working_pull, | |
| 501 | pull: pull.map(|pull| MentionPull { | |
| 502 | agent_authored: made_by_g1t(&pull), | |
| 503 | source: pull.fork.clone().unwrap_or_else(|| path.clone()), | |
| 504 | in_repo: pull.fork.is_none(), | |
| 505 | files: pull.files.iter().map(|file| file.path.clone()).collect(), | |
| 506 | id: pull.id, | |
| 507 | status: pull.status, | |
| 508 | branch: pull.branch, | |
| 509 | head_commit: pull.head_commit, | |
| 510 | }), | |
| 511 | repo: path, | |
| 512 | })) | |
| 513 | } | |
| 514 | ||
| 515 | pub(crate) async fn mention_revision( | |
| 516 | &self, | |
| 517 | a: MentionRevisionArgs, | |
| 518 | ) -> Result<Outcome<LifecycleJob>> { | |
| 519 | let Some(row) = self | |
| 520 | .db | |
| 521 | .prepare("SELECT * FROM agent_mentions WHERE comment_id = ? AND status = 'taken'") | |
| 522 | .bind(&[a.comment_id.as_str().into()])? | |
| 523 | .first::<MentionRow>(None) | |
| 524 | .await? | |
| 525 | else { | |
| 526 | return Ok(Outcome::fail(FailureCode::NotFound, "No such mention.")); | |
| 527 | }; | |
| 528 | let actor: User = serde_json::from_str(&row.actor)?; | |
| 529 | let Some(pull) = (match &row.pull_id { | |
| 530 | Some(id) => self.pull_by_id(id).await?, | |
| 531 | None => None, | |
| 532 | }) else { | |
| 533 | return Ok(Outcome::fail(FailureCode::NotFound, "Pull request not found.")); | |
| 534 | }; | |
| 535 | if row.member == 0 || !made_by_g1t(&pull) { | |
| 536 | return Ok(Outcome::fail( | |
| 537 | FailureCode::Forbidden, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 538 | "Only someone with the Write role or higher can send g1t back to a pull request it made.", |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 539 | )); |
| 540 | } | |
| 541 | match pull.status { | |
| 542 | PullStatus::Open => {} | |
| 543 | PullStatus::Draft => { | |
| 544 | return Ok(Outcome::fail( | |
| 545 | FailureCode::Conflict, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 546 | "g1t is still making this change.", |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 547 | )); |
| 548 | } | |
| 549 | status => { | |
| 550 | return Ok(Outcome::fail( | |
| 551 | FailureCode::Conflict, | |
| 552 | format!("This pull request is already {}.", serde_json::to_value(status)?.as_str().unwrap_or("closed")), | |
| 553 | )); | |
| 554 | } | |
| 555 | } | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 556 | let viewer = self.owner_viewer(&pull).await?; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 557 | let repo: Outcome<Repo> = g1t_kit::call( |
| 558 | &self.repos, | |
| 559 | "get_by_id", | |
| 560 | &GetByIdArgs { | |
| 561 | id: pull.repo_id.clone(), | |
| 562 | viewer, | |
| 563 | }, | |
| 564 | ) | |
| 565 | .await?; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 566 | let (Outcome::Ok(repo), Some(source)) = (crate::retired::unless_archived(repo), pull.fork.clone()) else { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 567 | return Ok(Outcome::fail(FailureCode::NotFound, "Pull request not found.")); |
| 568 | }; | |
| 569 | // A person asking outranks a stop and the limit on revisions. | |
| 570 | self.db | |
| 571 | .prepare("UPDATE pulls SET stalled = NULL WHERE id = ?") | |
| 572 | .bind(&[pull.id.as_str().into()])? | |
| 573 | .run() | |
| 574 | .await?; | |
| 575 | if !self.claim(&pull.id, "revision", REVISION_MINUTES, true).await? { | |
| 576 | return Ok(Outcome::fail( | |
| 577 | FailureCode::Conflict, | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 578 | "g1t is already taking a step on this pull request.", |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 579 | )); |
| 580 | } | |
| 581 | let round = self | |
| 582 | .db | |
| 583 | .prepare("SELECT revisions FROM pulls WHERE id = ?") | |
| 584 | .bind(&[pull.id.as_str().into()])? | |
| 585 | .first::<RevisionsRow>(None) | |
| 586 | .await? | |
| 587 | .map_or(1, |row| row.revisions); | |
| 588 | let issue = match pull.issue { | |
| 589 | Some(number) => self.issue(&pull.repo_id, number).await?, | |
| 590 | None => None, | |
| 591 | }; | |
| 592 | self.note( | |
| 593 | &pull.repo_id, | |
| 594 | pull.number, | |
| 595 | (POLICY_ACTOR_ID, POLICY_ACTOR_NAME), | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 596 | &format!("sent g1t back to address {}'s comment", actor.username), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 597 | ) |
| 598 | .await?; | |
| 599 | Ok(Outcome::Ok(LifecycleJob { | |
| 600 | pull_id: pull.id, | |
| 601 | repo: RepoPath { | |
| 602 | namespace: repo.namespace, | |
| 603 | name: repo.name, | |
| 604 | }, | |
| 605 | number: pull.number, | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 606 | author: pull.requested_by.unwrap_or(pull.author), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 607 | source, |
| 608 | branch: None, | |
| 609 | default_branch: repo.default_branch, | |
| 610 | title: pull.title, | |
| 611 | description: pull.body.unwrap_or_default(), | |
| 612 | issue, | |
| 613 | feedback: format!( | |
| 614 | "{} mentioned you in a comment on this pull request:\n\n{}\n\nThis is a change a person asked for. Make it.", | |
| 615 | actor.username, | |
| 616 | row.body.trim() | |
| 617 | ), | |
| 618 | round, | |
| 619 | })) | |
| 620 | } | |
| 621 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 622 | /// Says something in a mention's thread as g1t, once per mention. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 623 | pub(crate) async fn reply_mention(&self, a: ReplyMentionArgs) -> Result<bool> { |
| 624 | let body: String = a.body.trim().chars().take(MAX_REPLY_CHARS).collect(); | |
| 625 | if body.is_empty() { | |
| 626 | return Ok(false); | |
| 627 | } | |
| 628 | let Some(row) = self | |
| 629 | .db | |
| 630 | .prepare( | |
| 631 | "UPDATE agent_mentions SET status = 'replied', outcome = ? | |
| 632 | WHERE comment_id = ? AND status = 'taken' | |
| 633 | RETURNING *", | |
| 634 | ) | |
| 635 | .bind(&[body.as_str().into(), a.comment_id.as_str().into()])? | |
| 636 | .first::<MentionRow>(None) | |
| 637 | .await? | |
| 638 | else { | |
| 639 | return Ok(false); | |
| 640 | }; | |
| 641 | let now = now_ms(); | |
| 642 | let id = new_id("cmt", now); | |
| 643 | let at = rfc3339(now); | |
| 644 | let table = if row.pull_id.is_some() { "pulls" } else { "issues" }; | |
| 645 | self.db | |
| 646 | .batch(vec![ | |
| 647 | self.db | |
| 648 | .prepare( | |
| 649 | "INSERT INTO comments | |
| 650 | (id, repo_id, number, author_id, author_name, body, created_at) | |
| 651 | VALUES (?, ?, ?, ?, ?, ?, ?)", | |
| 652 | ) | |
| 653 | .bind(&[ | |
| 654 | id.as_str().into(), | |
| 655 | row.repo_id.as_str().into(), | |
| 656 | row.number.into(), | |
| 657 | AGENT_ID.into(), | |
| 658 | AGENT_NAME.into(), | |
| 659 | body.as_str().into(), | |
| 660 | at.as_str().into(), | |
| 661 | ])?, | |
| 662 | self.db | |
| 663 | .prepare(format!( | |
| 664 | "UPDATE {table} SET updated_at = ? WHERE repo_id = ? AND number = ?" | |
| 665 | )) | |
| 666 | .bind(&[at.as_str().into(), row.repo_id.as_str().into(), row.number.into()])?, | |
| 667 | ]) | |
| 668 | .await?; | |
| 669 | self.publish_as( | |
| 670 | "comment.created", | |
| 671 | &row.repo_id, | |
| 672 | Some(AGENT_ID.to_owned()), | |
| 673 | CommentCreated { | |
| 674 | comment_id: id, | |
| 675 | repo_id: row.repo_id.clone(), | |
| 676 | number: row.number, | |
| 677 | pull_id: row.pull_id, | |
| 678 | verdict: None, | |
| 679 | }, | |
| 680 | ) | |
| 681 | .await?; | |
| 682 | Ok(true) | |
| 683 | } | |
| 684 | ||
| 685 | // --- The label rule ---------------------------------------------------- | |
| 686 | ||
| 687 | async fn rules(&self, repo_id: &str) -> Result<AgentRules> { | |
| 688 | Ok(self | |
| 689 | .db | |
| 690 | .prepare("SELECT label, updated_by, updated_at FROM agent_rules WHERE repo_id = ?") | |
| 691 | .bind(&[repo_id.into()])? | |
| 692 | .first::<RulesRow>(None) | |
| 693 | .await? | |
| 694 | .map_or_else(AgentRules::default, |row| AgentRules { | |
| 695 | label: row.label, | |
| 696 | updated_by: Some(row.updated_by), | |
| 697 | updated_at: Some(row.updated_at), | |
| 698 | })) | |
| 699 | } | |
| 700 | ||
| 701 | pub(crate) async fn get_agent_rules(&self, a: GetAgentRulesArgs) -> Result<Outcome<AgentRules>> { | |
| 702 | let repo = match self.repo(&a.repo, &a.viewer).await? { | |
| 703 | Outcome::Ok(repo) => repo, | |
| 704 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 705 | }; | |
| 706 | Ok(Outcome::Ok(self.rules(&repo.id).await?)) | |
| 707 | } | |
| 708 | ||
| 709 | pub(crate) async fn set_agent_rules(&self, a: SetAgentRulesArgs) -> Result<Outcome<AgentRules>> { | |
| 710 | let repo = match self.repo(&a.repo, &Some(a.actor.clone())).await? { | |
| 711 | Outcome::Ok(repo) => repo, | |
| 712 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 713 | }; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 714 | if let Outcome::Fail(failure) = crate::retired::writable(&repo) { |
| 715 | return Ok(Outcome::Fail(failure)); | |
| 716 | } | |
| 717 | if !a.actor.verified { | |
| 718 | return Ok(Outcome::fail(FailureCode::Forbidden, crate::UNVERIFIED)); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 719 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 720 | if let Outcome::Fail(failure) = crate::allowed(Some(&a.actor), &repo, Capability::ManageSettings) { |
| 721 | return Ok(Outcome::Fail(failure)); | |
| 722 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 723 | let label = match normalize_label(a.label.as_deref()) { |
| 724 | Ok(label) => label, | |
| 725 | Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)), | |
| 726 | }; | |
| 727 | let rules = AgentRules { | |
| 728 | label, | |
| 729 | updated_by: Some(a.actor.username.clone()), | |
| 730 | updated_at: Some(rfc3339(now_ms())), | |
| 731 | }; | |
| 732 | self.db | |
| 733 | .prepare( | |
| 734 | "INSERT INTO agent_rules (repo_id, label, updated_by, updated_at) | |
| 735 | VALUES (?, ?, ?, ?) | |
| 736 | ON CONFLICT (repo_id) DO UPDATE SET | |
| 737 | label = excluded.label, | |
| 738 | updated_by = excluded.updated_by, | |
| 739 | updated_at = excluded.updated_at", | |
| 740 | ) | |
| 741 | .bind(&[ | |
| 742 | repo.id.as_str().into(), | |
| 743 | rules.label.as_deref().map_or(JsValue::NULL, JsValue::from), | |
| 744 | rules.updated_by.as_deref().unwrap_or_default().into(), | |
| 745 | rules.updated_at.as_deref().unwrap_or_default().into(), | |
| 746 | ])? | |
| 747 | .run() | |
| 748 | .await?; | |
| 749 | Ok(Outcome::Ok(rules)) | |
| 750 | } | |
| 751 | ||
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 752 | /// Queues an issue for g1t when it has just been given the label |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 753 | /// the repository's rule names, by someone who may run agents in it. |
| 754 | /// The runner starts queued issues as there is room, as it does a | |
| 755 | /// plan's. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 756 | pub(crate) async fn apply_label_rule(&self, actor: &User, issue: &Issue, before: &[String]) -> Result<()> { |
| 757 | if issue.state != State::Open || issue.queued || issue.agent.is_some() { | |
| 758 | return Ok(()); | |
| 759 | } | |
| 760 | let Some(label) = self.rules(&issue.repo_id).await?.label else { | |
| 761 | return Ok(()); | |
| 762 | }; | |
| 763 | if !issue.labels.contains(&label) || before.contains(&label) { | |
| 764 | return Ok(()); | |
| 765 | } | |
| 766 | let path: Option<RepoPath> = g1t_kit::call( | |
| 767 | &self.repos, | |
| 768 | "path_by_id", | |
| 769 | &PathByIdArgs { | |
| 770 | id: issue.repo_id.clone(), | |
| 771 | }, | |
| 772 | ) | |
| 773 | .await?; | |
| 774 | let Some(path) = path else { | |
| 775 | return Ok(()); | |
| 776 | }; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 777 | // Running needs Write, which public alone never gives, so whether |
| 778 | // the repository is public does not matter here. | |
| 779 | let target = access::RepoRef { id: &issue.repo_id, namespace: &path.namespace, private: true }; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 780 | if !actor.verified |
| 781 | || actor.kind == PrincipalKind::Agent | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 782 | || !access::can(Some(actor), target, Capability::Run) |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 783 | { |
| 784 | return Ok(()); | |
| 785 | } | |
| 786 | let queued = self | |
| 787 | .db | |
| 788 | .prepare( | |
| 789 | "UPDATE issues SET queued_by = ? WHERE id = ? AND queued_by IS NULL | |
| 790 | RETURNING id AS value", | |
| 791 | ) | |
| 792 | .bind(&[serde_json::to_string(actor)?.into(), issue.id.as_str().into()])? | |
| 793 | .first::<ValueRow>(None) | |
| 794 | .await?; | |
| 795 | if queued.is_some() { | |
| 796 | self.note( | |
| 797 | &issue.repo_id, | |
| 798 | issue.number, | |
| 799 | (POLICY_ACTOR_ID, POLICY_ACTOR_NAME), | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 800 | &format!("queued this for g1t, because it was labelled {label}"), |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 801 | ) |
| 802 | .await?; | |
| 803 | } | |
| 804 | Ok(()) | |
| 805 | } | |
| 806 | } | |
| 807 | ||
| 808 | #[cfg(test)] | |
| 809 | mod tests { | |
| 810 | use super::*; | |
| 811 | ||
| 812 | #[test] | |
| 813 | fn a_mention_is_found_whatever_its_case() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 814 | assert!(mentions_agent("@g1t take this")); |
| 815 | assert!(mentions_agent("@G1T take this")); | |
| 816 | assert!(mentions_agent("Thanks, @g1t.")); | |
| 817 | assert!(mentions_agent("(@g1t) and")); | |
| 818 | assert!(mentions_agent("cc @g1t, please")); | |
| 819 | assert!(mentions_agent("first line\n@g1t")); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 820 | } |
| 821 | ||
| 822 | #[test] | |
| 823 | fn code_does_not_mention_anyone() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 824 | assert!(!mentions_agent("Type `@g1t take this` to hand it over.")); |
| 825 | assert!(!mentions_agent("Use ``@g1t `x` `` like so.")); | |
| 826 | assert!(!mentions_agent("```\n@g1t take this\n```")); | |
| 827 | assert!(!mentions_agent("~~~md\n@g1t\n~~~")); | |
| 828 | assert!(!mentions_agent("````\n```\n@g1t\n```\n````")); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 829 | // Outside the code, it still counts. |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 830 | assert!(mentions_agent("`code` then @g1t fix it")); |
| 831 | assert!(mentions_agent("```\nx\n```\n@g1t fix it")); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 832 | // A backtick that opens nothing is text. |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 833 | assert!(mentions_agent("a ` b @g1t")); |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 834 | } |
| 835 | ||
| 836 | #[test] | |
| 837 | fn quoting_a_mention_does_not_repeat_it() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 838 | assert!(!mentions_agent("> @g1t take this\n\nI don't think we should.")); |
| 839 | assert!(!mentions_agent(" > > @g1t")); | |
| 840 | assert!(mentions_agent("> earlier\n\n@g1t yes, do it")); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 841 | } |
| 842 | ||
| 843 | #[test] | |
| 844 | fn email_addresses_and_longer_names_are_not_mentions() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 845 | assert!(!mentions_agent("write to ops@g1t.sh")); |
| 846 | assert!(!mentions_agent("mail g1t@users.noreply.g1t.sh")); | |
| 847 | assert!(!mentions_agent("bot@g1t")); | |
| 848 | assert!(!mentions_agent("@g1t.dev is the address")); | |
| 849 | assert!(!mentions_agent("@g1t.sh")); | |
| 850 | assert!(!mentions_agent("@g1ts")); | |
| 851 | assert!(!mentions_agent("@g1t2")); | |
| 852 | assert!(!mentions_agent("@g1t-2")); | |
| 853 | assert!(!mentions_agent("@g1t-bot take this")); | |
| 854 | assert!(!mentions_agent("@g1t-agent take this")); | |
| 855 | assert!(!mentions_agent("@g1t_x")); | |
| 856 | assert!(!mentions_agent("https://g1t.sh/@g1t")); | |
| 857 | assert!(!mentions_agent("see https://g1t.sh/g1t/docs")); | |
| 858 | assert!(!mentions_agent("\\@g1t")); | |
| 859 | assert!(!mentions_agent("g1t without the at")); | |
| 860 | assert!(!mentions_agent("import { x } from \"@g1t/contracts\";")); | |
| 861 | assert!(!mentions_agent("npm i @g1t/contracts")); | |
| 862 | assert!(!mentions_agent("@@g1t")); | |
| 863 | assert!(!mentions_agent("name@g1t: hi")); | |
| 864 | } | |
| 865 | ||
| 866 | #[test] | |
| 867 | fn a_mention_ends_at_punctuation() { | |
| 868 | assert!(mentions_agent("@g1t: take this")); | |
| 869 | assert!(mentions_agent("@g1t! fix it")); | |
| 870 | assert!(mentions_agent("ok @g1t?")); | |
| 871 | assert!(mentions_agent("\"@g1t\" take this")); | |
| 872 | assert!(mentions_agent("**@g1t** take this")); | |
| 873 | assert!(mentions_agent("@g1t.\nThanks")); | |
| 874 | assert_eq!(mentions_in("@g1t and @g1t-bot and @g1t"), vec![(0, 4), (22, 26)]); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 875 | } |
| 876 | ||
| 877 | #[test] | |
| 878 | fn non_ascii_text_around_a_mention_is_fine() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 879 | assert!(mentions_agent("é @g1t ü")); |
| 880 | assert!(!mentions_agent("é@g1t")); | |
| 881 | assert_eq!(mentions_in("ü @g1t"), vec![(3, 3 + HANDLE.len())]); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 882 | } |
| 883 | ||
| 884 | #[test] | |
| 885 | fn a_request_is_work() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 886 | assert_eq!(intent("@g1t take this"), Intent::Work); |
| 887 | assert_eq!(intent("@g1t"), Intent::Work); | |
| 888 | assert_eq!(intent("@g1t please fix the typo in the README"), Intent::Work); | |
| 889 | assert_eq!(intent("@g1t can you add tests for this?"), Intent::Work); | |
| 890 | assert_eq!(intent("Looks close. @g1t rename `foo` to `bar`."), Intent::Work); | |
| 891 | assert_eq!(intent("@G1T Handle the empty case too"), Intent::Work); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 892 | } |
| 893 | ||
| 894 | #[test] | |
| 895 | fn a_question_is_answered() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 896 | assert_eq!(intent("@g1t why does this fail on Windows?"), Intent::Question); |
| 897 | assert_eq!(intent("@g1t how is the cache invalidated"), Intent::Question); | |
| 898 | assert_eq!(intent("@g1t can you explain the retry logic"), Intent::Question); | |
| 899 | assert_eq!(intent("@g1t is this safe to merge as it is?"), Intent::Question); | |
| 900 | assert_eq!(intent("@g1t the parser or the lexer?"), Intent::Question); | |
| 901 | assert_eq!(intent("What does this do, @g1t?"), Intent::Question); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 902 | } |
| 903 | ||
| 904 | #[test] | |
| 905 | fn a_review_is_a_review() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 906 | assert_eq!(intent("@g1t review this"), Intent::Review); |
| 907 | assert_eq!(intent("@g1t please review"), Intent::Review); | |
| 908 | assert_eq!(intent("@g1t could you review the migration?"), Intent::Review); | |
| 909 | assert_eq!(intent("@g1t re-review"), Intent::Review); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 910 | } |
| 911 | ||
| 912 | #[test] | |
| 913 | fn quoted_and_code_text_does_not_change_the_intent() { | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 914 | assert_eq!(intent("> why?\n\n@g1t fix it"), Intent::Work); |
| 915 | assert_eq!(intent("@g1t fix `why?`"), Intent::Work); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 916 | } |
| 917 | ||
| 918 | #[test] | |
| 919 | fn labels_are_stored_as_issues_store_them() { | |
| 920 | assert_eq!(normalize_label(Some(" Agent ")), Ok(Some("agent".to_owned()))); | |
| 921 | assert_eq!(normalize_label(Some(" ")), Ok(None)); | |
| 922 | assert_eq!(normalize_label(None), Ok(None)); | |
| 923 | assert!(normalize_label(Some(&"x".repeat(41))).is_err()); | |
| 924 | } | |
| 925 | } |