135 lines
6.6 KiB
Markdown
135 lines
6.6 KiB
Markdown
# 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](https://github.com/openai/codex/issues/26966),
|
|
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:
|
|
|
|
```sh
|
|
cp codex_goal.py ~/bin/codex_goal.py # or run it in place
|
|
chmod +x codex_goal.py
|
|
```
|
|
|
|
## Usage
|
|
|
|
```sh
|
|
# 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](../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
|
|
|
|
```sh
|
|
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](demo.txt) 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).
|