feat(missions): commit as the operator, overridable per deployment
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped

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:
Omar Sobh
2026-08-02 16:32:02 -07:00
co-authored by Claude Opus 5
parent 1a979f500f
commit ddab8e35f5
2 changed files with 46 additions and 13 deletions
+43 -10
View File
@@ -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 {