diff --git a/ventures/codex-goal/.gitignore b/ventures/codex-goal/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/ventures/codex-goal/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/ventures/codex-goal/README.md b/ventures/codex-goal/README.md new file mode 100644 index 0000000..62e67f5 --- /dev/null +++ b/ventures/codex-goal/README.md @@ -0,0 +1,134 @@ +# 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/.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). diff --git a/ventures/codex-goal/codex_goal.py b/ventures/codex-goal/codex_goal.py new file mode 100755 index 0000000..7983505 --- /dev/null +++ b/ventures/codex-goal/codex_goal.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +"""codex-goal: persist a per-project objective across `codex exec` sessions. + +External memory, not native Codex state: it stores the goal outside the +directory Codex is allowed to write to, and injects it as plain user-context +text on every `set`/`show`/`run` the human runs. Codex itself never sees a +"mark complete" affordance -- only `set` can change the stored goal, and +`set` is never invoked by codex-goal's own `run` command. + +Usage: + codex_goal.py set [-C DIR] GOAL_TEXT... + codex_goal.py show [-C DIR] [--json] [--history] + codex_goal.py run [-C DIR] [--codex-bin PATH] [--resume ID | --last] + [--dry-run] [-- CODEX_ARGS... [PROMPT]] +""" +from __future__ import annotations + +import argparse +import contextlib +import hashlib +import json +import os +import signal +import subprocess +import sys +import tempfile +import time +import uuid + +try: + import fcntl +except ImportError: # pragma: no cover - POSIX only, matches board brief hosts + fcntl = None + +SCHEMA_VERSION = 1 +MAX_GOAL_CHARS = 4000 +MAX_HISTORY = 50 +ENV_HOME = "CODEX_GOAL_HOME" + + +def state_home() -> str: + override = os.environ.get(ENV_HOME) + if override: + return override + return os.path.join(os.path.expanduser("~"), ".codex-goal") + + +def project_key(project_dir: str) -> str: + real = os.path.realpath(project_dir) + digest = hashlib.sha256(real.encode("utf-8")).hexdigest()[:16] + base = os.path.basename(real.rstrip("/")) or "root" + safe_base = "".join(c if c.isalnum() or c in "-_." else "-" for c in base) + return f"{safe_base}-{digest}" + + +def goal_path(home: str, key: str) -> str: + return os.path.join(home, "goals", f"{key}.json") + + +def runlog_path(home: str, key: str) -> str: + return os.path.join(home, "runs", f"{key}.jsonl") + + +def lock_path(home: str, key: str) -> str: + return os.path.join(home, "locks", f"{key}.lock") + + +@contextlib.contextmanager +def locked(home: str, key: str): + """Exclusive file lock guarding read-modify-write of one project's state.""" + path = lock_path(home, key) + os.makedirs(os.path.dirname(path), exist_ok=True) + fh = open(path, "a+") + try: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + yield + finally: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_UN) + fh.close() + + +def atomic_write_json(path: str, payload: dict) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + fd, tmp = tempfile.mkstemp(prefix=".tmp-", dir=os.path.dirname(path)) + try: + with os.fdopen(fd, "w") as fh: + json.dump(payload, fh, indent=2, sort_keys=True) + fh.write("\n") + fh.flush() + os.fsync(fh.fileno()) + os.replace(tmp, path) # atomic on POSIX: a reader never sees a partial file + except Exception: + with contextlib.suppress(OSError): + os.remove(tmp) + raise + + +def append_jsonl(path: str, record: dict) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + line = json.dumps(record, sort_keys=True) + "\n" + with open(path, "a") as fh: + fh.write(line) + fh.flush() + os.fsync(fh.fileno()) + + +def now_iso() -> str: + return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + + +def load_state(path: str) -> dict | None: + if not os.path.exists(path): + return None + with open(path) as fh: + return json.load(fh) + + +def cmd_set(args: argparse.Namespace) -> int: + text = " ".join(args.goal).strip() + if not text: + print("codex-goal: refusing an empty goal", file=sys.stderr) + return 2 + if len(text) > MAX_GOAL_CHARS: + print(f"codex-goal: goal exceeds {MAX_GOAL_CHARS} characters", file=sys.stderr) + return 2 + + home = state_home() + key = project_key(args.project) + path = goal_path(home, key) + + with locked(home, key): + state = load_state(path) + ts = now_iso() + if state is None: + state = { + "schema_version": SCHEMA_VERSION, + "project": {"path": os.path.realpath(args.project), "key": key}, + "current": None, + "history": [], + } + if state["current"] is not None: + state["history"].append(state["current"]) + state["history"] = state["history"][-MAX_HISTORY:] + state["current"] = { + "goal_id": f"g-{uuid.uuid4().hex[:12]}", + "text": text, + "created_at": ts, + "updated_at": ts, + } + atomic_write_json(path, state) + + print(f"codex-goal: saved goal {state['current']['goal_id']} for {key}") + return 0 + + +def cmd_show(args: argparse.Namespace) -> int: + home = state_home() + key = project_key(args.project) + state = load_state(goal_path(home, key)) + + if state is None or state.get("current") is None: + if args.json: + print(json.dumps({"project_key": key, "current": None})) + else: + print(f"codex-goal: no goal set for {key}. Run: codex_goal.py set ") + return 1 + + if args.json: + payload = {"project_key": key, "current": state["current"]} + if args.history: + payload["history"] = state["history"] + print(json.dumps(payload, indent=2, sort_keys=True)) + else: + cur = state["current"] + print(f"project {key}") + print(f"goal_id {cur['goal_id']}") + print(f"updated {cur['updated_at']}") + print(f"text {cur['text']}") + if args.history and state["history"]: + print("\nhistory (oldest first):") + for h in state["history"]: + print(f" - [{h['updated_at']}] {h['goal_id']}: {h['text']}") + return 0 + + +INJECT_HEADER = "[codex-goal] Persisted objective for this project (external memory, injected by a wrapper -- not native Codex state):" +INJECT_FOOTER = "[/codex-goal] Continue working toward the objective above unless the user says otherwise." + + +def build_prompt(goal_text: str, extra_prompt: str | None) -> str: + block = f"{INJECT_HEADER}\n{goal_text}\n{INJECT_FOOTER}" + if extra_prompt: + return f"{block}\n\n{extra_prompt}" + return block + + +def cmd_run(args: argparse.Namespace) -> int: + home = state_home() + key = project_key(args.project) + state = load_state(goal_path(home, key)) + if state is None or state.get("current") is None: + print(f"codex-goal: no goal set for {key}. Run: codex_goal.py set ", file=sys.stderr) + return 2 + + goal = state["current"] + passthrough = list(args.codex_args) + extra_prompt = None + if passthrough and not passthrough[-1].startswith("-"): + # Last bare token (if any) is the user's extra instruction for this turn; + # everything else is forwarded to `codex exec` untouched. + extra_prompt = passthrough.pop() + + prompt = build_prompt(goal["text"], extra_prompt) + + argv = [args.codex_bin, "exec"] + mode = "new" + session_arg = None + if args.resume or args.last: + argv.append("resume") + mode = "resume" + if args.last: + argv.append("--last") + session_arg = "last" + else: + argv.append(args.resume) + session_arg = args.resume + argv.extend(passthrough) + argv.append(prompt) + + if args.dry_run: + print(json.dumps({"argv": argv, "mode": mode}, indent=2)) + return 0 + + record = { + "ts": now_iso(), + "mode": mode, + "session_arg": session_arg, + "goal_id": goal["goal_id"], + "goal_preview": goal["text"][:120], + "argv": argv, + } + + try: + proc = subprocess.Popen(argv, start_new_session=True) + except FileNotFoundError: + print(f"codex-goal: codex binary not found: {args.codex_bin}", file=sys.stderr) + record["exit_code"] = 127 + append_jsonl(runlog_path(home, key), record) + return 127 + + def forward_signal(signum, _frame): + with contextlib.suppress(ProcessLookupError): + os.killpg(proc.pid, signum) + + old_sigint = signal.signal(signal.SIGINT, forward_signal) + old_sigterm = signal.signal(signal.SIGTERM, forward_signal) + try: + exit_code = proc.wait() + finally: + signal.signal(signal.SIGINT, old_sigint) + signal.signal(signal.SIGTERM, old_sigterm) + + record["exit_code"] = exit_code + append_jsonl(runlog_path(home, key), record) + return exit_code + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog="codex_goal.py", description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + common = argparse.ArgumentParser(add_help=False) + common.add_argument("-C", "--project", default=os.getcwd(), help="project directory (default: cwd)") + + p_set = sub.add_parser("set", parents=[common], help="save/replace the goal for this project") + p_set.add_argument("goal", nargs="+", help="goal text") + p_set.set_defaults(func=cmd_set) + + p_show = sub.add_parser("show", parents=[common], help="print the current goal") + p_show.add_argument("--json", action="store_true") + p_show.add_argument("--history", action="store_true") + p_show.set_defaults(func=cmd_show) + + p_run = sub.add_parser("run", parents=[common], help="spawn `codex exec` with the goal injected") + p_run.add_argument("--codex-bin", default=os.environ.get("CODEX_GOAL_BIN", "codex")) + resume_group = p_run.add_mutually_exclusive_group() + resume_group.add_argument("--resume", metavar="SESSION_ID", help="resume this session id") + resume_group.add_argument("--last", action="store_true", help="resume the most recent session") + p_run.add_argument("--dry-run", action="store_true", help="print the argv that would run, do not spawn") + p_run.add_argument("codex_args", nargs=argparse.REMAINDER, metavar="-- CODEX_ARGS...") + p_run.set_defaults(func=cmd_run) + + args = parser.parse_args(argv) + if args.command == "run" and args.codex_args and args.codex_args[0] == "--": + args.codex_args = args.codex_args[1:] + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/ventures/codex-goal/demo.py b/ventures/codex-goal/demo.py new file mode 100644 index 0000000..b587d22 --- /dev/null +++ b/ventures/codex-goal/demo.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Recorded, runnable demo for codex-goal. + +Uses a disposable project dir, a disposable $CODEX_GOAL_HOME, and the +deterministic tests/fake_codex.py stand-in -- no network calls, no real +Codex account/auth needed, nothing billed. It demonstrates: + + 1. `set` then `show` (round trip). + 2. Run 1: a fresh `codex exec`, goal injected, fake run exits 0. + 3. Run 2: `--last` resume, goal injected again -- same goal survives + across the two separate process invocations (the thing upstream + closed as not planned). + 4. A run that crashes (killed mid-flight): the stored goal is untouched + afterward, proving the wrapper never half-writes state. + +Prints each command and its output, so this file doubles as demo.txt's +generator: `python3 demo.py > demo.txt`. +""" +import json +import os +import shlex +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL = os.path.join(HERE, "codex_goal.py") +FAKE_CODEX = os.path.join(HERE, "tests", "fake_codex.py") + + +def display(argv, project): + pretty = [] + for a in argv: + if a == sys.executable: + pretty.append("python3") + elif a == TOOL: + pretty.append("codex_goal.py") + elif a == FAKE_CODEX: + pretty.append("tests/fake_codex.py") + elif a == project: + pretty.append("$PROJECT") + else: + pretty.append(a) + return " ".join(shlex.quote(a) for a in pretty) + + +def run(argv, env, label, project): + print(f"$ {display(argv, project)}") + proc = subprocess.run(argv, env=env, capture_output=True, text=True) + out = (proc.stdout + proc.stderr).rstrip() + if out: + print(out) + print(f"[exit {proc.returncode}] {label}\n") + return proc + + +def main() -> int: + os.chmod(FAKE_CODEX, 0o755) + with tempfile.TemporaryDirectory() as home, tempfile.TemporaryDirectory() as parent: + project = os.path.join(parent, "myapp") + os.makedirs(project) + env = dict(os.environ) + env["CODEX_GOAL_HOME"] = home + env.pop("FAKE_CODEX_SLEEP", None) + + print("# 1. set + show") + run([sys.executable, TOOL, "set", "-C", project, + "Ship codex-goal's two-run persistence demo"], env, "goal saved", project) + run([sys.executable, TOOL, "show", "-C", project], env, "goal shown", project) + + print("# 2. run 1 -- fresh session (fake codex, exit 0)") + env1 = dict(env, FAKE_CODEX_EXIT="0") + run([sys.executable, TOOL, "run", "-C", project, + "--codex-bin", FAKE_CODEX], env1, "run 1 done", project) + + print("# 3. run 2 -- resume --last, in a *new* process, goal still there") + run([sys.executable, TOOL, "run", "-C", project, "--last", + "--codex-bin", FAKE_CODEX], env1, "run 2 done", project) + + print("# 4. crash mid-run: kill -9 the wrapper while codex is 'working'") + env2 = dict(env, FAKE_CODEX_SLEEP="3") + proc = subprocess.Popen( + [sys.executable, TOOL, "run", "-C", project, "--codex-bin", FAKE_CODEX], + env=env2, + ) + import time + time.sleep(0.4) + proc.kill() + proc.wait() + print(f"$ (background run) ; kill -9 {proc.pid}\n[killed] simulated crash\n") + + print("# 5. show again -- goal survived the crash, byte for byte") + show = run([sys.executable, TOOL, "show", "-C", project, "--json"], + env, "goal after crash", project) + payload = json.loads(show.stdout) + assert payload["current"]["text"] == "Ship codex-goal's two-run persistence demo", ( + "goal did not survive the crash" + ) + print("ASSERTION PASSED: goal text identical before and after the crash.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ventures/codex-goal/demo.txt b/ventures/codex-goal/demo.txt new file mode 100644 index 0000000..d4340b0 --- /dev/null +++ b/ventures/codex-goal/demo.txt @@ -0,0 +1,38 @@ +# 1. set + show +$ python3 codex_goal.py set -C '$PROJECT' 'Ship codex-goal'"'"'s two-run persistence demo' +codex-goal: saved goal g-a2c8350ac309 for myapp-58cda05ecfff4ec2 +[exit 0] goal saved + +$ python3 codex_goal.py show -C '$PROJECT' +project myapp-58cda05ecfff4ec2 +goal_id g-a2c8350ac309 +updated 2026-10-10T20:16:17Z +text Ship codex-goal's two-run persistence demo +[exit 0] goal shown + +# 2. run 1 -- fresh session (fake codex, exit 0) +$ python3 codex_goal.py run -C '$PROJECT' --codex-bin tests/fake_codex.py +[exit 0] run 1 done + +# 3. run 2 -- resume --last, in a *new* process, goal still there +$ python3 codex_goal.py run -C '$PROJECT' --last --codex-bin tests/fake_codex.py +[exit 0] run 2 done + +# 4. crash mid-run: kill -9 the wrapper while codex is 'working' +$ (background run) ; kill -9 35478 +[killed] simulated crash + +# 5. show again -- goal survived the crash, byte for byte +$ python3 codex_goal.py show -C '$PROJECT' --json +{ + "current": { + "created_at": "2026-10-10T20:16:17Z", + "goal_id": "g-a2c8350ac309", + "text": "Ship codex-goal's two-run persistence demo", + "updated_at": "2026-10-10T20:16:17Z" + }, + "project_key": "myapp-58cda05ecfff4ec2" +} +[exit 0] goal after crash + +ASSERTION PASSED: goal text identical before and after the crash. diff --git a/ventures/codex-goal/site/README.md b/ventures/codex-goal/site/README.md new file mode 100644 index 0000000..62e67f5 --- /dev/null +++ b/ventures/codex-goal/site/README.md @@ -0,0 +1,134 @@ +# 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/.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). diff --git a/ventures/codex-goal/site/codex_goal.py b/ventures/codex-goal/site/codex_goal.py new file mode 100755 index 0000000..7983505 --- /dev/null +++ b/ventures/codex-goal/site/codex_goal.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +"""codex-goal: persist a per-project objective across `codex exec` sessions. + +External memory, not native Codex state: it stores the goal outside the +directory Codex is allowed to write to, and injects it as plain user-context +text on every `set`/`show`/`run` the human runs. Codex itself never sees a +"mark complete" affordance -- only `set` can change the stored goal, and +`set` is never invoked by codex-goal's own `run` command. + +Usage: + codex_goal.py set [-C DIR] GOAL_TEXT... + codex_goal.py show [-C DIR] [--json] [--history] + codex_goal.py run [-C DIR] [--codex-bin PATH] [--resume ID | --last] + [--dry-run] [-- CODEX_ARGS... [PROMPT]] +""" +from __future__ import annotations + +import argparse +import contextlib +import hashlib +import json +import os +import signal +import subprocess +import sys +import tempfile +import time +import uuid + +try: + import fcntl +except ImportError: # pragma: no cover - POSIX only, matches board brief hosts + fcntl = None + +SCHEMA_VERSION = 1 +MAX_GOAL_CHARS = 4000 +MAX_HISTORY = 50 +ENV_HOME = "CODEX_GOAL_HOME" + + +def state_home() -> str: + override = os.environ.get(ENV_HOME) + if override: + return override + return os.path.join(os.path.expanduser("~"), ".codex-goal") + + +def project_key(project_dir: str) -> str: + real = os.path.realpath(project_dir) + digest = hashlib.sha256(real.encode("utf-8")).hexdigest()[:16] + base = os.path.basename(real.rstrip("/")) or "root" + safe_base = "".join(c if c.isalnum() or c in "-_." else "-" for c in base) + return f"{safe_base}-{digest}" + + +def goal_path(home: str, key: str) -> str: + return os.path.join(home, "goals", f"{key}.json") + + +def runlog_path(home: str, key: str) -> str: + return os.path.join(home, "runs", f"{key}.jsonl") + + +def lock_path(home: str, key: str) -> str: + return os.path.join(home, "locks", f"{key}.lock") + + +@contextlib.contextmanager +def locked(home: str, key: str): + """Exclusive file lock guarding read-modify-write of one project's state.""" + path = lock_path(home, key) + os.makedirs(os.path.dirname(path), exist_ok=True) + fh = open(path, "a+") + try: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + yield + finally: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_UN) + fh.close() + + +def atomic_write_json(path: str, payload: dict) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + fd, tmp = tempfile.mkstemp(prefix=".tmp-", dir=os.path.dirname(path)) + try: + with os.fdopen(fd, "w") as fh: + json.dump(payload, fh, indent=2, sort_keys=True) + fh.write("\n") + fh.flush() + os.fsync(fh.fileno()) + os.replace(tmp, path) # atomic on POSIX: a reader never sees a partial file + except Exception: + with contextlib.suppress(OSError): + os.remove(tmp) + raise + + +def append_jsonl(path: str, record: dict) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + line = json.dumps(record, sort_keys=True) + "\n" + with open(path, "a") as fh: + fh.write(line) + fh.flush() + os.fsync(fh.fileno()) + + +def now_iso() -> str: + return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + + +def load_state(path: str) -> dict | None: + if not os.path.exists(path): + return None + with open(path) as fh: + return json.load(fh) + + +def cmd_set(args: argparse.Namespace) -> int: + text = " ".join(args.goal).strip() + if not text: + print("codex-goal: refusing an empty goal", file=sys.stderr) + return 2 + if len(text) > MAX_GOAL_CHARS: + print(f"codex-goal: goal exceeds {MAX_GOAL_CHARS} characters", file=sys.stderr) + return 2 + + home = state_home() + key = project_key(args.project) + path = goal_path(home, key) + + with locked(home, key): + state = load_state(path) + ts = now_iso() + if state is None: + state = { + "schema_version": SCHEMA_VERSION, + "project": {"path": os.path.realpath(args.project), "key": key}, + "current": None, + "history": [], + } + if state["current"] is not None: + state["history"].append(state["current"]) + state["history"] = state["history"][-MAX_HISTORY:] + state["current"] = { + "goal_id": f"g-{uuid.uuid4().hex[:12]}", + "text": text, + "created_at": ts, + "updated_at": ts, + } + atomic_write_json(path, state) + + print(f"codex-goal: saved goal {state['current']['goal_id']} for {key}") + return 0 + + +def cmd_show(args: argparse.Namespace) -> int: + home = state_home() + key = project_key(args.project) + state = load_state(goal_path(home, key)) + + if state is None or state.get("current") is None: + if args.json: + print(json.dumps({"project_key": key, "current": None})) + else: + print(f"codex-goal: no goal set for {key}. Run: codex_goal.py set ") + return 1 + + if args.json: + payload = {"project_key": key, "current": state["current"]} + if args.history: + payload["history"] = state["history"] + print(json.dumps(payload, indent=2, sort_keys=True)) + else: + cur = state["current"] + print(f"project {key}") + print(f"goal_id {cur['goal_id']}") + print(f"updated {cur['updated_at']}") + print(f"text {cur['text']}") + if args.history and state["history"]: + print("\nhistory (oldest first):") + for h in state["history"]: + print(f" - [{h['updated_at']}] {h['goal_id']}: {h['text']}") + return 0 + + +INJECT_HEADER = "[codex-goal] Persisted objective for this project (external memory, injected by a wrapper -- not native Codex state):" +INJECT_FOOTER = "[/codex-goal] Continue working toward the objective above unless the user says otherwise." + + +def build_prompt(goal_text: str, extra_prompt: str | None) -> str: + block = f"{INJECT_HEADER}\n{goal_text}\n{INJECT_FOOTER}" + if extra_prompt: + return f"{block}\n\n{extra_prompt}" + return block + + +def cmd_run(args: argparse.Namespace) -> int: + home = state_home() + key = project_key(args.project) + state = load_state(goal_path(home, key)) + if state is None or state.get("current") is None: + print(f"codex-goal: no goal set for {key}. Run: codex_goal.py set ", file=sys.stderr) + return 2 + + goal = state["current"] + passthrough = list(args.codex_args) + extra_prompt = None + if passthrough and not passthrough[-1].startswith("-"): + # Last bare token (if any) is the user's extra instruction for this turn; + # everything else is forwarded to `codex exec` untouched. + extra_prompt = passthrough.pop() + + prompt = build_prompt(goal["text"], extra_prompt) + + argv = [args.codex_bin, "exec"] + mode = "new" + session_arg = None + if args.resume or args.last: + argv.append("resume") + mode = "resume" + if args.last: + argv.append("--last") + session_arg = "last" + else: + argv.append(args.resume) + session_arg = args.resume + argv.extend(passthrough) + argv.append(prompt) + + if args.dry_run: + print(json.dumps({"argv": argv, "mode": mode}, indent=2)) + return 0 + + record = { + "ts": now_iso(), + "mode": mode, + "session_arg": session_arg, + "goal_id": goal["goal_id"], + "goal_preview": goal["text"][:120], + "argv": argv, + } + + try: + proc = subprocess.Popen(argv, start_new_session=True) + except FileNotFoundError: + print(f"codex-goal: codex binary not found: {args.codex_bin}", file=sys.stderr) + record["exit_code"] = 127 + append_jsonl(runlog_path(home, key), record) + return 127 + + def forward_signal(signum, _frame): + with contextlib.suppress(ProcessLookupError): + os.killpg(proc.pid, signum) + + old_sigint = signal.signal(signal.SIGINT, forward_signal) + old_sigterm = signal.signal(signal.SIGTERM, forward_signal) + try: + exit_code = proc.wait() + finally: + signal.signal(signal.SIGINT, old_sigint) + signal.signal(signal.SIGTERM, old_sigterm) + + record["exit_code"] = exit_code + append_jsonl(runlog_path(home, key), record) + return exit_code + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog="codex_goal.py", description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + common = argparse.ArgumentParser(add_help=False) + common.add_argument("-C", "--project", default=os.getcwd(), help="project directory (default: cwd)") + + p_set = sub.add_parser("set", parents=[common], help="save/replace the goal for this project") + p_set.add_argument("goal", nargs="+", help="goal text") + p_set.set_defaults(func=cmd_set) + + p_show = sub.add_parser("show", parents=[common], help="print the current goal") + p_show.add_argument("--json", action="store_true") + p_show.add_argument("--history", action="store_true") + p_show.set_defaults(func=cmd_show) + + p_run = sub.add_parser("run", parents=[common], help="spawn `codex exec` with the goal injected") + p_run.add_argument("--codex-bin", default=os.environ.get("CODEX_GOAL_BIN", "codex")) + resume_group = p_run.add_mutually_exclusive_group() + resume_group.add_argument("--resume", metavar="SESSION_ID", help="resume this session id") + resume_group.add_argument("--last", action="store_true", help="resume the most recent session") + p_run.add_argument("--dry-run", action="store_true", help="print the argv that would run, do not spawn") + p_run.add_argument("codex_args", nargs=argparse.REMAINDER, metavar="-- CODEX_ARGS...") + p_run.set_defaults(func=cmd_run) + + args = parser.parse_args(argv) + if args.command == "run" and args.codex_args and args.codex_args[0] == "--": + args.codex_args = args.codex_args[1:] + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/ventures/codex-goal/site/demo.txt b/ventures/codex-goal/site/demo.txt new file mode 100644 index 0000000..d4340b0 --- /dev/null +++ b/ventures/codex-goal/site/demo.txt @@ -0,0 +1,38 @@ +# 1. set + show +$ python3 codex_goal.py set -C '$PROJECT' 'Ship codex-goal'"'"'s two-run persistence demo' +codex-goal: saved goal g-a2c8350ac309 for myapp-58cda05ecfff4ec2 +[exit 0] goal saved + +$ python3 codex_goal.py show -C '$PROJECT' +project myapp-58cda05ecfff4ec2 +goal_id g-a2c8350ac309 +updated 2026-10-10T20:16:17Z +text Ship codex-goal's two-run persistence demo +[exit 0] goal shown + +# 2. run 1 -- fresh session (fake codex, exit 0) +$ python3 codex_goal.py run -C '$PROJECT' --codex-bin tests/fake_codex.py +[exit 0] run 1 done + +# 3. run 2 -- resume --last, in a *new* process, goal still there +$ python3 codex_goal.py run -C '$PROJECT' --last --codex-bin tests/fake_codex.py +[exit 0] run 2 done + +# 4. crash mid-run: kill -9 the wrapper while codex is 'working' +$ (background run) ; kill -9 35478 +[killed] simulated crash + +# 5. show again -- goal survived the crash, byte for byte +$ python3 codex_goal.py show -C '$PROJECT' --json +{ + "current": { + "created_at": "2026-10-10T20:16:17Z", + "goal_id": "g-a2c8350ac309", + "text": "Ship codex-goal's two-run persistence demo", + "updated_at": "2026-10-10T20:16:17Z" + }, + "project_key": "myapp-58cda05ecfff4ec2" +} +[exit 0] goal after crash + +ASSERTION PASSED: goal text identical before and after the crash. diff --git a/ventures/codex-goal/site/fonts/DSEG7Classic-Bold.woff2 b/ventures/codex-goal/site/fonts/DSEG7Classic-Bold.woff2 new file mode 100644 index 0000000..558eec4 Binary files /dev/null and b/ventures/codex-goal/site/fonts/DSEG7Classic-Bold.woff2 differ diff --git a/ventures/codex-goal/site/fonts/OFL.txt b/ventures/codex-goal/site/fonts/OFL.txt new file mode 100644 index 0000000..ad4c0bf --- /dev/null +++ b/ventures/codex-goal/site/fonts/OFL.txt @@ -0,0 +1,95 @@ +Copyright (c) 2017, keshikan (http://www.keshikan.net), +with Reserved Font Name "DSEG". + + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/ventures/codex-goal/site/index.html b/ventures/codex-goal/site/index.html new file mode 100644 index 0000000..0a3a350 --- /dev/null +++ b/ventures/codex-goal/site/index.html @@ -0,0 +1,345 @@ + + + + + + +codex-goal · notplanned + + + +
+ +
+
NOTPLANNED ARCADE
+
VENTURE 004 · LIVE
+
+ +
+
+
upstream call: openai/codex#26966 · closed · not planned
+

don't let the agent forget the goal.

+

codex exec resumes a transcript but never restates why. codex‑goal is a small wrapper that stores one objective per project outside Codex's own workspace, and hands it back as plain context on every new or resumed run — built anyway after upstream closed the request.

+ +
+
02
codex exec runs
+
01
goal, both runs
+
100
% intact after kill -9
+
+ +
+
★ join the roster — be first when hosted tiers ship
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+
+
✓ ON THE ROSTER — thanks, you're in.We'll only email you about codex-goal. Never shared.
+

One line on your email: used only to reach you about codex-goal, never sold, never shared. The CLI below stays free no matter what you pick.

+
+
+ +
+

the matchup

+

Same two commands, two sessions apart. One remembers the assignment; one doesn't.

+
+
+
Stock codex execLOST
+
$ codex exec "migrate billing to new schema"
+… session ends, summarized …
+
+$ codex exec resume --last "continue"
+> continuing. what's the task again?
+  (transcript survived. the point of it didn't.)
+

Resume brings back messages, not the objective that framed them.

+
+
+
codex‑goal runWON
+
$ codex_goal.py set "migrate billing to new schema"
+$ codex_goal.py run
+> [codex-goal] persisted objective: migrate billing…
+
+… session ends …
+
+$ codex_goal.py run --last
+> [codex-goal] persisted objective: migrate billing…
+  (re-injected before Codex sees anything else.)
+

Every new or resumed run gets the same goal text, re-stated, every time.

+
+
+
+ +
+

instant replay

+

Real, runnable output from demo.py — a disposable project dir, a disposable $CODEX_GOAL_HOME, and a deterministic stand-in for the real codex binary (no network, no account, nothing billed). Two separate process runs see the same goal; a kill -9 mid-run doesn't touch it.

+
+
demo.py — full output in demo.txt
+
$ python3 codex_goal.py set -C $PROJECT 'Ship codex-goal's two-run persistence demo'
+codex-goal: saved goal g-a2c8350ac309 for myapp-58cda05ecfff4ec2
+[exit 0] goal saved
+
+$ python3 codex_goal.py run -C $PROJECT --codex-bin tests/fake_codex.py
+[exit 0] run 1 done              ← fresh session, goal injected
+
+$ python3 codex_goal.py run -C $PROJECT --last --codex-bin tests/fake_codex.py
+[exit 0] run 2 done              ← resumed, SAME goal injected again
+
+$ (background run) ; kill -9 35478
+[killed] simulated crash          ← mid-run, hard kill
+
+$ python3 codex_goal.py show -C $PROJECT --json
+{
+  "current": {
+    "text": "Ship codex-goal's two-run persistence demo",
+    "goal_id": "g-a2c8350ac309"
+  }
+}
+ASSERTION PASSED: goal text identical before and after the crash.
+
+

Full transcript: demo.txt · run it yourself: README

+
+ +
+

how it scores

+
    +
  • 01 Goal state lives under ~/.codex-goal — outside the directory Codex's own sandbox lets it write to. set is a human command; the wrapper never calls it on Codex's behalf.
  • +
  • 02 Every set is versioned: the previous goal moves to a capped history instead of being silently overwritten.
  • +
  • 03 Writes are atomic (temp file + rename) under an exclusive file lock — a crash mid-write can't leave torn JSON.
  • +
  • 04 run never edits the goal. It only appends one line to a separate run-log, after the child exits, with the real exit code.
  • +
  • 05 Codex is spawned via an argv list, never a shell string — a goal containing ; rm -rf / # is one opaque argument, not a command.
  • +
  • 06 Ctrl‑C / SIGTERM sent to the wrapper is forwarded to the Codex child, so cancellation behaves exactly like calling codex directly. The wrapper's exit code is the child's.
  • +
+
+ +
+

game passes

+

Free tier works offline, forever. Paid tiers are founding-price early access — there is no checkout yet.

+
+
+
Free
+
$0
+
anyone with their own Codex auth
+
  • Full CLI, source, tests, demo
  • Unlimited local projects
  • No account, no telemetry
+
forever free
+
+
+
Early Access
+
$6/mo
+
solo devs juggling several agent projects
+
  • Hosted goal + run-log viewer
  • Cross-machine goal sync
  • Email digest on run failures
+
founding price · waitlist, not billed yet
+
+
+
Team
+
$24/mo
+
teams sharing Codex objectives across a repo
+
  • Shared goals, per-teammate run history
  • Slack alert when a run fails
  • Audit log export
+
founding price · waitlist, not billed yet
+
+
+
+ +
+

rules of the game

+
    +
  • 1 goal One objective per project at a time. Not a task list, not a scheduler, not a replacement for your issue tracker.
  • +
  • no force The injected text is plain, visible user context — it carries no special authority inside Codex. A model can still choose to ignore it, same as any other instruction.
  • +
  • no grading This only guarantees the goal is faithfully re-stated every run. It does not inspect diffs or grade progress — pair it with finishline-supervisor for that.
  • +
  • plain json $CODEX_GOAL_HOME is ordinary local storage with your file permissions, no encryption. Don't put secrets in goal text.
  • +
  • local lock Locking is advisory and local-filesystem only — it doesn't coordinate over NFS/SMB or across machines.
  • +
  • your flags run forwards whatever codex exec flags you give it, including dangerous ones if you ask for them. This wrapper adds or removes none of Codex's own safety behavior.
  • +
+
+
+ + +
+ + + diff --git a/ventures/codex-goal/tests/fake_codex.py b/ventures/codex-goal/tests/fake_codex.py new file mode 100755 index 0000000..38669b6 --- /dev/null +++ b/ventures/codex-goal/tests/fake_codex.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Deterministic stand-in for the real `codex` binary, used only by tests/demo. + +Records the exact argv it received (proof there was no shell interpolation), +optionally sleeps so a test can send SIGINT/SIGTERM and check cancellation +is forwarded, then exits with a configurable code. +""" +import json +import os +import signal +import sys +import time + + +def main() -> int: + record_path = os.environ.get("FAKE_CODEX_RECORD") + if record_path: + with open(record_path, "w") as fh: + json.dump({"argv": sys.argv[1:], "pid": os.getpid()}, fh) + + sleep_s = float(os.environ.get("FAKE_CODEX_SLEEP", "0")) + if sleep_s: + caught = {} + + def handler(signum, _frame): + caught["signal"] = signum + + signal.signal(signal.SIGINT, handler) + signal.signal(signal.SIGTERM, handler) + end = time.time() + sleep_s + while time.time() < end and "signal" not in caught: + time.sleep(0.02) + if "signal" in caught: + return 130 + + return int(os.environ.get("FAKE_CODEX_EXIT", "0")) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ventures/codex-goal/tests/test_codex_goal.py b/ventures/codex-goal/tests/test_codex_goal.py new file mode 100644 index 0000000..123ac3e --- /dev/null +++ b/ventures/codex-goal/tests/test_codex_goal.py @@ -0,0 +1,248 @@ +import contextlib +import io +import json +import os +import signal +import subprocess +import sys +import tempfile +import time +import unittest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT) +FAKE_CODEX = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fake_codex.py") + +import codex_goal # noqa: E402 + + +class TempHome(unittest.TestCase): + def setUp(self): + self._home_dir = tempfile.TemporaryDirectory() + self._project_dir = tempfile.TemporaryDirectory() + os.environ["CODEX_GOAL_HOME"] = self._home_dir.name + os.chmod(FAKE_CODEX, 0o755) + + def tearDown(self): + os.environ.pop("CODEX_GOAL_HOME", None) + self._home_dir.cleanup() + self._project_dir.cleanup() + + def run_cli(self, *argv): + return codex_goal.main(list(argv)) + + @property + def project(self): + return self._project_dir.name + + +class SetShowTests(TempHome): + def test_set_then_show_roundtrip(self): + rc = self.run_cli("set", "-C", self.project, "Ship", "the", "thing") + self.assertEqual(rc, 0) + + key = codex_goal.project_key(self.project) + state = codex_goal.load_state(codex_goal.goal_path(codex_goal.state_home(), key)) + self.assertEqual(state["current"]["text"], "Ship the thing") + self.assertEqual(state["history"], []) + + def test_show_without_goal_fails_clearly(self): + rc = self.run_cli("show", "-C", self.project) + self.assertEqual(rc, 1) + + def test_second_set_pushes_history(self): + self.run_cli("set", "-C", self.project, "first goal") + self.run_cli("set", "-C", self.project, "second goal") + + key = codex_goal.project_key(self.project) + state = codex_goal.load_state(codex_goal.goal_path(codex_goal.state_home(), key)) + self.assertEqual(state["current"]["text"], "second goal") + self.assertEqual(len(state["history"]), 1) + self.assertEqual(state["history"][0]["text"], "first goal") + + def test_empty_goal_rejected(self): + rc = self.run_cli("set", "-C", self.project, " ") + self.assertEqual(rc, 2) + + def test_two_projects_get_independent_state(self): + with tempfile.TemporaryDirectory() as other: + self.run_cli("set", "-C", self.project, "goal A") + self.run_cli("set", "-C", other, "goal B") + + key_a = codex_goal.project_key(self.project) + key_b = codex_goal.project_key(other) + self.assertNotEqual(key_a, key_b) + + state_a = codex_goal.load_state(codex_goal.goal_path(codex_goal.state_home(), key_a)) + state_b = codex_goal.load_state(codex_goal.goal_path(codex_goal.state_home(), key_b)) + self.assertEqual(state_a["current"]["text"], "goal A") + self.assertEqual(state_b["current"]["text"], "goal B") + + def test_state_lives_outside_project_dir(self): + self.run_cli("set", "-C", self.project, "stay external") + key = codex_goal.project_key(self.project) + path = codex_goal.goal_path(codex_goal.state_home(), key) + self.assertFalse(path.startswith(os.path.realpath(self.project))) + self.assertTrue(path.startswith(self._home_dir.name)) + + +class RunDryRunTests(TempHome): + def test_run_without_goal_errors_and_does_not_spawn(self): + rc = self.run_cli("run", "-C", self.project, "--dry-run") + self.assertEqual(rc, 2) + + def test_dry_run_new_session_injects_goal(self): + self.run_cli("set", "-C", self.project, "win the game") + rc = self.run_cli("run", "-C", self.project, "--codex-bin", "codex", "--dry-run") + self.assertEqual(rc, 0) + + def test_dry_run_resume_last_builds_resume_argv(self): + self.run_cli("set", "-C", self.project, "resume goal") + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + rc = self.run_cli("run", "-C", self.project, "--last", "--dry-run") + self.assertEqual(rc, 0) + payload = json.loads(buf.getvalue()) + self.assertEqual(payload["mode"], "resume") + self.assertIn("resume", payload["argv"]) + self.assertIn("--last", payload["argv"]) + + def test_shell_metacharacters_survive_as_one_argv_item(self): + goal = "ship it; rm -rf / #not a shell command" + self.run_cli("set", "-C", self.project, goal) + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + self.run_cli("run", "-C", self.project, "--dry-run") + payload = json.loads(buf.getvalue()) + self.assertIn(goal, payload["argv"][-1]) + # the dangerous text is one argv element, never concatenated into a command string + self.assertEqual(sum(goal in item for item in payload["argv"]), 1) + + +class RunSpawnTests(TempHome): + def setUp(self): + super().setUp() + self._record = tempfile.NamedTemporaryFile(delete=False, suffix=".json") + self._record.close() + os.environ["FAKE_CODEX_RECORD"] = self._record.name + + def tearDown(self): + os.environ.pop("FAKE_CODEX_RECORD", None) + os.environ.pop("FAKE_CODEX_EXIT", None) + os.environ.pop("FAKE_CODEX_SLEEP", None) + with contextlib.suppress(OSError): + os.remove(self._record.name) + super().tearDown() + + def test_run_spawns_fake_codex_injects_goal_and_logs(self): + self.run_cli("set", "-C", self.project, "beat the deadline") + os.environ["FAKE_CODEX_EXIT"] = "0" + rc = self.run_cli("run", "-C", self.project, "--codex-bin", FAKE_CODEX) + self.assertEqual(rc, 0) + + with open(self._record.name) as fh: + recorded = json.load(fh) + self.assertEqual(recorded["argv"][0], "exec") + self.assertIn("beat the deadline", recorded["argv"][-1]) + self.assertIn(codex_goal.INJECT_HEADER, recorded["argv"][-1]) + + key = codex_goal.project_key(self.project) + log_path = codex_goal.runlog_path(codex_goal.state_home(), key) + with open(log_path) as fh: + lines = [json.loads(l) for l in fh if l.strip()] + self.assertEqual(len(lines), 1) + self.assertEqual(lines[0]["mode"], "new") + self.assertEqual(lines[0]["exit_code"], 0) + + def test_run_preserves_nonzero_exit_status(self): + self.run_cli("set", "-C", self.project, "goal") + os.environ["FAKE_CODEX_EXIT"] = "17" + rc = self.run_cli("run", "-C", self.project, "--codex-bin", FAKE_CODEX) + self.assertEqual(rc, 17) + + def test_two_runs_both_see_the_same_persisted_goal(self): + self.run_cli("set", "-C", self.project, "persist across sessions") + os.environ["FAKE_CODEX_EXIT"] = "0" + + self.run_cli("run", "-C", self.project, "--codex-bin", FAKE_CODEX) + with open(self._record.name) as fh: + first = json.load(fh) + + self.run_cli("run", "-C", self.project, "--last", "--codex-bin", FAKE_CODEX) + with open(self._record.name) as fh: + second = json.load(fh) + + self.assertIn("persist across sessions", first["argv"][-1]) + self.assertIn("persist across sessions", second["argv"][-1]) + self.assertIn("resume", second["argv"]) + + key = codex_goal.project_key(self.project) + log_path = codex_goal.runlog_path(codex_goal.state_home(), key) + with open(log_path) as fh: + lines = [json.loads(l) for l in fh if l.strip()] + self.assertEqual(len(lines), 2) + self.assertEqual(lines[0]["mode"], "new") + self.assertEqual(lines[1]["mode"], "resume") + + def test_failed_run_does_not_touch_the_goal(self): + self.run_cli("set", "-C", self.project, "do not corrupt me") + os.environ["FAKE_CODEX_EXIT"] = "1" + self.run_cli("run", "-C", self.project, "--codex-bin", FAKE_CODEX) + + key = codex_goal.project_key(self.project) + state = codex_goal.load_state(codex_goal.goal_path(codex_goal.state_home(), key)) + self.assertEqual(state["current"]["text"], "do not corrupt me") + + def test_sigint_is_forwarded_and_cancels_child(self): + self.run_cli("set", "-C", self.project, "long running") + os.environ["FAKE_CODEX_SLEEP"] = "5" + + child_argv = [ + sys.executable, + os.path.join(ROOT, "codex_goal.py"), + "run", + "-C", + self.project, + "--codex-bin", + FAKE_CODEX, + ] + env = dict(os.environ) + proc = subprocess.Popen(child_argv, env=env) + time.sleep(0.5) + proc.send_signal(signal.SIGINT) + rc = proc.wait(timeout=5) + self.assertEqual(rc, 130) + + +class AtomicWriteTests(TempHome): + def test_failed_write_leaves_existing_state_intact(self): + self.run_cli("set", "-C", self.project, "original goal") + key = codex_goal.project_key(self.project) + path = codex_goal.goal_path(codex_goal.state_home(), key) + with open(path) as fh: + before = fh.read() + + with self.assertRaises(RuntimeError): + with codex_goal.locked(codex_goal.state_home(), key): + state = codex_goal.load_state(path) + state["current"]["text"] = "corrupted" + + real_dump = json.dump + + def exploding_dump(*a, **k): + raise RuntimeError("simulated crash mid-write") + + json.dump = exploding_dump + try: + codex_goal.atomic_write_json(path, state) + finally: + json.dump = real_dump + + with open(path) as fh: + after = fh.read() + self.assertEqual(before, after) + self.assertNotIn("corrupted", after) + + +if __name__ == "__main__": + unittest.main()