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.
|
||||
const MAX_PATCH_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Who delivery commits as. Not a real mailbox — it identifies the pipeline in
|
||||
/// `git log` and keeps commits attributable to the platform rather than to
|
||||
/// whichever agent last touched the checkout's git config.
|
||||
const COMMIT_IDENTITY_NAME: &str = "ClawMates delivery";
|
||||
const COMMIT_IDENTITY_EMAIL: &str = "[email protected]";
|
||||
/// Who delivery commits as.
|
||||
///
|
||||
/// The operator's identity by default, so pushed commits associate with their
|
||||
/// forge account the way their own commits do. Overridable per deployment via
|
||||
/// `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
|
||||
/// 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()),
|
||||
];
|
||||
full.extend_from_slice(args);
|
||||
let (name, email) = commit_identity();
|
||||
let out = tokio::process::Command::new("git")
|
||||
.args(&full)
|
||||
// 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
|
||||
// committer as the pipeline — which is the truth. The agents' own
|
||||
// commits keep whatever identity they set.
|
||||
.env("GIT_AUTHOR_NAME", COMMIT_IDENTITY_NAME)
|
||||
.env("GIT_AUTHOR_EMAIL", COMMIT_IDENTITY_EMAIL)
|
||||
.env("GIT_COMMITTER_NAME", COMMIT_IDENTITY_NAME)
|
||||
.env("GIT_COMMITTER_EMAIL", COMMIT_IDENTITY_EMAIL)
|
||||
.env("GIT_AUTHOR_NAME", &name)
|
||||
.env("GIT_AUTHOR_EMAIL", &email)
|
||||
.env("GIT_COMMITTER_NAME", &name)
|
||||
.env("GIT_COMMITTER_EMAIL", &email)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("spawn git: {e}"))?;
|
||||
@@ -502,7 +525,17 @@ pub async fn commit_phase_work(
|
||||
.unwrap_or_default();
|
||||
if !staged.trim().is_empty() {
|
||||
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 {
|
||||
format!("pass {}", iteration + 1)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user