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

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).