Files
time/ventures/codex-goal/README.md
T

6.6 KiB

codex-goal

A tiny wrapper that remembers one objective per project across separate codex exec invocations, and injects it as plain user-context text every time you run or resume. Inspired by openai/codex#26966: Persist goals across codex exec sessions, closed as not planned. This is external memory bolted onto the outside of Codex, not a native goal mode: Codex itself is never changed, and never gets an API to mark the goal "done" or rewrite it.

Problem

codex exec is stateless about why you're running it. Resume brings back the transcript, but nothing re-states the objective, so a long-running or multi-session task slowly drifts as context gets summarized away. Upstream closed the feature request; this reconstructs it from outside the agent.

Install

Requires Python 3.9+, a POSIX host (macOS/Linux — matches the board's hb-5 Mac and Colima container targets), and the codex CLI on PATH for real use. No pip install, no dependencies:

cp codex_goal.py ~/bin/codex_goal.py   # or run it in place
chmod +x codex_goal.py

Usage

# Save (or replace) the objective for the current directory's project.
codex_goal.py set "Migrate the billing module to the new schema, keep tests green"

# Inspect it any time.
codex_goal.py show
codex_goal.py show --json --history

# Start a new codex exec session with the goal injected as user context.
codex_goal.py run

# Resume the most recent session -- goal is re-injected, not just the transcript.
codex_goal.py run --last

# Resume a specific session id, and forward real codex flags/an extra prompt.
codex_goal.py run --resume 3fae2b1e-... -- -s workspace-write "focus on the invoices table next"

Everything after run's own flags is forwarded to codex exec (or codex exec resume ...) untouched and in order — sandbox mode, model, -C, etc. all still work exactly as they do calling codex directly. A trailing bare argument (not starting with -) is treated as this turn's extra instruction and appended after the injected goal; everything else is passed straight through.

-C DIR (default: cwd) on any subcommand selects the project. The project key is a hash of its realpath, so set/show/run keep working no matter which subdirectory you're standing in, and two projects never collide.

How persistence works

  • State lives under $CODEX_GOAL_HOME (default ~/.codex-goal), outside whatever directory codex exec's sandbox would let the agent write to. Codex running in its default workspace-write sandbox cannot see or modify its own goal file — set is a human-run command, never something the wrapper calls on Codex's behalf.
  • Goal records are versioned: each set pushes the previous value into a capped history (50 entries) instead of overwriting it silently.
  • Writes are atomic (tempfile + os.replace) and guarded by an exclusive file lock (fcntl.flock) around the whole read-modify-write, so a crash or a concurrent set can't leave a half-written or torn JSON file.
  • run never modifies the goal. It only appends one line to a separate run-log ($CODEX_GOAL_HOME/runs/<key>.jsonl) after the child process exits, recording the mode, a goal preview, the exact argv, and the real exit code. A run that crashes mid-flight (killed, SIGKILL, power loss) leaves both the goal and the prior run-log lines untouched — there is no "mark this goal complete" affordance for Codex or the wrapper to call, so nothing can silently clear it.
  • Codex is spawned via subprocess.Popen([...]) with an argv list, never a shell string — a goal containing ; rm -rf / # is passed as one opaque argv element, not executed. The child gets its own process group, and SIGINT/SIGTERM sent to the wrapper are forwarded to it, so Ctrl-C cancels a run the same way it would cancel codex directly. The wrapper's own exit code is the child's exit code.

Limits and trust

  • One objective per project at a time. It is not a task list, not a scheduler, and it does not talk to the Codex API directly — it only shells out to the codex binary you already have configured and authenticated.
  • The injected text is plain, visible user context ("[codex-goal] Persisted objective...") — it carries no special authority inside Codex's own permission model. A model can still choose to ignore it, same as any other instruction.
  • No enforcement that the agent is actually working the stated goal; this only guarantees the goal text is faithfully re-presented every run. It does not inspect diffs, run checks, or grade progress — see finishline-supervisor for that kind of evidence wrapper, which composes with this one but is a separate tool.
  • $CODEX_GOAL_HOME is trusted local storage with your own file permissions; this tool adds no encryption or extra access control. Don't put secrets in goal text — it is written to disk in plain JSON.
  • Locking is advisory (flock) and local-filesystem only; it does not work over NFS/SMB and does not coordinate across machines.
  • run passes through whatever codex exec flags you give it, including --dangerously-bypass-approvals-and-sandbox if you ask for it — this wrapper does not add or remove any safety behavior from Codex itself.

Develop and verify

python3 -m unittest discover -s tests -p 'test_*.py' -v
python3 demo.py > demo.txt   # recorded, runnable, no network/auth needed

Tests and the demo use tests/fake_codex.py, a deterministic stand-in for the real codex binary (records the exact argv it received, exits with a configurable code, optionally sleeps so cancellation can be tested) — no model calls, no API key, nothing billed, per the board's preference for deterministic, cheap, inspectable checks over live model calls. Recorded demo output covers: set/show round trip, run 1 (fresh session), run 2 (--last resume, same goal), and a kill -9 mid-run followed by a show proving the goal survived byte-for-byte.

Verified: all 16 unit tests plus the demo on the local macOS (hb-5) host, and inside the project's Colima Linux container (same Python 3, same tests/fake_codex.py, no code changes needed — the tool has no platform-specific dependency beyond POSIX fcntl/process groups). Real codex binary integration (the actual CLI shown in Usage above) was exercised manually against the installed codex CLI's --help output to confirm the exec/exec resume --last/flag-forwarding argv shape this wrapper builds matches the real CLI; it was not run against a live Codex backend (that needs your own account/auth and would spend real requests).