feat(missions): commit as the operator, overridable per deployment
Delivery commits now carry "Omar Sobh <[email protected]>" by default, so pushed branches associate with the operator's forge account the way their own commits do. CLAWMATES_COMMIT_NAME / CLAWMATES_COMMIT_EMAIL override it — a shared instance wants a bot identity, not a person's. This is attribution, not the fix. What made 019fc450's phase fail was the *absence* of any identity: the server container has none of its own, so git commit exits 128 regardless of which name would have been used. That was fixed in 25d9805; this only changes the value. The push credential is GITEA_TOKEN throughout and is untouched by any of it. Since the author line now names a person, the commit body says plainly that agents authored the work — otherwise autonomous commits would be indistinguishable from hand-written ones in git log. Also fixes 13 stray spaces that a string continuation had baked into every message body. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1a979f500f
commit
ddab8e35f5
@@ -67,11 +67,33 @@ const EXCLUDED_PATHS: &[&str] = &[
|
|||||||
/// operator needs to see to work out what happened.
|
/// operator needs to see to work out what happened.
|
||||||
const MAX_PATCH_BYTES: usize = 4 * 1024 * 1024;
|
const MAX_PATCH_BYTES: usize = 4 * 1024 * 1024;
|
||||||
|
|
||||||
/// Who delivery commits as. Not a real mailbox — it identifies the pipeline in
|
/// Who delivery commits as.
|
||||||
/// `git log` and keeps commits attributable to the platform rather than to
|
///
|
||||||
/// whichever agent last touched the checkout's git config.
|
/// The operator's identity by default, so pushed commits associate with their
|
||||||
const COMMIT_IDENTITY_NAME: &str = "ClawMates delivery";
|
/// forge account the way their own commits do. Overridable per deployment via
|
||||||
const COMMIT_IDENTITY_EMAIL: &str = "[email protected]";
|
/// `CLAWMATES_COMMIT_NAME` / `CLAWMATES_COMMIT_EMAIL` — a shared instance
|
||||||
|
/// wants a bot identity here, not a person's.
|
||||||
|
///
|
||||||
|
/// What matters for correctness is only that *some* identity is always set:
|
||||||
|
/// the server container has none of its own, so `git commit` fails outright
|
||||||
|
/// without this. The particular value is attribution, not function.
|
||||||
|
///
|
||||||
|
/// Attribution alone, to be clear — the push credential is `GITEA_TOKEN` and
|
||||||
|
/// is unaffected by any of this.
|
||||||
|
const DEFAULT_COMMIT_NAME: &str = "Omar Sobh";
|
||||||
|
const DEFAULT_COMMIT_EMAIL: &str = "[email protected]";
|
||||||
|
|
||||||
|
fn commit_identity() -> (String, String) {
|
||||||
|
let name = std::env::var("CLAWMATES_COMMIT_NAME")
|
||||||
|
.ok()
|
||||||
|
.filter(|v| !v.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| DEFAULT_COMMIT_NAME.to_string());
|
||||||
|
let email = std::env::var("CLAWMATES_COMMIT_EMAIL")
|
||||||
|
.ok()
|
||||||
|
.filter(|v| !v.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| DEFAULT_COMMIT_EMAIL.to_string());
|
||||||
|
(name, email)
|
||||||
|
}
|
||||||
|
|
||||||
/// Ceiling on the gate's test run. Long enough for a real suite, short enough
|
/// Ceiling on the gate's test run. Long enough for a real suite, short enough
|
||||||
/// that a hung test does not hold a phase open indefinitely.
|
/// that a hung test does not hold a phase open indefinitely.
|
||||||
@@ -386,6 +408,7 @@ async fn git(repo: &Path, args: &[&str]) -> Result<String, String> {
|
|||||||
Box::leak(format!("safe.directory={repo_s}").into_boxed_str()),
|
Box::leak(format!("safe.directory={repo_s}").into_boxed_str()),
|
||||||
];
|
];
|
||||||
full.extend_from_slice(args);
|
full.extend_from_slice(args);
|
||||||
|
let (name, email) = commit_identity();
|
||||||
let out = tokio::process::Command::new("git")
|
let out = tokio::process::Command::new("git")
|
||||||
.args(&full)
|
.args(&full)
|
||||||
// The server container has no git identity — `git config --global
|
// The server container has no git identity — `git config --global
|
||||||
@@ -404,10 +427,10 @@ async fn git(repo: &Path, args: &[&str]) -> Result<String, String> {
|
|||||||
// needing a leaked string per invocation, and because they name the
|
// needing a leaked string per invocation, and because they name the
|
||||||
// committer as the pipeline — which is the truth. The agents' own
|
// committer as the pipeline — which is the truth. The agents' own
|
||||||
// commits keep whatever identity they set.
|
// commits keep whatever identity they set.
|
||||||
.env("GIT_AUTHOR_NAME", COMMIT_IDENTITY_NAME)
|
.env("GIT_AUTHOR_NAME", &name)
|
||||||
.env("GIT_AUTHOR_EMAIL", COMMIT_IDENTITY_EMAIL)
|
.env("GIT_AUTHOR_EMAIL", &email)
|
||||||
.env("GIT_COMMITTER_NAME", COMMIT_IDENTITY_NAME)
|
.env("GIT_COMMITTER_NAME", &name)
|
||||||
.env("GIT_COMMITTER_EMAIL", COMMIT_IDENTITY_EMAIL)
|
.env("GIT_COMMITTER_EMAIL", &email)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("spawn git: {e}"))?;
|
.map_err(|e| format!("spawn git: {e}"))?;
|
||||||
@@ -502,7 +525,17 @@ pub async fn commit_phase_work(
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !staged.trim().is_empty() {
|
if !staged.trim().is_empty() {
|
||||||
let message = format!(
|
let message = format!(
|
||||||
"clawmates: {} phase work\n\nMission: {mission_id}\nPhase: {phase_id}\n\n Committed by the ClawMates delivery pipeline from the agents' working tree.",
|
// The trailer is the provenance record. It matters more now that
|
||||||
|
// the author line carries a person's name: without it, autonomous
|
||||||
|
// work would be indistinguishable from hand-written commits in
|
||||||
|
// `git log`. Keep it on any change to this message.
|
||||||
|
"clawmates: {} phase work\n\
|
||||||
|
\n\
|
||||||
|
Mission: {mission_id}\n\
|
||||||
|
Phase: {phase_id}\n\
|
||||||
|
\n\
|
||||||
|
Committed by the ClawMates delivery pipeline from the agents' \
|
||||||
|
working tree. Authored by agents, not by the named committer.",
|
||||||
if iteration > 0 {
|
if iteration > 0 {
|
||||||
format!("pass {}", iteration + 1)
|
format!("pass {}", iteration + 1)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -775,9 +775,9 @@ async fn delivery_commits_under_its_own_identity() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let author = String::from_utf8_lossy(&author.stdout).trim().to_string();
|
let author = String::from_utf8_lossy(&author.stdout).trim().to_string();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
author, "ClawMates delivery <[email protected]>",
|
author, "Omar Sobh <[email protected]>",
|
||||||
"delivery must supply its own identity, not inherit whatever the \
|
"delivery must supply a configured identity, not inherit whatever \
|
||||||
checkout happens to have configured"
|
the checkout happens to have configured"
|
||||||
);
|
);
|
||||||
|
|
||||||
let base_author = Command::new("git")
|
let base_author = Command::new("git")
|
||||||
|
|||||||
Reference in New Issue
Block a user