diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000000..f01272c61d --- /dev/null +++ b/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts +check-hidden = true +ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b +ignore-words-list = ratatui,ser diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000000..51df5c70b0 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,25 @@ +# Codespell configuration is within .codespellrc +--- +name: Codespell + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Annotate locations with typos + uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1 + - name: Codespell + uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2080ad849d..92707acfbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ You can install any of these versions: `npm install -g codex@version` +## `0.1.2505140839` + +### 🪲 Bug Fixes + +- Gpt-4.1 apply_patch handling (#930) +- Add support for fileOpener in config.json (#911) +- Patch in #366 and #367 for marked-terminal (#916) +- Remember to set lastIndex = 0 on shared RegExp (#918) +- Always load version from package.json at runtime (#909) +- Tweak the label for citations for better rendering (#919) +- Tighten up some logic around session timestamps and ids (#922) +- Change EventMsg enum so every variant takes a single struct (#925) +- Reasoning default to medium, show workdir when supplied (#931) +- Test_dev_null_write() was not using echo as intended (#923) + ## `0.1.2504301751` ### 🚀 Features diff --git a/codex-cli/examples/impossible-pong/task.yaml b/codex-cli/examples/impossible-pong/task.yaml index c89f3953da..8d8acbbf32 100644 --- a/codex-cli/examples/impossible-pong/task.yaml +++ b/codex-cli/examples/impossible-pong/task.yaml @@ -1,7 +1,7 @@ name: "impossible-pong" description: | Update index.html with the following features: - - Add an overlayed styled popup to start the game on first load + - Add an overlaid styled popup to start the game on first load - Between each point, show a 3 second countdown (this should be skipped if a player wins) - After each game the AI wins, display text at the bottom of the screen with lighthearted insults for the player - Add a leaderboard to the right of the court that shows how many games each player has won. diff --git a/codex-cli/examples/prompt-analyzer/template/prompts.csv b/codex-cli/examples/prompt-analyzer/template/prompts.csv index c6493bb5a3..498c03a189 100644 --- a/codex-cli/examples/prompt-analyzer/template/prompts.csv +++ b/codex-cli/examples/prompt-analyzer/template/prompts.csv @@ -13,7 +13,7 @@ act,prompt,for_devs "Advertiser","I want you to act as an advertiser. You will create a campaign to promote a product or service of your choice. You will choose a target audience, develop key messages and slogans, select the media channels for promotion, and decide on any additional activities needed to reach your goals. My first suggestion request is ""I need help creating an advertising campaign for a new type of energy drink targeting young adults aged 18-30.""",FALSE "Storyteller","I want you to act as a storyteller. You will come up with entertaining stories that are engaging, imaginative and captivating for the audience. It can be fairy tales, educational stories or any other type of stories which has the potential to capture people's attention and imagination. Depending on the target audience, you may choose specific themes or topics for your storytelling session e.g., if it's children then you can talk about animals; If it's adults then history-based tales might engage them better etc. My first request is ""I need an interesting story on perseverance.""",FALSE "Football Commentator","I want you to act as a football commentator. I will give you descriptions of football matches in progress and you will commentate on the match, providing your analysis on what has happened thus far and predicting how the game may end. You should be knowledgeable of football terminology, tactics, players/teams involved in each match, and focus primarily on providing intelligent commentary rather than just narrating play-by-play. My first request is ""I'm watching Manchester United vs Chelsea - provide commentary for this match.""",FALSE -"Stand-up Comedian","I want you to act as a stand-up comedian. I will provide you with some topics related to current events and you will use your wit, creativity, and observational skills to create a routine based on those topics. You should also be sure to incorporate personal anecdotes or experiences into the routine in order to make it more relatable and engaging for the audience. My first request is ""I want an humorous take on politics.""",FALSE +"Stand-up Comedian","I want you to act as a stand-up comedian. I will provide you with some topics related to current events and you will use your with, creativity, and observational skills to create a routine based on those topics. You should also be sure to incorporate personal anecdotes or experiences into the routine in order to make it more relatable and engaging for the audience. My first request is ""I want an humorous take on politics.""",FALSE "Motivational Coach","I want you to act as a motivational coach. I will provide you with some information about someone's goals and challenges, and it will be your job to come up with strategies that can help this person achieve their goals. This could involve providing positive affirmations, giving helpful advice or suggesting activities they can do to reach their end goal. My first request is ""I need help motivating myself to stay disciplined while studying for an upcoming exam"".",FALSE "Composer","I want you to act as a composer. I will provide the lyrics to a song and you will create music for it. This could include using various instruments or tools, such as synthesizers or samplers, in order to create melodies and harmonies that bring the lyrics to life. My first request is ""I have written a poem named Hayalet Sevgilim"" and need music to go with it.""""""",FALSE "Debater","I want you to act as a debater. I will provide you with some topics related to current events and your task is to research both sides of the debates, present valid arguments for each side, refute opposing points of view, and draw persuasive conclusions based on evidence. Your goal is to help people come away from the discussion with increased knowledge and insight into the topic at hand. My first request is ""I want an opinion piece about Deno.""",FALSE @@ -23,7 +23,7 @@ act,prompt,for_devs "Movie Critic","I want you to act as a movie critic. You will develop an engaging and creative movie review. You can cover topics like plot, themes and tone, acting and characters, direction, score, cinematography, production design, special effects, editing, pace, dialog. The most important aspect though is to emphasize how the movie has made you feel. What has really resonated with you. You can also be critical about the movie. Please avoid spoilers. My first request is ""I need to write a movie review for the movie Interstellar""",FALSE "Relationship Coach","I want you to act as a relationship coach. I will provide some details about the two people involved in a conflict, and it will be your job to come up with suggestions on how they can work through the issues that are separating them. This could include advice on communication techniques or different strategies for improving their understanding of one another's perspectives. My first request is ""I need help solving conflicts between my spouse and myself.""",FALSE "Poet","I want you to act as a poet. You will create poems that evoke emotions and have the power to stir people's soul. Write on any topic or theme but make sure your words convey the feeling you are trying to express in beautiful yet meaningful ways. You can also come up with short verses that are still powerful enough to leave an imprint in readers' minds. My first request is ""I need a poem about love.""",FALSE -"Rapper","I want you to act as a rapper. You will come up with powerful and meaningful lyrics, beats and rhythm that can 'wow' the audience. Your lyrics should have an intriguing meaning and message which people can relate too. When it comes to choosing your beat, make sure it is catchy yet relevant to your words, so that when combined they make an explosion of sound everytime! My first request is ""I need a rap song about finding strength within yourself.""",FALSE +"Rapper","I want you to act as a rapper. You will come up with powerful and meaningful lyrics, beats and rhythm that can 'wow' the audience. Your lyrics should have an intriguing meaning and message which people can relate too. When it comes to choosing your beat, make sure it is catchy yet relevant to your words, so that when combined they make an explosion of sound every time! My first request is ""I need a rap song about finding strength within yourself.""",FALSE "Motivational Speaker","I want you to act as a motivational speaker. Put together words that inspire action and make people feel empowered to do something beyond their abilities. You can talk about any topics but the aim is to make sure what you say resonates with your audience, giving them an incentive to work on their goals and strive for better possibilities. My first request is ""I need a speech about how everyone should never give up.""",FALSE "Philosophy Teacher","I want you to act as a philosophy teacher. I will provide some topics related to the study of philosophy, and it will be your job to explain these concepts in an easy-to-understand manner. This could include providing examples, posing questions or breaking down complex ideas into smaller pieces that are easier to comprehend. My first request is ""I need help understanding how different philosophical theories can be applied in everyday life.""",FALSE "Philosopher","I want you to act as a philosopher. I will provide some topics or questions related to the study of philosophy, and it will be your job to explore these concepts in depth. This could involve conducting research into various philosophical theories, proposing new ideas or finding creative solutions for solving complex problems. My first request is ""I need help developing an ethical framework for decision making.""",FALSE diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index 7d9ea25639..d53ed07f74 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -308,7 +308,7 @@ config = { model: model ?? config.model, notify: Boolean(cli.flags.notify), reasoningEffort: - (cli.flags.reasoning as ReasoningEffort | undefined) ?? "high", + (cli.flags.reasoning as ReasoningEffort | undefined) ?? "medium", flexMode: cli.flags.flexMode || (config.flexMode ?? false), provider, disableResponseStorage, diff --git a/codex-cli/src/components/chat/terminal-chat-response-item.tsx b/codex-cli/src/components/chat/terminal-chat-response-item.tsx index a81d541406..bda4fea9db 100644 --- a/codex-cli/src/components/chat/terminal-chat-response-item.tsx +++ b/codex-cli/src/components/chat/terminal-chat-response-item.tsx @@ -173,6 +173,11 @@ function TerminalChatResponseToolCall({ command + {details?.workdir ? ( + {` (${details?.workdir})`} + ) : ( + "" + )} $ {details?.cmdReadableText} @@ -325,14 +330,12 @@ function rewriteFileCitations( fileOpener: FileOpenerScheme | undefined, cwd: string = process.cwd(), ): string { - if (!fileOpener) { - // Should we reformat the citations even if we cannot linkify them? - return markdown; - } - citationRegex.lastIndex = 0; return markdown.replace(citationRegex, (_match, file, start, _end) => { const absPath = path.resolve(cwd, file); + if (!fileOpener) { + return `[${file}](${absPath})`; + } const uri = `${fileOpener}://file${absPath}:${start}`; const label = `${file}:${start}`; // In practice, sometimes multiple citations for the same file, but with a diff --git a/codex-cli/src/components/chat/terminal-message-history.tsx b/codex-cli/src/components/chat/terminal-message-history.tsx index 5ecf7fe0b0..5036f0813d 100644 --- a/codex-cli/src/components/chat/terminal-message-history.tsx +++ b/codex-cli/src/components/chat/terminal-message-history.tsx @@ -62,11 +62,19 @@ const TerminalMessageHistory: React.FC = ({ key={`${message.id}-${index}`} flexDirection="column" marginLeft={ - message.type === "message" && message.role === "user" ? 0 : 4 + message.type === "message" && + (message.role === "user" || message.role === "assistant") + ? 0 + : 4 } marginTop={ message.type === "message" && message.role === "user" ? 0 : 1 } + marginBottom={ + message.type === "message" && message.role === "assistant" + ? 1 + : 0 + } > = [ + `User: ${userName}`, + `Workdir: ${workdir}`, +]; +if (spawnSync("rg", ["--version"], { stdio: "ignore" }).status === 0) { + dynamicLines.push( + "- Always use rg instead of grep/ls -R because it is much faster and respects gitignore", + ); +} +const dynamicPrefix = dynamicLines.join("\n"); const prefix = `You are operating as and within the Codex CLI, a terminal-based agentic coding assistant built by OpenAI. It wraps OpenAI models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful. You can: @@ -1535,7 +1557,9 @@ You MUST adhere to the following criteria when executing the task: - Respond in a friendly tone as a remote teammate, who is knowledgeable, capable and eager to help with coding. - When your task involves writing or modifying files: - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using \`apply_patch\`. Instead, reference the file as already saved. - - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them.`; + - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them. + +${dynamicPrefix}`; function filterToApiMessages( items: Array, diff --git a/codex-cli/src/utils/agent/apply-patch.ts b/codex-cli/src/utils/agent/apply-patch.ts index 525404c30a..0f66a8e090 100644 --- a/codex-cli/src/utils/agent/apply-patch.ts +++ b/codex-cli/src/utils/agent/apply-patch.ts @@ -550,7 +550,15 @@ export function text_to_patch( !(lines[0] ?? "").startsWith(PATCH_PREFIX.trim()) || lines[lines.length - 1] !== PATCH_SUFFIX.trim() ) { - throw new DiffError("Invalid patch text"); + let reason = "Invalid patch text: "; + if (lines.length < 2) { + reason += "Patch text must have at least two lines."; + } else if (!(lines[0] ?? "").startsWith(PATCH_PREFIX.trim())) { + reason += "Patch text must start with the correct patch prefix."; + } else if (lines[lines.length - 1] !== PATCH_SUFFIX.trim()) { + reason += "Patch text must end with the correct patch suffix."; + } + throw new DiffError(reason); } const parser = new Parser(orig, lines); parser.index = 1; @@ -762,3 +770,46 @@ if (import.meta.url === `file://${process.argv[1]}`) { } }); } + +export const applyPatchToolInstructions = ` +To edit files, ALWAYS use the \`shell\` tool with \`apply_patch\` CLI. \`apply_patch\` effectively allows you to execute a diff/patch against a file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the \`apply_patch\` CLI, you should call the shell tool with the following structure: + +\`\`\`bash +{"cmd": ["apply_patch", "<<'EOF'\\n*** Begin Patch\\n[YOUR_PATCH]\\n*** End Patch\\nEOF\\n"], "workdir": "..."} +\`\`\` + +Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format. + +*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete. +For each snippet of code that needs to be changed, repeat the following: +[context_before] -> See below for further instructions on context. +- [old_code] -> Precede the old code with a minus sign. ++ [new_code] -> Precede the new, replacement code with a plus sign. +[context_after] -> See below for further instructions on context. + +For instructions on [context_before] and [context_after]: +- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines. +- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have: +@@ class BaseClass +[3 lines of pre-context] +- [old_code] ++ [new_code] +[3 lines of post-context] + +- If a code block is repeated so many times in a class or function such that even a single \`@@\` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple \`@@\` statements to jump to the right context. For instance: + +@@ class BaseClass +@@ def method(): +[3 lines of pre-context] +- [old_code] ++ [new_code] +[3 lines of post-context] + +Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below. + +\`\`\`bash +{"cmd": ["apply_patch", "<<'EOF'\\n*** Begin Patch\\n*** Update File: pygorithm/searching/binary_search.py\\n@@ class BaseClass\\n@@ def search():\\n- pass\\n+ raise NotImplementedError()\\n@@ class Subclass\\n@@ def search():\\n- pass\\n+ raise NotImplementedError()\\n*** End Patch\\nEOF\\n"], "workdir": "..."} +\`\`\` + +File references can only be relative, NEVER ABSOLUTE. After the apply_patch command is run, it will always say "Done!", regardless of whether the patch was successfully applied or not. However, you can determine if there are issue and errors by looking at any warnings or logging lines printed BEFORE the "Done!" is output. +`; diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index 58de989522..1f56dde1e3 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -9,11 +9,13 @@ import { execWithLandlock } from "./sandbox/landlock.js"; import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js"; import { exec as rawExec } from "./sandbox/raw-exec.js"; import { formatCommandForDisplay } from "../../format-command.js"; +import { log } from "../logger/log.js"; import fs from "fs"; import os from "os"; import path from "path"; import { parse } from "shell-quote"; import { resolvePathAgainstWorkdir } from "src/approvals.js"; +import { PATCH_SUFFIX } from "src/parse-apply-patch.js"; const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds @@ -81,12 +83,22 @@ export function execApplyPatch( patchText: string, workdir: string | undefined = undefined, ): ExecResult { - // This is a temporary measure to understand what are the common base commands - // until we start persisting and uploading rollouts + // This find/replace is required from some models like 4.1 where the patch + // text is wrapped in quotes that breaks the apply_patch command. + let applyPatchInput = patchText + .replace(/('|")?<<('|")EOF('|")/, "") + .replace(/\*\*\* End Patch\nEOF('|")?/, "*** End Patch") + .trim(); + + if (!applyPatchInput.endsWith(PATCH_SUFFIX)) { + applyPatchInput += "\n" + PATCH_SUFFIX; + } + + log(`Applying patch: \`\`\`${applyPatchInput}\`\`\`\n\n`); try { const result = process_patch( - patchText, + applyPatchInput, (p) => fs.readFileSync(resolvePathAgainstWorkdir(p, workdir), "utf8"), (p, c) => { const resolvedPath = resolvePathAgainstWorkdir(p, workdir); diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index d151c05f7d..aba99b1352 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -76,7 +76,7 @@ export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || ""; export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || ""; // Can be set `true` when Codex is running in an environment that is marked as already -// considered sufficiently locked-down so that we allow running wihtout an explicit sandbox. +// considered sufficiently locked-down so that we allow running without an explicit sandbox. export const CODEX_UNSAFE_ALLOW_NO_SANDBOX = Boolean( process.env["CODEX_UNSAFE_ALLOW_NO_SANDBOX"] || "", ); diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index ed86833f51..7075ac051c 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -35,6 +35,7 @@ export function parseToolCallOutput(toolCallOutput: string): { export type CommandReviewDetails = { cmd: Array; cmdReadableText: string; + workdir: string | undefined; }; /** @@ -51,12 +52,13 @@ export function parseToolCall( return undefined; } - const { cmd } = toolCallArgs; + const { cmd, workdir } = toolCallArgs; const cmdReadableText = formatCommandForDisplay(cmd); return { cmd, cmdReadableText, + workdir, }; } diff --git a/codex-rs/README.md b/codex-rs/README.md index fa7244200d..4babf226ab 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -10,7 +10,7 @@ To that end, we are moving forward with a Rust implementation of Codex CLI conta - Can make direct, native calls to [seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) and [landlock](https://man7.org/linux/man-pages/man7/landlock.7.html) in order to support sandboxing on Linux. - No runtime garbage collection, resulting in lower memory consumption and better, more predictable performance. -Currently, the Rust implementation is materially behind the TypeScript implementation in functionality, so continue to use the TypeScript implmentation for the time being. We will publish native executables via GitHub Releases as soon as we feel the Rust version is usable. +Currently, the Rust implementation is materially behind the TypeScript implementation in functionality, so continue to use the TypeScript implementation for the time being. We will publish native executables via GitHub Releases as soon as we feel the Rust version is usable. ## Code Organization diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 440451a90d..a98a272417 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -832,7 +832,7 @@ async fn run_turn( let prev_id = if store { state.previous_response_id.clone() } else { - // When using ZDR, the Reponses API may send previous_response_id + // When using ZDR, the Responses API may send previous_response_id // back, but trying to use it results in a 400. None }; diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index de758c8894..af09ded0bd 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -318,7 +318,7 @@ pub(crate) async fn spawn_child_async( cmd.kill_on_drop(true).spawn() } -/// Alternative verison of `spawn_child_async()` that returns +/// Alternative version of `spawn_child_async()` that returns /// `std::process::Child` instead of `tokio::process::Child`. This is useful for /// spawning a child process in a thread that is not running a Tokio runtime. pub fn spawn_child_sync( diff --git a/codex-rs/execpolicy/src/arg_resolver.rs b/codex-rs/execpolicy/src/arg_resolver.rs index 060009f810..4342f1b45b 100644 --- a/codex-rs/execpolicy/src/arg_resolver.rs +++ b/codex-rs/execpolicy/src/arg_resolver.rs @@ -30,7 +30,7 @@ pub fn resolve_observed_args_with_patterns( // `suffix_patterns` and use that to determine how many args are left to // be matched by `vararg_pattern` (which could be zero). // - // After assocating positional args with `vararg_pattern`, we match the + // After associating positional args with `vararg_pattern`, we match the // `suffix_patterns` with the remaining args. let ParitionedArgs { num_prefix_args, diff --git a/codex-rs/execpolicy/src/default.policy b/codex-rs/execpolicy/src/default.policy index bd27a0bb30..a0e2b27eff 100644 --- a/codex-rs/execpolicy/src/default.policy +++ b/codex-rs/execpolicy/src/default.policy @@ -136,7 +136,7 @@ define_program( ["--glob", "src"], ], # TODO(mbolin): Perhaps we need a way to indicate that we expect `rg` to be - # bundled with the host environment and we should be using that verison. + # bundled with the host environment and we should be using that version. system_path=[], )