Repository navigation
feat(commit): let an agent author stand - #412
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb255e95e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
⚡ Grok Review
Reviewed commit: fe98aa7927
❌ Patch is incorrect: 1 finding.
An agent whose git author is a bot still needs --coauthor when only one GIT_AUTHOR variable is set, because the exemption treats that partial pair as no author while git fills the missing half from config and records the bot.
Treat a partial GIT_AUTHOR pair as git does (
crates/devkit-git/src/backend.rs#L363-L384)
| fn author(&self, dir: &Path) -> Result<Option<(String, String)>> { | ||
| // GIT_AUTHOR_* wins, and one half set alone does not mix with | ||
| // config. author.* overrides user.*. | ||
| let from_env = ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"] | ||
| .map(|key| std::env::var(key).ok().filter(|v| !v.trim().is_empty())); | ||
| let [name, email] = match from_env { | ||
| [Some(name), Some(email)] => [name, email], | ||
| [None, None] => ["name", "email"].map(|half| { | ||
| for key in [format!("author.{half}"), format!("user.{half}")] { | ||
| if let Ok(value) = Git::at(dir).args(["config", &key]).output() { | ||
| let value = value.trim().to_string(); | ||
| if !value.is_empty() { | ||
| return value; | ||
| } | ||
| } | ||
| } | ||
| String::new() | ||
| }), | ||
| _ => return Ok(None), | ||
| }; | ||
| Ok((!name.trim().is_empty() && !email.trim().is_empty()).then_some((name, email))) | ||
| } |
There was a problem hiding this comment.
Treat a partial GIT_AUTHOR pair as git does
When an agent caller has coauthors required for agents and no --coauthor, devkit commit exempts the commit only if VersionControl::author returns a GitHub bot, Claude, or Codex address. GitBackend::author returns Ok(None) whenever exactly one of GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL is set, even when the set half is a bot email and the missing half is filled from author.* or user.*. Git itself does not do that: a set GIT_AUTHOR_EMAIL overrides the email, and the unset name comes from GIT_AUTHOR_NAME, then author.name, then user.name (and the reverse for a name-only pair). A harness that exports only GIT_AUTHOR_EMAIL=261865991+grok-by-xai[bot]@users.noreply.github.com, or only GIT_AUTHOR_NAME, while the other half lives in git config, therefore gets "devkit commit needs --coauthor=... (required for agents)" and HEAD stays put, even though the commit git would record is already that agent. The same lookup is what the skill text calls the address the commit would record. Resolve each half the way git does, and return Ok(None) only when that half is still unset or blank.
| fn author(&self, dir: &Path) -> Result<Option<(String, String)>> { | |
| // GIT_AUTHOR_* wins, and one half set alone does not mix with | |
| // config. author.* overrides user.*. | |
| let from_env = ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"] | |
| .map(|key| std::env::var(key).ok().filter(|v| !v.trim().is_empty())); | |
| let [name, email] = match from_env { | |
| [Some(name), Some(email)] => [name, email], | |
| [None, None] => ["name", "email"].map(|half| { | |
| for key in [format!("author.{half}"), format!("user.{half}")] { | |
| if let Ok(value) = Git::at(dir).args(["config", &key]).output() { | |
| let value = value.trim().to_string(); | |
| if !value.is_empty() { | |
| return value; | |
| } | |
| } | |
| } | |
| String::new() | |
| }), | |
| _ => return Ok(None), | |
| }; | |
| Ok((!name.trim().is_empty() && !email.trim().is_empty()).then_some((name, email))) | |
| } | |
| fn author(&self, dir: &Path) -> Result<Option<(String, String)>> { | |
| // Each half is independent, as git records it: a set GIT_AUTHOR_* | |
| // half wins, and an unset half falls through to author.* then user.*. | |
| // A blank half is not an author. | |
| let halves = ["NAME", "EMAIL"].map(|half| { | |
| let from_env = std::env::var(format!("GIT_AUTHOR_{half}")) | |
| .ok() | |
| .filter(|v| !v.trim().is_empty()); | |
| if let Some(value) = from_env { | |
| return value; | |
| } | |
| let key = half.to_ascii_lowercase(); | |
| for config_key in [format!("author.{key}"), format!("user.{key}")] { | |
| if let Ok(value) = Git::at(dir).args(["config", &config_key]).output() { | |
| let value = value.trim().to_string(); | |
| if !value.is_empty() { | |
| return value; | |
| } | |
| } | |
| } | |
| String::new() | |
| }); | |
| let [name, email] = halves; | |
| Ok((!name.trim().is_empty() && !email.trim().is_empty()).then_some((name, email))) | |
| } |
Co-authored-by: Grok 4.7 <261865991+grok-by-xai[bot]@users.noreply.github.com>
An amend keeps the existing author, and author.email overrides user.email. The co-author exemption follows that author, and only the unchanged built-in commit_message. A custom template still needs --coauthor. The bot form is a numeric id, a name, and a terminal [bot]. Claude and Codex noreply addresses stand too. The supplied body is left as written. Co-authored-by: Grok 4.7 <261865991+grok-by-xai[bot]@users.noreply.github.com>
The agent-author exemption treated every required coauthors marking as met. required = "always" still needs --coauthor, including a GitHub bot, Claude or Codex author. The exemption now applies only when the declared requirement is agents. Co-authored-by: Grok 4.7 <261865991+grok-by-xai[bot]@users.noreply.github.com>
author() omitted that author.name and author.email win before user.name and user.email, and that a partial GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL pair is not an author. Co-authored-by: Grok 4.7 <261865991+grok-by-xai[bot]@users.noreply.github.com>
Passing --cleanup=verbatim changed how every devkit commit stores its message: trailing spaces and runs of blank lines were kept instead of tidied as git commit -m does. Only an agent-author spacing test relied on it, and the issue asks for no such guarantee, so devkit keeps git's default cleanup. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
fe98aa7 to
0cc071d
Compare
🤖 I have created a release *beep* *boop* --- ## [0.14.25](v0.14.24...v0.14.25) (2026-10-18) ### Features * **commit:** let an agent author stand ([#412](#412)) ([47f6b61](47f6b61)) * **pr:** refuse a summary repeating template headings ([#414](#414)) ([467affb](467affb)) ### Bug Fixes * **pr:** close agent bypasses of the PR gates ([#417](#417)) ([0f2adf0](0f2adf0)) * report a missing home instead of panicking ([#416](#416)) ([916ab68](916ab68)) * **ui:** read terminal width through console ([#413](#413)) ([ee1e9f6](ee1e9f6)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Closes #402
Agent summary
A project can require a
Co-authored-bytrailer on each agent commit. When the git author is already the agent, that trailer repeats the author line, anddevkit commitstill refuses the commit.devkit committreats that author as meeting acoauthorsrequirement for agents on the unchanged built-incommit_message. The author is the one Git records. A completeGIT_AUTHOR_NAMEandGIT_AUTHOR_EMAILpair wins. One half set is not an author. Otherwiseauthor.namewins beforeuser.name, andauthor.emailwins beforeuser.email. An agent author is a strict GitHub bot address,<digits>+<name>[bot]@users.noreply.github.com, or one of two built-in harness addresses: Claudenoreply@anthropic.comand Codexnoreply@openai.com. No new config key. A person address, a blank half, a human caller,required = "always", a[templates].commit_messageoverride, and a[templates.custom.commit_message]override still need--coauthor. An amend checks the author Git keeps on the existing commit.proofanddone_whenstay as they are.Proof
tests/commit.rsan_agent_author_meets_a_coauthor_requirement_with_no_trailer,an_agent_author_from_user_config_meets_a_coauthor_requirement,a_claude_author_meets_a_coauthor_requirement,a_codex_author_meets_a_coauthor_requirement, and the user-config Claude and Codex twins: an agent caller,coauthorsrequired for agents, no--coauthor. The commit isfix: awith noCo-authored-bytrailer.required = "always"still refuses a bot, Claude, and Codex author.tests/commit.rsan_agent_must_name_its_coauthors_when_the_project_says_so,author_email_set_to_a_person_over_a_bot_user_email_still_needs_a_coauthor, andamending_a_person_commit_with_a_bot_environment_still_needs_a_coauthor: an agent caller with a person author. Each refuses withdevkit commit needs --coauthor=... (required for agents)and leaves HEAD unchanged.plugin/skills/using-devkit/references/templates.mdandcommit.mdname the strict bot form, Claudenoreply@anthropic.com, Codexnoreply@openai.com, the author lookup order, and the template overrides that still need--coauthor.crates/devkit-vcs/src/lib.rsauthor()documents that same lookup.🤖 Generated with Grok 4.7 via Grok Build