diff --git a/.a5c/config.yml b/.a5c/config.yml new file mode 100644 index 00000000000..5d0f96a682a --- /dev/null +++ b/.a5c/config.yml @@ -0,0 +1,26 @@ +remote_agents: + enabled: true + cache_timeout: 120 + retry_attempts: 5 + retry_delay: 2000 + sources: + individual: + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/developer-agent.agent.md + alias: developer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/validator-agent.agent.md + alias: validator-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/build-fixer-agent.agent.md + alias: build-fixer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/research/researcher-base-agent.agent.md + alias: researcher-base-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/communication/content-writer-agent.agent.md + alias: content-writer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/producer-agent.agent.md + alias: producer-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/conflict-resolver-agent.agent.md + alias: conflict-resolver-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/recruiter-agent.agent.md + alias: recruiter-agent + - uri: https://raw.githubusercontent.com/a5c-ai/registry/main/agents/development/product-optimizer-agent.agent.md + alias: product-optimizer-agent + groups: {} diff --git a/.github/workflows/a5c.yml b/.github/workflows/a5c.yml new file mode 100644 index 00000000000..ac9f8b06fc4 --- /dev/null +++ b/.github/workflows/a5c.yml @@ -0,0 +1,135 @@ +# Template for a5c agent workflow aligned to pnpm/Corepack. +# Maintainers: this template is authoritative. Please sync into +# .github/workflows/a5c.yml so the active workflow uses pnpm caching: +# - actions/setup-node@v4 with `cache: pnpm` +# - `cache-dependency-path: pnpm-lock.yaml` + +name: a5c + +env: + DISABLE_AUTOUPDATER: 1 + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1 + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + AZURE_OPENAI_PROJECT_NAME: ${{ vars.AZURE_OPENAI_PROJECT_NAME || '' }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY || '' }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY || '' }} + DEBUG: ${{ github.event.inputs.debug || 'false' }} + GITHUB_TOKEN: ${{ secrets.A5C_AGENT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + # Optional: supply a GitHub App token for validator checks + # A5C_AGENT_GITHUB_TOKEN: ${{ secrets.A5C_AGENT_GITHUB_TOKEN }} + # Feature flags for validator checks behavior + # A5C_VALIDATOR_ENABLE_CHECKS: "true" # set false to disable Check Runs + # A5C_VALIDATOR_USE_STATUSES: "false" # if supported, use commit statuses instead of checks + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + DISCORD_GUILD_ID: ${{ vars.DISCORD_GUILD_ID }} + A5C_CLI_TOOL: ${{ vars.A5C_CLI_TOOL }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN || '' }} + SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET || '' }} + SLACK_APP_TOKEN: ${{ secrets.SLACK_APP_TOKEN || '' }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN || '' }} + VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID || '' }} + VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID || '' }} + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN || '' }} + SUPABASE_ORG_ID: ${{ vars.SUPABASE_ORG_ID || '' }} + SUPABASE_PROJECT_REF: ${{ vars.SUPABASE_PROJECT_REF || '' }} + SUPABASE_PROJECT_URL: ${{ vars.SUPABASE_PROJECT_URL || '' }} + SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD || '' }} + STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY || '' }} + STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY || '' }} + STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET || '' }} + STRIPE_WEBHOOK_URL: ${{ vars.STRIPE_WEBHOOK_URL || '' }} + STRIPE_WEBHOOK_ID: ${{ vars.STRIPE_WEBHOOK_ID || '' }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID || '' }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }} + AWS_REGION: ${{ vars.AWS_REGION || '' }} + GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS || '' }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID || '' }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET || '' }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID || '' }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID || '' }} + AZURE_ACR_NAME: ${{ vars.AZURE_ACR_NAME || '' }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID || '' }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET || '' }} + AUTH_GITHUB_CLIENT_ID: ${{ secrets.AUTH_GITHUB_CLIENT_ID || '' }} + AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.AUTH_GITHUB_CLIENT_SECRET || '' }} + AUTH_GITHUB_ORG_ID: ${{ vars.AUTH_GITHUB_ORG_ID || '' }} + AUTH_GITHUB_ORG_NAME: ${{ vars.AUTH_GITHUB_ORG_NAME || '' }} + AUTH_GITHUB_ORG_DESCRIPTION: ${{ vars.AUTH_GITHUB_ORG_DESCRIPTION || '' }} + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY || '' }} + HEROKU_APP_NAME: ${{ vars.HEROKU_APP_NAME || '' }} + HEROKU_APP_ID: ${{ vars.HEROKU_APP_ID || '' }} + HEROKU_APP_URL: ${{ vars.HEROKU_APP_URL || '' }} + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, develop, master] + push: + branches: [] + issues: + types: [opened] + issue_comment: + types: [created] + schedule: + - cron: '*/30 * * * *' + workflow_run: + types: [completed] + workflows: [Build,Deploy,Tests,Release,E2E Tests,Infrastructure Deployment,Integration Tests,rust-ci] + workflow_dispatch: + inputs: + agent_uri: + description: 'Specific agent to run (optional - leave empty for auto-routing)' + required: false + debug: + description: 'Enable debug mode' + required: false + default: false + type: boolean + +jobs: + a5c: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + security-events: write + actions: write + attestations: write + checks: write + deployments: write + discussions: write + id-token: write + models: read + packages: write + pages: write + statuses: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Filter Self Workflow-run + id: filter-self + if: github.event_name == 'workflow_run' && (github.event.workflow_run.conclusion != 'failure' || github.event.workflow_run.head_branch != 'main') + run: | + echo "skip=true" >> "$GITHUB_OUTPUT" + + - name: Run A5C + id: agents + if: steps.filter-self.outputs.skip != 'true' + uses: a5c-ai/action@main + with: + agent_uri: ${{ github.event.inputs.agent_uri || '' }} + github_token: ${{ secrets.A5C_AGENT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + id: upload-artifacts + uses: actions/upload-artifact@v4 + with: + name: a5c-artifacts + path: | + /tmp/agent-output.md + /tmp/agent-output-*/** diff --git a/README.md b/README.md index 668a03acaf8..5b946e8f5ca 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Codex CLI supports a rich set of configuration options, with preferences stored - [Login on a "Headless" machine](./docs/authentication.md#connecting-on-a-headless-machine) - [**Advanced**](./docs/advanced.md) - [Non-interactive / CI mode](./docs/advanced.md#non-interactive--ci-mode) + - [Automation with codex exec](./docs/automation-exec.md) - [Tracing / verbose logging](./docs/advanced.md#tracing--verbose-logging) - [Model Context Protocol (MCP)](./docs/advanced.md#model-context-protocol-mcp) - [**Zero data retention (ZDR)**](./docs/zdr.md) @@ -99,4 +100,3 @@ Codex CLI supports a rich set of configuration options, with preferences stored ## License This repository is licensed under the [Apache-2.0 License](LICENSE). - diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh index 6cf2faafc84..0bcac89bff1 100755 --- a/codex-cli/scripts/install_native_deps.sh +++ b/codex-cli/scripts/install_native_deps.sh @@ -20,7 +20,7 @@ CODEX_CLI_ROOT="" # Until we start publishing stable GitHub releases, we have to grab the binaries # from the GitHub Action that created them. Update the URL below to point to the # appropriate workflow run: -WORKFLOW_URL="https://github.com/openai/codex/actions/runs/16840150768" # rust-v0.20.0-alpha.2 +WORKFLOW_URL="https://github.com/a5c-incubator/codex/actions/runs/17322665068" # rust-v0.20.0-alpha.2 while [[ $# -gt 0 ]]; do case "$1" in diff --git a/codex-rs/README.md b/codex-rs/README.md index 390f5d31aa2..a844b5ac843 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -63,6 +63,18 @@ codex completion zsh codex completion fish ``` +### Usage and Guardrail Resets + +Check your current guardrail usage and next reset times via: + +``` +codex usage +# or +codex /usage +``` + +When usage data is unavailable, the command prints a clear, non-fatal message and exits successfully. + ### Experimenting with the Codex Sandbox To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI: diff --git a/codex-rs/chatgpt/src/lib.rs b/codex-rs/chatgpt/src/lib.rs index 440a309db64..9b47b6f04bb 100644 --- a/codex-rs/chatgpt/src/lib.rs +++ b/codex-rs/chatgpt/src/lib.rs @@ -2,3 +2,4 @@ pub mod apply_command; mod chatgpt_client; mod chatgpt_token; pub mod get_task; +pub mod usage; diff --git a/codex-rs/chatgpt/src/usage.rs b/codex-rs/chatgpt/src/usage.rs new file mode 100644 index 00000000000..20dd4941919 --- /dev/null +++ b/codex-rs/chatgpt/src/usage.rs @@ -0,0 +1,80 @@ +use anyhow::Context; +use codex_core::config::Config; +use serde::Deserialize; + +use crate::chatgpt_client::chatgpt_get_request; + +/// High-level summary of guardrail usage for display in CLI. +#[derive(Debug, Clone, Default)] +pub struct UsageSummary { + pub plan: Option, + pub standard_used_minutes: Option, + pub standard_limit_minutes: Option, + pub reasoning_used_minutes: Option, + pub reasoning_limit_minutes: Option, + pub next_reset_at: Option, +} + +/// Flexible wire model so we can tolerate backend changes without breaking the CLI. +#[derive(Debug, Deserialize)] +struct RawUsage { + #[serde(default)] + plan: Option, + #[serde(default)] + next_reset_at: Option, + #[serde(default)] + reset_at: Option, + #[serde(default)] + standard: Option, + #[serde(default)] + reasoning: Option, +} + +#[derive(Debug, Deserialize)] +struct Bucket { + #[serde(default)] + used_minutes: Option, + #[serde(default)] + limit_minutes: Option, + #[serde(default)] + used: Option, + #[serde(default)] + limit: Option, +} + +impl From for UsageSummary { + fn from(raw: RawUsage) -> Self { + let plan = raw.plan; + let next_reset_at = raw.next_reset_at.or(raw.reset_at); + let (mut standard_used_minutes, mut standard_limit_minutes) = (None, None); + let (mut reasoning_used_minutes, mut reasoning_limit_minutes) = (None, None); + + if let Some(b) = raw.standard { + standard_used_minutes = b.used_minutes.or(b.used); + standard_limit_minutes = b.limit_minutes.or(b.limit); + } + if let Some(b) = raw.reasoning { + reasoning_used_minutes = b.used_minutes.or(b.used); + reasoning_limit_minutes = b.limit_minutes.or(b.limit); + } + + UsageSummary { + plan, + standard_used_minutes, + standard_limit_minutes, + reasoning_used_minutes, + reasoning_limit_minutes, + next_reset_at, + } + } +} + +/// Fetch ChatGPT guardrail usage using the current auth and config. +pub async fn get_usage(config: &Config) -> anyhow::Result { + // This path is provided by the ChatGPT backend for Codex usage display. + // The structure is intentionally parsed via a flexible wire model. + let raw: RawUsage = chatgpt_get_request(config, "/wham/usage".to_string()) + .await + .context("Failed to fetch usage from ChatGPT backend")?; + Ok(raw.into()) +} diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index c6d80c0adfa..7c8da6b5b16 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -2,6 +2,7 @@ pub mod debug_sandbox; mod exit_status; pub mod login; pub mod proto; +pub mod usage; use clap::Parser; use codex_common::CliConfigOverrides; diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 2acc3d84c50..c23c83ec7fc 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -12,12 +12,14 @@ use codex_cli::login::run_login_with_api_key; use codex_cli::login::run_login_with_chatgpt; use codex_cli::login::run_logout; use codex_cli::proto; +use codex_cli::usage::run_usage_command; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; use std::path::PathBuf; use crate::proto::ProtoCli; +use codex_cli::usage::UsageCommand; /// Codex CLI /// @@ -76,6 +78,10 @@ enum Subcommand { /// Internal: generate TypeScript protocol bindings. #[clap(hide = true)] GenerateTs(GenerateTsCommand), + + /// Show current guardrail usage and reset times. + #[clap(visible_alias = "/usage")] + Usage(UsageCommand), } #[derive(Debug, Parser)] @@ -212,6 +218,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() Some(Subcommand::GenerateTs(gen_cli)) => { codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?; } + Some(Subcommand::Usage(mut usage_cli)) => { + prepend_config_flags(&mut usage_cli.config_overrides, cli.config_overrides); + run_usage_command(usage_cli.config_overrides).await?; + } } Ok(()) diff --git a/codex-rs/cli/src/usage.rs b/codex-rs/cli/src/usage.rs new file mode 100644 index 00000000000..74323454985 --- /dev/null +++ b/codex-rs/cli/src/usage.rs @@ -0,0 +1,100 @@ +use codex_common::CliConfigOverrides; +use codex_core::config::Config; +use codex_core::config::ConfigOverrides; +use codex_login::AuthMode; +use codex_login::CodexAuth; + +use codex_chatgpt::usage::get_usage as get_chatgpt_usage; + +#[derive(Debug, clap::Parser)] +pub struct UsageCommand { + #[clap(skip)] + pub config_overrides: CliConfigOverrides, +} + +pub async fn run_usage_command(cli_config_overrides: CliConfigOverrides) -> anyhow::Result<()> { + let config = load_config_or_exit(cli_config_overrides); + + match CodexAuth::from_codex_home(&config.codex_home, config.preferred_auth_method) { + Ok(Some(auth)) => match auth.mode { + AuthMode::ApiKey => { + let plan = auth + .get_plan_type() + .unwrap_or_else(|| "unknown".to_string()); + println!("Plan: {plan}"); + println!( + "Using an API key. Guardrail usage does not apply; billing is per-token.\nSee https://platform.openai.com/account/usage for detailed usage." + ); + Ok(()) + } + AuthMode::ChatGPT => { + let plan = auth + .get_plan_type() + .unwrap_or_else(|| "unknown".to_string()); + match get_chatgpt_usage(&config).await { + Ok(summary) => { + println!("Plan: {plan}"); + if let Some(when) = summary.next_reset_at.as_deref() { + println!("Next reset: {when}"); + } + if let (Some(u), Some(l)) = ( + summary.standard_used_minutes, + summary.standard_limit_minutes, + ) { + println!("Standard: {u} / {l} minutes used"); + } + if let (Some(u), Some(l)) = ( + summary.reasoning_used_minutes, + summary.reasoning_limit_minutes, + ) { + println!("Reasoning: {u} / {l} minutes used"); + } + + // If no buckets printed, fall back to a generic message. + if summary.standard_used_minutes.is_none() + && summary.reasoning_used_minutes.is_none() + { + println!("Usage data retrieved, but no bucket details available."); + } + Ok(()) + } + Err(e) => { + println!( + "Plan: {plan}\nUnable to retrieve usage from ChatGPT backend.\nReason: {e}\nUsage information is currently unavailable." + ); + Ok(()) + } + } + } + }, + Ok(None) => { + println!("Not logged in. Usage information requires authentication.\nRun: codex login"); + Ok(()) + } + Err(e) => { + println!( + "Unable to determine authentication status.\nReason: {e}\nUsage information is currently unavailable." + ); + Ok(()) + } + } +} + +fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config { + let cli_overrides = match cli_config_overrides.parse_overrides() { + Ok(v) => v, + Err(e) => { + eprintln!("Error parsing -c overrides: {e}"); + std::process::exit(1); + } + }; + + let config_overrides = ConfigOverrides::default(); + match Config::load_with_cli_overrides(cli_overrides, config_overrides) { + Ok(config) => config, + Err(e) => { + eprintln!("Error loading configuration: {e}"); + std::process::exit(1); + } + } +} diff --git a/codex-rs/cli/tests/usage.rs b/codex-rs/cli/tests/usage.rs new file mode 100644 index 00000000000..dff0dd1fa35 --- /dev/null +++ b/codex-rs/cli/tests/usage.rs @@ -0,0 +1,11 @@ +use codex_cli::usage::run_usage_command; +use codex_common::CliConfigOverrides; + +#[tokio::test] +async fn usage_command_runs() { + // Should not error; prints a helpful message even if not logged in. + let overrides = CliConfigOverrides::default(); + run_usage_command(overrides) + .await + .expect("usage should run"); +} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d7c39c14a56..69b170603cc 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -556,6 +556,7 @@ impl Session { Some(turn_context.approval_policy), Some(turn_context.sandbox_policy.clone()), Some(sess.user_shell.clone()), + if config.plan_mode { Some(true) } else { None }, ))); sess.record_conversation_items(&conversation_items).await; @@ -567,6 +568,7 @@ impl Session { model, history_log_id, history_entry_count, + plan_mode: config.plan_mode, }), }) .chain(post_session_configured_error_events.into_iter()); @@ -613,8 +615,12 @@ impl Session { reason: Option, ) -> oneshot::Receiver { let (tx_approve, rx_approve) = oneshot::channel(); + { + let mut state = self.state.lock_unchecked(); + state.pending_approvals.insert(sub_id.clone(), tx_approve); + } let event = Event { - id: sub_id.clone(), + id: sub_id, msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id, command, @@ -623,10 +629,6 @@ impl Session { }), }; let _ = self.tx_event.send(event).await; - { - let mut state = self.state.lock_unchecked(); - state.pending_approvals.insert(sub_id, tx_approve); - } rx_approve } @@ -639,8 +641,12 @@ impl Session { grant_root: Option, ) -> oneshot::Receiver { let (tx_approve, rx_approve) = oneshot::channel(); + { + let mut state = self.state.lock_unchecked(); + state.pending_approvals.insert(sub_id.clone(), tx_approve); + } let event = Event { - id: sub_id.clone(), + id: sub_id, msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id, changes: convert_apply_patch_to_protocol(action), @@ -649,10 +655,6 @@ impl Session { }), }; let _ = self.tx_event.send(event).await; - { - let mut state = self.state.lock_unchecked(); - state.pending_approvals.insert(sub_id, tx_approve); - } rx_approve } @@ -1070,6 +1072,7 @@ async fn submission_loop( model, effort, summary, + plan_mode, } => { // Recalculate the persistent turn context with provided overrides. let prev = Arc::clone(&turn_context); @@ -1138,13 +1141,19 @@ async fn submission_loop( // Install the new persistent context for subsequent tasks/turns. turn_context = Arc::new(new_turn_context); - if cwd.is_some() || approval_policy.is_some() || sandbox_policy.is_some() { + if cwd.is_some() + || approval_policy.is_some() + || sandbox_policy.is_some() + || plan_mode.is_some() + { sess.record_conversation_items(&[ResponseItem::from(EnvironmentContext::new( cwd, approval_policy, sandbox_policy, // Shell is not configurable from turn to turn None, + // Allow per-turn override of plan mode via context + plan_mode, ))]) .await; } @@ -1166,6 +1175,7 @@ async fn submission_loop( model, effort, summary, + plan_mode: _, } => { // attempt to inject input into current task if let Err(items) = sess.inject_input(items) { diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 7845f5d4ce0..ed68d2942b8 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -185,6 +185,10 @@ pub struct Config { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: bool, + + /// When true, the agent operates in Plan Mode, proposing a plan first and + /// awaiting a decision before execution. + pub plan_mode: bool, } impl Config { @@ -497,6 +501,9 @@ pub struct ConfigToml { /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: Option, + + /// Top-level Plan Mode toggle. Defaults to false when omitted. + pub plan_mode: Option, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] @@ -605,6 +612,7 @@ pub struct ConfigOverrides { pub disable_response_storage: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, + pub plan_mode: Option, } impl Config { @@ -633,6 +641,7 @@ impl Config { disable_response_storage, show_raw_agent_reasoning, tools_web_search_request: override_tools_web_search_request, + plan_mode: _override_plan_mode, } = overrides; let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { @@ -807,6 +816,11 @@ impl Config { .unwrap_or(false), include_view_image_tool, disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false), + plan_mode: overrides + .plan_mode + .or(config_profile.plan_mode) + .or(cfg.plan_mode) + .unwrap_or(false), }; Ok(config) } @@ -1177,6 +1191,7 @@ disable_response_storage = true use_experimental_streamable_shell_tool: false, include_view_image_tool: true, disable_paste_burst: false, + plan_mode: false, }, o3_profile_config ); @@ -1235,6 +1250,7 @@ disable_response_storage = true use_experimental_streamable_shell_tool: false, include_view_image_tool: true, disable_paste_burst: false, + plan_mode: false, }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1308,6 +1324,7 @@ disable_response_storage = true use_experimental_streamable_shell_tool: false, include_view_image_tool: true, disable_paste_burst: false, + plan_mode: false, }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); diff --git a/codex-rs/core/src/config_profile.rs b/codex-rs/core/src/config_profile.rs index 54869919fbc..340d70439c7 100644 --- a/codex-rs/core/src/config_profile.rs +++ b/codex-rs/core/src/config_profile.rs @@ -15,6 +15,8 @@ pub struct ConfigProfile { /// [`ModelProviderInfo`] to use. pub model_provider: Option, pub approval_policy: Option, + /// When true, the agent operates in Plan Mode. + pub plan_mode: Option, pub disable_response_storage: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index b7ee862517a..ef5aad9c362 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -29,6 +29,7 @@ pub(crate) struct EnvironmentContext { pub sandbox_mode: Option, pub network_access: Option, pub shell: Option, + pub plan_mode: Option, } impl EnvironmentContext { @@ -37,6 +38,7 @@ impl EnvironmentContext { approval_policy: Option, sandbox_policy: Option, shell: Option, + plan_mode: Option, ) -> Self { Self { cwd, @@ -60,6 +62,7 @@ impl EnvironmentContext { None => None, }, shell, + plan_mode, } } } @@ -76,6 +79,7 @@ impl EnvironmentContext { /// ... /// ... /// ... + /// ... /// /// ``` pub fn serialize_to_xml(self) -> String { @@ -101,6 +105,9 @@ impl EnvironmentContext { { lines.push(format!(" {shell_name}")); } + if let Some(plan_mode) = self.plan_mode { + lines.push(format!(" {plan_mode}")); + } lines.push(ENVIRONMENT_CONTEXT_END.to_string()); lines.join("\n") } diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 999f8072862..46d221e03b2 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -393,6 +393,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() { model: Some("o3".to_string()), effort: Some(ReasoningEffort::High), summary: Some(ReasoningSummary::Detailed), + plan_mode: None, }) .await .unwrap(); @@ -521,6 +522,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() { model: "o3".to_string(), effort: ReasoningEffort::High, summary: ReasoningSummary::Detailed, + plan_mode: false, }) .await .unwrap(); diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 8403bbde28b..f23dab3aeb9 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -508,10 +508,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { } EventMsg::SessionConfigured(session_configured_event) => { let SessionConfiguredEvent { - session_id, - model, - history_log_id: _, - history_entry_count: _, + session_id, model, .. } = session_configured_event; ts_println!( @@ -529,6 +526,15 @@ impl EventProcessor for EventProcessorWithHumanOutput { ts_println!(self, "explanation: {explanation:?}"); ts_println!(self, "plan: {plan:?}"); } + EventMsg::PlanProposed(proposed) => { + ts_println!(self, "{}", "plan proposed:".style(self.magenta)); + for (i, step) in proposed.plan.steps.iter().enumerate() { + ts_println!(self, " {}. [{}] {}", i + 1, step.kind, step.description); + } + if let Some(notes) = &proposed.plan.notes { + ts_println!(self, "notes: {}", notes); + } + } EventMsg::GetHistoryEntryResponse(_) => { // Currently ignored in exec output. } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 785272a692c..d2ff1773b74 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -148,6 +148,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any base_instructions: None, include_plan_tool: None, include_apply_patch_tool: None, + plan_mode: None, include_view_image_tool: None, disable_response_storage: oss.then_some(true), show_raw_agent_reasoning: oss.then_some(true), diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index aae463ad925..e4bccdcbfba 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -399,6 +399,7 @@ impl CodexMessageProcessor { codex_protocol::config_types::ConfigProfile { model: v.model, approval_policy: v.approval_policy, + plan_mode: v.plan_mode, model_reasoning_effort: v.model_reasoning_effort, }, ) @@ -539,6 +540,7 @@ impl CodexMessageProcessor { model, effort, summary, + plan_mode: self.config.plan_mode, }) .await; @@ -798,6 +800,7 @@ fn derive_config_from_params( base_instructions, include_plan_tool, include_apply_patch_tool, + plan_mode: None, include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index c29cb52c22a..f80ddfa5686 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -160,6 +160,7 @@ impl CodexToolCallParam { codex_linux_sandbox_exe, base_instructions, include_plan_tool, + plan_mode: None, include_apply_patch_tool: None, include_view_image_tool: None, disable_response_storage: None, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index b297dadd9b7..ca06cba7527 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -193,6 +193,10 @@ async fn run_codex_tool_session_inner( .await; continue; } + EventMsg::PlanProposed(_) => { + // Tool currently does not handle plan review; ignore. + continue; + } EventMsg::Error(err_event) => { // Return a response to conclude the tool call when the Codex session reports an error (e.g., interruption). let result = json!({ diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index abfc542e24f..d6f8d3a4c2f 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -260,6 +260,7 @@ mod tests { model: "gpt-4o".to_string(), history_log_id: 1, history_entry_count: 1000, + plan_mode: false, }), }; @@ -289,6 +290,7 @@ mod tests { model: "gpt-4o".to_string(), history_log_id: 1, history_entry_count: 1000, + plan_mode: false, }; let event = Event { id: "1".to_string(), @@ -317,6 +319,7 @@ mod tests { "model": session_configured_event.model, "history_log_id": session_configured_event.history_log_id, "history_entry_count": session_configured_event.history_entry_count, + "plan_mode": session_configured_event.plan_mode, "type": "session_configured", } }); diff --git a/codex-rs/mcp-server/tests/suite/config.rs b/codex-rs/mcp-server/tests/suite/config.rs index cec41b25e00..e57e84198a6 100644 --- a/codex-rs/mcp-server/tests/suite/config.rs +++ b/codex-rs/mcp-server/tests/suite/config.rs @@ -71,6 +71,7 @@ async fn get_config_toml_returns_subset() { ConfigProfile { model: Some("gpt-4o".into()), approval_policy: Some(AskForApproval::OnRequest), + plan_mode: None, model_reasoning_effort: Some(ReasoningEffort::High), }, )])), diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 46fd9c1359b..08d90217f5d 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -57,5 +57,9 @@ pub enum SandboxMode { pub struct ConfigProfile { pub model: Option, pub approval_policy: Option, + /// When true, the agent operates in Plan Mode: it proposes a plan first + /// and awaits a decision (accept/edit/cancel) before execution. + #[serde(default)] + pub plan_mode: Option, pub model_reasoning_effort: Option, } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 1f6cbe07b80..1e4d8e7d1fb 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -77,6 +77,12 @@ pub enum Op { /// Will only be honored if the model is configured to use reasoning. summary: ReasoningSummaryConfig, + + /// Whether this turn should run with Plan Mode enabled. When true, the + /// agent will propose a plan first and await a decision before + /// executing. Defaults to the session-configured value. + #[serde(default)] + plan_mode: bool, }, /// Override parts of the persistent turn context for subsequent turns. @@ -109,6 +115,10 @@ pub enum Op { /// Updated reasoning summary preference (honored only for reasoning-capable models). #[serde(skip_serializing_if = "Option::is_none")] summary: Option, + + /// Updated Plan Mode setting for subsequent turns. + #[serde(skip_serializing_if = "Option::is_none")] + plan_mode: Option, }, /// Approve a command execution @@ -127,6 +137,17 @@ pub enum Op { decision: ReviewDecision, }, + /// Decision in response to a proposed plan. + PlanDecision { + /// The id of the submission correlated with the proposal. + id: String, + /// The user's decision. + decision: PlanDecision, + /// When editing, the updated plan to use. + #[serde(skip_serializing_if = "Option::is_none")] + edited_plan: Option, + }, + /// Append an entry to the persistent cross-session message history. /// /// Note the entry is not guaranteed to be logged if the user has @@ -437,6 +458,9 @@ pub enum EventMsg { /// Ack the client's configure message. SessionConfigured(SessionConfiguredEvent), + /// The agent proposes a plan for the user to review and approve. + PlanProposed(PlanProposedEvent), + McpToolCallBegin(McpToolCallBeginEvent), McpToolCallEnd(McpToolCallEndEvent), @@ -839,6 +863,53 @@ pub struct SessionConfiguredEvent { /// Current number of entries in the history log. pub history_entry_count: usize, + + /// Whether the session is in Plan Mode. + #[serde(default)] + pub plan_mode: bool, +} + +/// A single step in a proposed plan. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +#[serde(rename_all = "snake_case")] +pub struct PlanStep { + pub kind: PlanStepKind, + /// Short, imperative description of the step. + pub description: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Display, TS)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PlanStepKind { + Edit, + Command, + Test, +} + +/// A proposed plan consisting of ordered steps plus free‑form notes. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +#[serde(rename_all = "snake_case")] +pub struct Plan { + pub steps: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)] +#[serde(rename_all = "snake_case")] +pub struct PlanProposedEvent { + pub plan: Plan, +} + +/// User's decision in response to a `PlanProposed` event. +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, TS)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PlanDecision { + Accept, + Edit, + Cancel, } /// User's decision in response to an ExecApprovalRequest. @@ -912,12 +983,13 @@ mod tests { model: "codex-mini-latest".to_string(), history_log_id: 0, history_entry_count: 0, + plan_mode: false, }), }; let serialized = serde_json::to_string(&event).unwrap(); assert_eq!( serialized, - r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"codex-mini-latest","history_log_id":0,"history_entry_count":0}}"# + r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"codex-mini-latest","history_log_id":0,"history_entry_count":0,"plan_mode":false}}"# ); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 4695c6566cf..ac3fdde5523 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -823,6 +823,9 @@ impl ChatWidget { SlashCommand::Status => { self.add_status_output(); } + SlashCommand::Usage => { + self.add_usage_output(); + } SlashCommand::Mcp => { self.add_mcp_output(); } @@ -1005,6 +1008,9 @@ impl ChatWidget { EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev), EventMsg::ListCustomPromptsResponse(ev) => self.on_list_custom_prompts(ev), EventMsg::ShutdownComplete => self.on_shutdown_complete(), + codex_core::protocol::EventMsg::PlanProposed(_) => { + // TODO: Render plan proposal in UI overlay (future work). + } EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff), EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => { self.on_background_event(message) @@ -1070,6 +1076,10 @@ impl ChatWidget { )); } + pub(crate) fn add_usage_output(&mut self) { + self.add_to_history(history_cell::new_usage_output(&self.config)); + } + /// Open a popup to choose the model preset (model + reasoning effort). pub(crate) fn open_model_popup(&mut self) { let current_model = self.config.model.clone(); @@ -1091,6 +1101,7 @@ impl ChatWidget { model: Some(model_slug.clone()), effort: Some(effort), summary: None, + plan_mode: None, })); tx.send(AppEvent::UpdateModel(model_slug.clone())); tx.send(AppEvent::UpdateReasoningEffort(effort)); @@ -1132,6 +1143,7 @@ impl ChatWidget { model: None, effort: None, summary: None, + plan_mode: None, })); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone())); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index cc05b36fc0c..c4b83c1d710 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -18,6 +18,8 @@ use codex_core::protocol::McpInvocation; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; +use codex_login::AuthMode; +use codex_login::CodexAuth; use codex_login::get_auth_file; use codex_login::try_read_auth_json; use codex_protocol::parse_command::ParsedCommand; @@ -247,12 +249,7 @@ pub(crate) fn new_session_info( event: SessionConfiguredEvent, is_first_event: bool, ) -> PlainHistoryCell { - let SessionConfiguredEvent { - model, - session_id: _, - history_log_id: _, - history_entry_count: _, - } = event; + let SessionConfiguredEvent { model, .. } = event; if is_first_event { let cwd_str = match relativize_to_home(&config.cwd) { Some(rel) if !rel.as_os_str().is_empty() => { @@ -846,6 +843,46 @@ pub(crate) fn new_status_output( PlainHistoryCell { lines } } +/// Render guardrail usage information (placeholder until providers are wired). +pub(crate) fn new_usage_output(config: &Config) -> PlainHistoryCell { + let mut lines: Vec> = Vec::new(); + lines.push(Line::from("")); + lines.push(Line::from("/usage".magenta())); + + match CodexAuth::from_codex_home(&config.codex_home, config.preferred_auth_method) { + Ok(Some(auth)) => { + let plan = auth + .get_plan_type() + .unwrap_or_else(|| "unknown".to_string()); + match auth.mode { + AuthMode::ApiKey | AuthMode::ChatGPT => { + lines.push(Line::from("")); + lines.push(Line::from("Usage information is currently unavailable.")); + lines.push(Line::from(format!("Plan: {plan}"))); + lines.push(Line::from( + "Note: Guardrail usage and reset times will appear here when supported.", + )); + } + } + } + Ok(None) => { + lines.push(Line::from("")); + lines.push(Line::from( + "Not logged in. Usage information requires authentication.", + )); + lines.push(Line::from("Run: codex login")); + } + Err(e) => { + lines.push(Line::from("")); + lines.push(Line::from("Unable to determine authentication status.")); + lines.push(Line::from(format!("Reason: {e}"))); + lines.push(Line::from("Usage information is currently unavailable.")); + } + } + + PlainHistoryCell { lines } +} + /// Render a summary of configured MCP servers from the current `Config`. pub(crate) fn empty_mcp_output() -> PlainHistoryCell { let lines: Vec> = vec![ diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 544aec272a6..6c84fa0a332 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -126,6 +126,7 @@ pub async fn run_main( base_instructions: None, include_plan_tool: Some(true), include_apply_patch_tool: None, + plan_mode: None, include_view_image_tool: None, disable_response_storage: cli.oss.then_some(true), show_raw_agent_reasoning: cli.oss.then_some(true), diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 3268a92a2db..919751dec6c 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -20,6 +20,7 @@ pub enum SlashCommand { Diff, Mention, Status, + Usage, Mcp, Logout, Quit, @@ -38,6 +39,7 @@ impl SlashCommand { SlashCommand::Diff => "show git diff (including untracked files)", SlashCommand::Mention => "mention a file", SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::Usage => "show guardrail usage and reset times", SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Approvals => "choose what Codex can do without approval", SlashCommand::Mcp => "list configured MCP tools", @@ -65,6 +67,7 @@ impl SlashCommand { SlashCommand::Diff | SlashCommand::Mention | SlashCommand::Status + | SlashCommand::Usage | SlashCommand::Mcp | SlashCommand::Quit => true, diff --git a/docs/advanced.md b/docs/advanced.md index 26f735991f2..74f5b117dbf 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -2,7 +2,7 @@ ## Non-interactive / CI mode -Run Codex head-less in pipelines. Example GitHub Action step: +Run Codex head-less in pipelines. See also: `docs/automation-exec.md` for a complete guide. Example GitHub Action step: ```yaml - name: Update changelog via Codex @@ -39,4 +39,4 @@ env = { "API_KEY" = "value" } ``` > [!TIP] -> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. \ No newline at end of file +> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. diff --git a/docs/automation-exec.md b/docs/automation-exec.md new file mode 100644 index 00000000000..5ef55c59675 --- /dev/null +++ b/docs/automation-exec.md @@ -0,0 +1,113 @@ +# Automation with `codex exec` + +Run Codex headless for CI, cron jobs, or local scripts. The `codex exec` subcommand processes a prompt non‑interactively, streams progress to stdout, and exits when the task completes. + +- Basic use: `codex exec "…"` +- From stdin: `echo "…" | codex exec -` +- JSONL logs: `codex exec --json "…" | tee codex.jsonl` +- Save last reply: `codex exec --output-last-message out.txt "…"` + +## Prompt input + +- Argument: `codex exec "bump CHANGELOG for next release"` +- Stdin: `echo "bump CHANGELOG" | codex exec -` + - If no argument is given and stdin is a TTY, `codex exec` exits with an error. Use `-` to force reading from stdin. + +## Exit codes + +`codex exec` exits with: +- 0 on normal completion. +- 1 for usage/config errors, e.g.: + - No prompt provided (and stdin not forced via `-`). + - Failed `-c` overrides parsing. + - Outside a Git repo without `--skip-git-repo-check`. + +Note: Non‑zero exit codes from model‑executed shell commands are surfaced in logs/events, but do not change the process exit code. In CI, use `--json` and filter events (see below) to fail your job on specific conditions. + +## Approvals and sandbox + +- Non‑interactive runs default to “never ask for approval”. +- Recommended for automation: `--full-auto` (workspace‑write sandbox, no prompts): + - `codex exec --full-auto "update CHANGELOG for next release"` +- Or pick an explicit sandbox: `--sandbox read-only|workspace-write|danger-full-access`. + - See `docs/sandbox.md` for details and policies. + +## Output modes + +- Human logs (default): timestamped, human‑readable progress and diffs. Use `--color=never` for plain logs. +- JSONL logs: `--json` prints one JSON object per line for events. Streaming deltas are suppressed; all other events are included. The first two lines are a compact config summary and the prompt you provided, then event lines follow. +- Last message file: `--output-last-message path.txt` writes the assistant’s final message to a file. + +### JSONL examples (with jq) + +Capture logs: + +``` +codex exec --json --full-auto "update CHANGELOG for next release" | tee codex.jsonl +``` + +List commands and exit codes: + +``` +rg '"type":"exec_command_end"' codex.jsonl \ + | jq -r 'fromjson | select(.msg.type=="exec_command_end") | [.msg.call_id, (.msg.exit_code // 0), (.msg.duration_ms // null)] | @tsv' +``` + +Fail if any command exits non‑zero or the run was aborted: + +``` +# exits 1 if any matching event exists +jq -e 'select( + (.msg.type=="exec_command_end" and (.msg.exit_code // 0) != 0) + or .msg.type=="error" + or .msg.type=="turn_aborted" +)' codex.jsonl >/dev/null +``` + +Save the last assistant message for downstream steps: + +``` +codex exec --json --output-last-message last.txt "summarize recent changes" +cat last.txt +``` + +## CI snippets + +### GitHub Actions + +``` +- name: Install Codex + run: npm install -g @openai/codex + +- name: Bump changelog via Codex (JSONL; fail on tool errors) + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_KEY }} + run: | + set -euo pipefail + codex exec --json --full-auto "update CHANGELOG for next release" | tee codex.jsonl + # Fail the job if any command failed, or agent reported an error/abort + jq -e 'select( + (.msg.type=="exec_command_end" and (.msg.exit_code // 0) != 0) + or .msg.type=="error" + or .msg.type=="turn_aborted" + )' codex.jsonl >/dev/null && { echo "codex exec reported failures"; exit 1; } || true +``` + +Tips: +- Prefer `--full-auto` for CI to allow workspace writes under a sandbox. +- For headless machines, API key auth is simplest. See `docs/authentication.md`. +- Set `--color=never` to produce plain logs if needed. + +## Local scripts + +See `scripts/codex-exec-examples/` for runnable examples: +- `run_sync.sh`: simple synchronous run with human logs. +- `stdin_prompt.sh`: pass the prompt via stdin. +- `capture_jsonl_and_fail_on_errors.sh`: capture JSONL, validate with jq, and set an appropriate exit code. + +## Related docs + +- Sandbox & approvals: `docs/sandbox.md` +- Configuration: `docs/config.md` +- Advanced tips: `docs/advanced.md` + diff --git a/docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md b/docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md new file mode 100644 index 00000000000..78969bc99f9 --- /dev/null +++ b/docs/dev/build-fixer-agent/arm-elicit-race-20250829T113152Z.md @@ -0,0 +1,17 @@ +# Fix: Patch approval race on ARM GNU runner + +## Context +- Workflow run failed on job: ubuntu-24.04-arm - aarch64-unknown-linux-gnu +- Failure: mcp-server test `suite::codex_tool::test_patch_approval_triggers_elicitation` timed out +- Log excerpt showed: `No pending approval found for sub_id: 1` + +## Root cause +- Race condition in `codex-core`: inserted `pending_approvals` after sending event on channel. On fast paths, approval arrives before insertion. + +## Change +- Reordered insertion before sending events in: + - `request_command_approval` + - `request_patch_approval` + +## Verification +- Ran `cargo test -p codex-mcp-server`: all tests passed locally. diff --git a/docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md b/docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md new file mode 100644 index 00000000000..d4d726d0e94 --- /dev/null +++ b/docs/dev/developer-agent/automation-codex-exec-20250830T022024Z.md @@ -0,0 +1,4 @@ +# Dev log for docs/automation-codex-exec + +- Initial scaffold for Automation with codex exec docs and example scripts. +- Will add examples for stdin, JSONL logs, failure handling, and CI snippets. diff --git a/docs/dev/developer-agent/cli-usage-20250829T084245Z.md b/docs/dev/developer-agent/cli-usage-20250829T084245Z.md new file mode 100644 index 00000000000..8dd88dd97fe --- /dev/null +++ b/docs/dev/developer-agent/cli-usage-20250829T084245Z.md @@ -0,0 +1,22 @@ +# CLI: /usage command + +## Context +Implement a discoverable `codex usage` (`/usage` alias) to print current 5h and weekly guardrail usage with reset times. Gracefully handle unavailable data. + +## Plan +- Add `usage` subcommand in `codex-rs/cli` (alias `/usage`). +- Create a simple usage reporter with pluggable providers; default to a clear fallback message. +- Update help, add a basic CLI test. +- Validate with `cargo test -p codex-cli`. + +## Notes +- Avoid touching sandbox env var logic. +- Keep TUI unchanged to avoid snapshot breaks; future PR can add a compact indicator. + +## Results +- Implemented `usage` subcommand (alias `/usage`) with graceful fallback. +- Added test and README updates. +- Validated with `cargo test -p codex-cli`. + +## Next +- Plug in real usage providers when upstream endpoints are available or when rate-limit headers are persisted by the client layer. diff --git a/docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md b/docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md new file mode 100644 index 00000000000..903fc07a89f --- /dev/null +++ b/docs/dev/developer-agent/issue-23-plan-mode-20250830T025442Z.md @@ -0,0 +1 @@ +Started work on issue #23 at 2025-08-30T02:54:42Z. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..bb67c752581 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "codex-monorepo", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-monorepo", + "devDependencies": { + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=22", + "pnpm": ">=9.0.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh b/scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh new file mode 100755 index 00000000000..e8b99e214b6 --- /dev/null +++ b/scripts/codex-exec-examples/capture_jsonl_and_fail_on_errors.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Capture JSONL logs and fail the script if any command run by the agent +# exited non-zero, or if the agent reported an error/abort. + +require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 2; }; } +require jq + +LOG_FILE=${1:-codex.jsonl} + +codex exec --json --full-auto "update CHANGELOG for next release" | tee "$LOG_FILE" + +if jq -e 'select( + (.msg.type=="exec_command_end" and (.msg.exit_code // 0) != 0) + or .msg.type=="error" + or .msg.type=="turn_aborted" + )' "$LOG_FILE" >/dev/null; then + echo "codex exec reported failures; see $LOG_FILE" >&2 + exit 1 +fi + +echo "codex exec completed successfully" + diff --git a/scripts/codex-exec-examples/run_sync.sh b/scripts/codex-exec-examples/run_sync.sh new file mode 100755 index 00000000000..0041bb6b9c3 --- /dev/null +++ b/scripts/codex-exec-examples/run_sync.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Simple synchronous run with human-readable logs. +# Requires Codex CLI to be installed and authenticated. + +codex exec --full-auto --color=never "update CHANGELOG for next release" + diff --git a/scripts/codex-exec-examples/stdin_prompt.sh b/scripts/codex-exec-examples/stdin_prompt.sh new file mode 100755 index 00000000000..ffa5449aee1 --- /dev/null +++ b/scripts/codex-exec-examples/stdin_prompt.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Provide the prompt via stdin using '-'. + +cat <<'PROMPT' | codex exec --full-auto - +Generate release notes in CHANGELOG.md for the latest commits. +- Keep the style consistent with prior entries. +- Include a short summary and categorized changes. +PROMPT + diff --git a/scripts/publish_to_npm.py b/scripts/publish_to_npm.py index f79843ffc85..1abe4252578 100755 --- a/scripts/publish_to_npm.py +++ b/scripts/publish_to_npm.py @@ -69,7 +69,7 @@ def main() -> int: download_dir.mkdir(parents=True, exist_ok=True) # 1) Download the artifact using gh - repo = "openai/codex" + repo = "a5c-incubator/codex" gh_cmd = [ "gh", "release",