Use prompt-based co-author attribution with config override (#11617)

This commit is contained in:
gabec-openai
2026-02-17 12:15:54 -08:00
committed by GitHub
parent 4c4255fcfc
commit 5341ad08f8
6 changed files with 122 additions and 0 deletions

View File

@@ -214,6 +214,9 @@
"child_agents_md": {
"type": "boolean"
},
"codex_git_commit": {
"type": "boolean"
},
"collab": {
"type": "boolean"
},
@@ -1308,6 +1311,10 @@
"default": null,
"description": "Preferred backend for storing CLI auth credentials. file (default): Use a file in the Codex home directory. keyring: Use an OS-specific keyring service. auto: Use the keyring if available, otherwise use a file."
},
"commit_attribution": {
"description": "Optional commit attribution text for commit message co-author trailers.\n\nSet to an empty string to disable automatic commit attribution.",
"type": "string"
},
"compact_prompt": {
"description": "Compact prompt used for history compaction.",
"type": "string"
@@ -1347,6 +1354,9 @@
"child_agents_md": {
"type": "boolean"
},
"codex_git_commit": {
"type": "boolean"
},
"collab": {
"type": "boolean"
},

View File

@@ -17,6 +17,7 @@ use crate::analytics_client::AnalyticsEventsClient;
use crate::analytics_client::AppInvocation;
use crate::analytics_client::build_track_events_context;
use crate::apps::render_apps_section;
use crate::commit_attribution::commit_message_trailer_instruction;
use crate::compact;
use crate::compact::run_inline_auto_compact_task;
use crate::compact::should_use_remote_compact_task;
@@ -2583,6 +2584,13 @@ impl Session {
if turn_context.features.enabled(Feature::Apps) {
items.push(DeveloperInstructions::new(render_apps_section()).into());
}
if turn_context.features.enabled(Feature::CodexGitCommit)
&& let Some(commit_message_instruction) = commit_message_trailer_instruction(
turn_context.config.commit_attribution.as_deref(),
)
{
items.push(DeveloperInstructions::new(commit_message_instruction).into());
}
if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
items.push(
UserInstructions {

View File

@@ -0,0 +1,76 @@
const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex <noreply@openai.com>";
fn build_commit_message_trailer(config_attribution: Option<&str>) -> Option<String> {
let value = resolve_attribution_value(config_attribution)?;
Some(format!("Co-authored-by: {value}"))
}
pub(crate) fn commit_message_trailer_instruction(
config_attribution: Option<&str>,
) -> Option<String> {
let trailer = build_commit_message_trailer(config_attribution)?;
Some(format!(
"When you write or edit a git commit message, ensure the message ends with this trailer exactly once:\n{trailer}\n\nRules:\n- Keep existing trailers and append this trailer at the end if missing.\n- Do not duplicate this trailer if it already exists.\n- Keep one blank line between the commit body and trailer block."
))
}
fn resolve_attribution_value(config_attribution: Option<&str>) -> Option<String> {
match config_attribution {
Some(value) => {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
None => Some(DEFAULT_ATTRIBUTION_VALUE.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::build_commit_message_trailer;
use super::commit_message_trailer_instruction;
use super::resolve_attribution_value;
#[test]
fn blank_attribution_disables_trailer_prompt() {
assert_eq!(build_commit_message_trailer(Some("")), None);
assert_eq!(commit_message_trailer_instruction(Some(" ")), None);
}
#[test]
fn default_attribution_uses_codex_trailer() {
assert_eq!(
build_commit_message_trailer(None).as_deref(),
Some("Co-authored-by: Codex <noreply@openai.com>")
);
}
#[test]
fn resolve_value_handles_default_custom_and_blank() {
assert_eq!(
resolve_attribution_value(None),
Some("Codex <noreply@openai.com>".to_string())
);
assert_eq!(
resolve_attribution_value(Some("MyAgent <me@example.com>")),
Some("MyAgent <me@example.com>".to_string())
);
assert_eq!(
resolve_attribution_value(Some("MyAgent")),
Some("MyAgent".to_string())
);
assert_eq!(resolve_attribution_value(Some(" ")), None);
}
#[test]
fn instruction_mentions_trailer_and_omits_generated_with() {
let instruction = commit_message_trailer_instruction(Some("AgentX <agent@example.com>"))
.expect("instruction expected");
assert!(instruction.contains("Co-authored-by: AgentX <agent@example.com>"));
assert!(instruction.contains("exactly once"));
assert!(!instruction.contains("Generated-with"));
}
}

View File

@@ -206,6 +206,13 @@ pub struct Config {
/// Compact prompt override.
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
///
/// - `None`: use default attribution (`Codex <noreply@openai.com>`)
/// - `Some("")` or whitespace-only: disable commit attribution
/// - `Some("...")`: use the provided attribution text verbatim
pub commit_attribution: Option<String>,
/// Optional external notifier command. When set, Codex will spawn this
/// program after each completed *turn* (i.e. when the agent finishes
/// processing a user submission). The value must be the full command
@@ -917,6 +924,11 @@ pub struct ConfigToml {
/// Compact prompt used for history compaction.
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
///
/// Set to an empty string to disable automatic commit attribution.
pub commit_attribution: Option<String>,
/// When set, restricts ChatGPT login to a specific workspace identifier.
#[serde(default)]
pub forced_chatgpt_workspace_id: Option<String>,
@@ -1696,6 +1708,8 @@ impl Config {
}
});
let commit_attribution = cfg.commit_attribution;
// Load base instructions override from a file if specified. If the
// path is relative, resolve it against the effective cwd so the
// behaviour matches other path-like config values.
@@ -1816,6 +1830,7 @@ impl Config {
personality,
developer_instructions,
compact_prompt,
commit_attribution,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
@@ -4192,6 +4207,7 @@ model_verbosity = "high"
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
@@ -4304,6 +4320,7 @@ model_verbosity = "high"
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
@@ -4414,6 +4431,7 @@ model_verbosity = "high"
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
@@ -4510,6 +4528,7 @@ model_verbosity = "high"
base_instructions: None,
developer_instructions: None,
compact_prompt: None,
commit_attribution: None,
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,

View File

@@ -105,6 +105,8 @@ pub enum Feature {
RemoteModels,
/// Experimental shell snapshotting.
ShellSnapshot,
/// Enable git commit attribution guidance via model instructions.
CodexGitCommit,
/// Enable runtime metrics snapshots via a manual reader.
RuntimeMetrics,
/// Persist rollout metadata to a local SQLite database.
@@ -466,6 +468,12 @@ pub const FEATURES: &[FeatureSpec] = &[
default_enabled: false,
},
// Experimental program. Rendered in the `/experimental` menu for users.
FeatureSpec {
id: Feature::CodexGitCommit,
key: "codex_git_commit",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::RuntimeMetrics,
key: "runtime_metrics",

View File

@@ -21,6 +21,7 @@ pub use codex_thread::ThreadConfigSnapshot;
mod agent;
mod codex_delegate;
mod command_canonicalization;
mod commit_attribution;
pub mod config;
pub mod config_loader;
pub mod connectors;