| 114 | 114 | | never in a URL. Never print it or send it anywhere else.\n\ |
| 115 | 115 | | - Follow the work-on-issue skill. Check the work context and the sessions on the issue before you start, and claim only what you take. \ |
| 116 | 116 | | When work splits into independent parts, you may give a part to a copy of yourself; each is recorded on g1t as a subagent and sees the others.\n\ |
| 117 | | − | - Put the trailer `G1t-Session: <the session id in your first message>` on every commit, so g1t ties the commits to this session.\n\ |
| 117 | + | - Put the trailer `G1t-Session: <the session id in your first message>` on every commit, so g1t ties the commits to this session. \ |
| 118 | + | The setup makes your commits the person's who started you: never set another author or committer.\n\ |
| 118 | 119 | | - Work on a branch whose name starts with the issue number, and open a draft pull request early. Never push to the default branch, \ |
| 119 | 120 | | except a repository's first commit when it has none yet: there is nothing to open a pull request against, so the first version goes \ |
| 120 | 121 | | straight to it.\n\ |
| ⋯ |
| 309 | 310 | | pub session_id: Option<&'a str>, |
| 310 | 311 | | /// For a session g1t starts: how to reach the repository. |
| 311 | 312 | | pub managed: bool, |
| 313 | + | /// For a session g1t starts: who its commits are by, the person who |
| 314 | + | /// started it (name, address), so it never sets an identity itself. |
| 315 | + | pub author: Option<(&'a str, &'a str)>, |
| 312 | 316 | | } |
| 313 | 317 | | |
| 314 | 318 | | fn cut(text: &str, max: usize) -> String { |
| ⋯ |
| 362 | 366 | | text.push_str(match (k.mode, &k.pull) { |
| 363 | 367 | | (AgentMode::Plan, _) => "Plan it: work out how it should be done and post the plan as a comment on the issue. Change no code and open no pull request.", |
| 364 | 368 | | (AgentMode::Implement, Some(_)) => "Push the change to the pull request's branch, get its checks passing, and keep it a draft until it is ready.", |
| 365 | | − | (AgentMode::Implement, None) => "Implement it: make the change on a branch, open a draft pull request for the issue early, get its checks passing, and mark it ready.", |
| 369 | + | (AgentMode::Implement, None) => { |
| 370 | + | "Implement it: make the change on a branch, open a draft pull request for the issue early, get its checks passing, and mark it ready. \ |
| 371 | + | If the repository has no commits yet, there is nothing to open a pull request against: \ |
| 372 | + | commit the first version straight to the default branch and push it there. That push is asked of you." |
| 373 | + | } |
| 366 | 374 | | (AgentMode::Review, _) => "Review it: read the issue's pull request and its checks, and review it on g1t with what should change, if anything.", |
| 367 | 375 | | }); |
| 368 | 376 | | text.push('\n'); |
| ⋯ |
| 370 | 378 | | text.push_str(&format!("\nAlso: {}\n", cut(instruction, MAX_INSTRUCTION_CHARS))); |
| 371 | 379 | | } |
| 372 | 380 | | if k.managed { |
| 373 | | − | text.push_str(&format!("\nSet up first, as one command:\n\n```sh\n{}\n```\n", setup_command(site, &repo, &k.repo.name, k.session_id))); |
| 381 | + | text.push_str(&format!("\nSet up first, as one command:\n\n```sh\n{}\n```\n", setup_command(site, &repo, &k.repo.name, k.session_id, k.author))); |
| 374 | 382 | | text.push_str(&format!( |
| 375 | 383 | | "\nIf /usr/local/bin cannot be written, install without G1T_INSTALL_DIR and run ~/.local/bin/g1t. Then work in /workspace/{}.\n", |
| 376 | 384 | | k.repo.name |
| ⋯ |
| 386 | 394 | | /// What a sandbox runs before it works: the g1t CLI from the site, git |
| 387 | 395 | | /// sending `G1T_TOKEN` to the site in a header (the vault swaps the |
| 388 | 396 | | /// placeholder there), the clone, and the clone's g1t session where the |
| 389 | | − | /// CLI keeps it, so claims and notes are this session's. |
| 390 | | − | pub fn setup_command(site: &str, repo: &str, name: &str, session_id: Option<&str>) -> String { |
| 397 | + | /// CLI keeps it, so claims and notes are this session's. Commits are by |
| 398 | + | /// the person who started it. |
| 399 | + | pub fn setup_command(site: &str, repo: &str, name: &str, session_id: Option<&str>, author: Option<(&str, &str)>) -> String { |
| 391 | 400 | | let site = site.trim_end_matches('/'); |
| 392 | 401 | | let mut steps = vec![ |
| 393 | 402 | | format!("curl -fsSL {site}/install | G1T_INSTALL_DIR=/usr/local/bin sh"), |
| 394 | 403 | | format!("git config --global http.{site}/.extraHeader \"Authorization: Bearer $G1T_TOKEN\""), |
| 395 | | − | format!("git clone {site}/{repo}.git /workspace/{name}"), |
| 396 | 404 | | ]; |
| 405 | + | if let Some((author, email)) = author { |
| 406 | + | steps.push(format!("git config --global user.name {}", shell_quote(author))); |
| 407 | + | steps.push(format!("git config --global user.email {}", shell_quote(email))); |
| 408 | + | } |
| 409 | + | steps.push(format!("git clone {site}/{repo}.git /workspace/{name}")); |
| 397 | 410 | | if let Some(id) = session_id { |
| 398 | 411 | | steps.push(format!("mkdir -p /workspace/{name}/.git/g1t && echo {id} > /workspace/{name}/.git/g1t/session")); |
| 399 | 412 | | } |
| ⋯ |
| 980 | 993 | | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), |
| 981 | 994 | | }; |
| 982 | 995 | | let pull_number = issue.pull.as_ref().map(|(number, _, _)| *number); |
| 996 | + | let author_email = g1t_contracts::accounts::noreply_address(&actor.id, &actor.username); |
| 983 | 997 | | let kickoff_of = |session_id: Option<&str>, managed: bool| { |
| 984 | 998 | | kickoff(&Kickoff { |
| 985 | 999 | | site: &self.site_url, |
| ⋯ |
| 993 | 1007 | | instruction: a.instruction.as_deref(), |
| 994 | 1008 | | session_id, |
| 995 | 1009 | | managed, |
| 1010 | + | author: managed.then_some((actor.username.as_str(), author_email.as_str())), |
| 996 | 1011 | | }) |
| 997 | 1012 | | }; |
| 998 | 1013 | | // The hand-off: nothing starts here. |
| ⋯ |
| 1580 | 1595 | | instruction: Some("Keep it short."), |
| 1581 | 1596 | | session_id: Some("ses_1"), |
| 1582 | 1597 | | managed: true, |
| 1598 | + | author: Some(("ada", "6c1d0efg+ada@users.noreply.g1t.sh")), |
| 1583 | 1599 | | }); |
| 1584 | 1600 | | for text in [session.to_string(), prompt.clone()] { |
| 1585 | 1601 | | assert!(!text.contains("g1t_secret")); |
| ⋯ |
| 1590 | 1606 | | assert!(prompt.contains("curl -fsSL https://g1t.sh/install | G1T_INSTALL_DIR=/usr/local/bin sh")); |
| 1591 | 1607 | | assert!(prompt.contains("git config --global http.https://g1t.sh/.extraHeader \"Authorization: Bearer $G1T_TOKEN\"")); |
| 1592 | 1608 | | assert!(prompt.contains("git clone https://g1t.sh/acme/web.git /workspace/web")); |
| 1609 | + | assert!(prompt.contains("git config --global user.name 'ada'")); |
| 1610 | + | assert!(prompt.contains("git config --global user.email '6c1d0efg+ada@users.noreply.g1t.sh'")); |
| 1611 | + | assert!(prompt.contains("commit the first version straight to the default branch and push it there"), "an empty repository's first push is asked for"); |
| 1593 | 1612 | | assert!(prompt.contains("echo ses_1 > /workspace/web/.git/g1t/session")); |
| 1594 | 1613 | | assert!(prompt.contains("G1t-Session: ses_1")); |
| 1595 | 1614 | | let environment = environment_body("g1t-acme-con_1", &hosts); |
| ⋯ |
| 1612 | 1631 | | instruction: None, |
| 1613 | 1632 | | session_id: None, |
| 1614 | 1633 | | managed: false, |
| 1634 | + | author: None, |
| 1615 | 1635 | | }) |
| 1616 | 1636 | | }; |
| 1617 | 1637 | | assert!(prompt(AgentMode::Plan).contains("Change no code")); |