mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Use prompt-based co-author attribution with config override (#11617)
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
76
codex-rs/core/src/commit_attribution.rs
Normal file
76
codex-rs/core/src/commit_attribution.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user