Skip to content

Commit 3fed29c

Browse files
committed
feat: make loop autonomous with persistent goals and robust codex binary resolution
1 parent d4cc594 commit 3fed29c

7 files changed

Lines changed: 426 additions & 151 deletions

File tree

README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# codex-self-iter
22

3-
`codex-self-iter` is a plugin-style repository that runs a continuous autonomous loop for coding tasks:
3+
`codex-self-iter` is a plugin-style repository that runs a continuous autonomous goal loop:
44

55
1. Read `TASK.md` + `plan.md`
6-
2. Generate one small next step
7-
3. Execute the step via agent command
8-
4. Review result and decide whether to continue
9-
5. Repeat until no meaningful next action exists or user stops it
6+
2. Execute one autonomous iteration in the repository
7+
3. Require the agent to emit a concrete `next_goal`
8+
4. Persist that goal and continue from it
9+
5. Repeat until completion/stop condition
1010

1111
## Repository Layout
1212

@@ -62,6 +62,8 @@ Or use:
6262
- `.codex-self-iter/prompts/`: planner/executor/reviewer prompts
6363
- `.codex-self-iter/logs/iterations.jsonl`: loop records
6464
- `.codex-self-iter/status.json`: last iteration state
65+
- `.codex-self-iter/runtime.json`: persisted backoff counters and window
66+
- `.codex-self-iter/loop.lock`: single-instance guard file
6567

6668
## Configuration
6769

@@ -72,6 +74,10 @@ Important fields:
7274
- `agent_command_template` must include `{prompt_file}`
7375
- `max_iterations = 0` means unbounded loop
7476
- `max_stagnation` prevents infinite cycling on identical `next_focus`
77+
- `no_change_gate` throttles no-op loops when no new git changes appear
78+
- `backoff_*` fields control exponential cooldown behavior
79+
- `lock_file_name` controls the single-instance lock path under state-dir
80+
- `completion_promise` marks completion when included in output
7581

7682
## Approval and Privilege Mode
7783

@@ -91,6 +97,20 @@ In bypass mode, wrapper uses:
9197
- `codex --dangerously-bypass-approvals-and-sandbox exec ...`
9298
This disables approvals and sandboxing entirely. Use only in externally sandboxed environments.
9399

100+
## Binary Resolution (macOS launchd/system services)
101+
102+
When running under `launchd` (or other service managers), interactive shell `PATH` is often not inherited.
103+
If `codex` is not found, set an explicit binary path:
104+
105+
```bash
106+
export CODEX_BIN=/absolute/path/to/codex
107+
```
108+
109+
The default wrapper checks in this order:
110+
1. `CODEX_BIN` (if executable)
111+
2. `PATH` via `command -v codex`
112+
3. common fallback paths (nvm/Homebrew/local bin)
113+
94114
## Development
95115

96116
Run tests:

config.example.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,21 @@ reviewer_model_hint = "strict"
2323

2424
# Number of history rows fed back to planner each round.
2525
history_tail = 8
26+
27+
# Gate tight restart loops when no new git changes are produced.
28+
no_change_gate = true
29+
30+
# Exponential backoff controls for transient errors.
31+
backoff_base_sec = 5
32+
backoff_max_sec = 300
33+
34+
# Backoff controls for low-signal outcomes.
35+
no_change_backoff_sec = 60
36+
continue_false_backoff_sec = 300
37+
planner_stop_backoff_sec = 600
38+
39+
# Single-instance lock under state-dir.
40+
lock_file_name = "loop.lock"
41+
42+
# If this token appears in agent output, the loop treats task as complete.
43+
completion_promise = "DONE"

scripts/invoke-codex-agent.sh

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
set -euo pipefail
2+
set -uo pipefail
33

44
if [[ $# -lt 2 ]]; then
55
echo "usage: $0 <prompt-file> <workspace>" >&2
@@ -8,12 +8,98 @@ fi
88

99
prompt_file="$1"
1010
workspace="$2"
11+
state_dir="${CODEX_SELF_ITER_STATE_DIR:-$workspace/.codex-self-iter}"
12+
session_file="${CODEX_SELF_ITER_SESSION_FILE:-$state_dir/codex-session-id}"
1113

12-
# Default mode: auto-confirm approval requests while keeping sandboxing.
13-
if [[ "${CODEX_SELF_ITER_AGENT_MODE:-}" == "bypass" ]]; then
14-
# Extremely dangerous: no approval prompts and no sandbox.
15-
cat "$prompt_file" | codex --dangerously-bypass-approvals-and-sandbox exec -C "$workspace" -
14+
mkdir -p "$state_dir"
15+
16+
msg_file="$(mktemp)"
17+
err_file="$(mktemp)"
18+
trap 'rm -f "$msg_file" "$err_file"' EXIT
19+
20+
session_id=""
21+
if [[ -f "$session_file" ]]; then
22+
session_id="$(tr -d '[:space:]' < "$session_file" 2>/dev/null || true)"
23+
fi
24+
25+
resolve_codex_bin() {
26+
if [[ -n "${CODEX_BIN:-}" ]] && [[ -x "${CODEX_BIN:-}" ]]; then
27+
echo "$CODEX_BIN"
28+
return 0
29+
fi
30+
if command -v codex >/dev/null 2>&1; then
31+
command -v codex
32+
return 0
33+
fi
34+
local candidates=(
35+
"$HOME/.nvm/versions/node/v24.10.0/bin/codex"
36+
"$HOME/.nvm/versions/node/v*/bin/codex"
37+
"/opt/homebrew/bin/codex"
38+
"/usr/local/bin/codex"
39+
)
40+
local c
41+
for c in "${candidates[@]}"; do
42+
for m in $c; do
43+
if [[ -x "$m" ]]; then
44+
echo "$m"
45+
return 0
46+
fi
47+
done
48+
done
49+
return 1
50+
}
51+
52+
CODEX_CMD="$(resolve_codex_bin || true)"
53+
if [[ -z "$CODEX_CMD" ]]; then
54+
cat >&2 <<'EOF'
55+
Error: codex binary not found.
56+
Tips:
57+
- Set CODEX_BIN to an absolute path (recommended for launchd/systemd)
58+
- Or ensure codex is available in PATH
59+
EOF
60+
exit 127
61+
fi
62+
63+
run_codex_new() {
64+
if [[ "${CODEX_SELF_ITER_AGENT_MODE:-}" == "bypass" ]]; then
65+
cat "$prompt_file" | "$CODEX_CMD" --dangerously-bypass-approvals-and-sandbox exec -C "$workspace" -o "$msg_file" - >/dev/null 2>"$err_file"
66+
else
67+
cat "$prompt_file" | "$CODEX_CMD" -a never exec -C "$workspace" -o "$msg_file" - >/dev/null 2>"$err_file"
68+
fi
69+
return $?
70+
}
71+
72+
run_codex_resume() {
73+
local sid="$1"
74+
if [[ "${CODEX_SELF_ITER_AGENT_MODE:-}" == "bypass" ]]; then
75+
cat "$prompt_file" | "$CODEX_CMD" --dangerously-bypass-approvals-and-sandbox exec resume "$sid" -o "$msg_file" - >/dev/null 2>"$err_file"
76+
else
77+
cat "$prompt_file" | "$CODEX_CMD" -a never exec resume "$sid" -o "$msg_file" - >/dev/null 2>"$err_file"
78+
fi
79+
return $?
80+
}
81+
82+
rc=1
83+
if [[ -n "$session_id" ]]; then
84+
run_codex_resume "$session_id"
85+
rc=$?
86+
fi
87+
88+
if [[ $rc -ne 0 ]]; then
89+
run_codex_new
90+
rc=$?
91+
fi
92+
93+
if [[ $rc -eq 0 ]]; then
94+
if [[ -z "$session_id" ]]; then
95+
sid="$(rg -o -N 'session id:\s*([0-9a-fA-F-]{36})' "$err_file" | head -n1 | awk '{print $3}' || true)"
96+
if [[ -n "$sid" ]]; then
97+
echo "$sid" > "$session_file"
98+
fi
99+
fi
100+
cat "$msg_file"
16101
else
17-
# No manual confirmation; command failures are returned directly to the model.
18-
cat "$prompt_file" | codex -a never exec -C "$workspace" -
102+
cat "$err_file"
19103
fi
104+
105+
exit $rc

src/codex_self_iter/config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ class LoopConfig:
2020
planner_model_hint: str = "balanced"
2121
reviewer_model_hint: str = "strict"
2222
history_tail: int = 8
23+
no_change_gate: bool = True
24+
backoff_base_sec: int = 5
25+
backoff_max_sec: int = 300
26+
no_change_backoff_sec: int = 60
27+
continue_false_backoff_sec: int = 300
28+
planner_stop_backoff_sec: int = 600
29+
lock_file_name: str = "loop.lock"
30+
completion_promise: str = "DONE"
2331

2432

2533
def read_text(path: Path, default: str = "") -> str:
@@ -44,4 +52,12 @@ def load_config(config_path: Path | None, default_agent_cmd: str) -> LoopConfig:
4452
planner_model_hint=str(raw.get("planner_model_hint", "balanced")),
4553
reviewer_model_hint=str(raw.get("reviewer_model_hint", "strict")),
4654
history_tail=int(raw.get("history_tail", 8)),
55+
no_change_gate=bool(raw.get("no_change_gate", True)),
56+
backoff_base_sec=int(raw.get("backoff_base_sec", 5)),
57+
backoff_max_sec=int(raw.get("backoff_max_sec", 300)),
58+
no_change_backoff_sec=int(raw.get("no_change_backoff_sec", 60)),
59+
continue_false_backoff_sec=int(raw.get("continue_false_backoff_sec", 300)),
60+
planner_stop_backoff_sec=int(raw.get("planner_stop_backoff_sec", 600)),
61+
lock_file_name=str(raw.get("lock_file_name", "loop.lock")),
62+
completion_promise=str(raw.get("completion_promise", "DONE")),
4763
)

src/codex_self_iter/prompts.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
from __future__ import annotations
22

33

4+
def autonomous_goal_prompt(
5+
task_text: str,
6+
plan_text: str,
7+
current_goal: str,
8+
history_jsonl_tail: str,
9+
model_hint: str,
10+
completion_promise: str,
11+
) -> str:
12+
return f"""You are an autonomous coding agent in an endless explore-and-improve loop.
13+
Model style hint: {model_hint}
14+
15+
Primary task:
16+
{task_text}
17+
18+
Plan context:
19+
{plan_text}
20+
21+
Current goal for this iteration:
22+
{current_goal}
23+
24+
Recent iteration history:
25+
{history_jsonl_tail}
26+
27+
You must do real work in the repository (code/tests/docs/commands), then decide the next goal.
28+
29+
Output exactly one JSON object with keys:
30+
- summary: short string, what you did this round
31+
- completed: boolean, true only if overall task is truly complete
32+
- next_goal: short string, required when completed=false
33+
- reason: short string, why next_goal is the right continuation
34+
- evidence: short string, key checks or observations
35+
- stop: boolean (true only if no meaningful next action exists)
36+
37+
Rules:
38+
1) Be self-directed: choose the most valuable next step based on actual repo state.
39+
2) Prefer measurable progress (tests, reproducibility, correctness).
40+
3) If not complete, always provide a concrete next_goal.
41+
4) If complete, include completion token "{completion_promise}" in summary.
42+
"""
43+
44+
445
def planner_prompt(task_text: str, plan_text: str, history_jsonl_tail: str, model_hint: str) -> str:
546
return f"""You are the planner in a self-iterating coding loop.
647
Model style hint: {model_hint}

0 commit comments

Comments
 (0)