codexapi 0.5.4__tar.gz → 0.5.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.5.4
3
+ Version: 0.5.5
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -73,8 +73,10 @@ echo "Say hello." | codexapi run
73
73
  ```bash
74
74
  codexapi task "Fix the failing tests." --max-iterations 5
75
75
  codexapi task -f task.yaml
76
+ codexapi task -f task.yaml -i README.md
76
77
  ```
77
78
  Progress is shown by default for `codexapi task`; use `--quiet` to suppress it.
79
+ When using `--item`, the task file must include at least one `{{item}}` placeholder.
78
80
 
79
81
  Task files default to using the standard check prompt for the task. Set `check: "None"` to skip verification.
80
82
  Use `max_iterations` in the task file to override the default attempt cap (0 means unlimited).
@@ -120,6 +122,8 @@ Run a task file across a list file:
120
122
  ```bash
121
123
  codexapi foreach list.txt task.yaml
122
124
  codexapi foreach list.txt task.yaml -n 4
125
+ codexapi foreach list.txt task.yaml --retry-failed
126
+ codexapi foreach list.txt task.yaml --retry-all
123
127
  ```
124
128
 
125
129
  ## API
@@ -59,8 +59,10 @@ echo "Say hello." | codexapi run
59
59
  ```bash
60
60
  codexapi task "Fix the failing tests." --max-iterations 5
61
61
  codexapi task -f task.yaml
62
+ codexapi task -f task.yaml -i README.md
62
63
  ```
63
64
  Progress is shown by default for `codexapi task`; use `--quiet` to suppress it.
65
+ When using `--item`, the task file must include at least one `{{item}}` placeholder.
64
66
 
65
67
  Task files default to using the standard check prompt for the task. Set `check: "None"` to skip verification.
66
68
  Use `max_iterations` in the task file to override the default attempt cap (0 means unlimited).
@@ -106,6 +108,8 @@ Run a task file across a list file:
106
108
  ```bash
107
109
  codexapi foreach list.txt task.yaml
108
110
  codexapi foreach list.txt task.yaml -n 4
111
+ codexapi foreach list.txt task.yaml --retry-failed
112
+ codexapi foreach list.txt task.yaml --retry-all
109
113
  ```
110
114
 
111
115
  ## API
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codexapi"
7
- version = "0.5.4"
7
+ version = "0.5.5"
8
8
  description = "Minimal Python API for running the Codex CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -15,4 +15,4 @@ __all__ = [
15
15
  "task",
16
16
  "task_result",
17
17
  ]
18
- __version__ = "0.5.4"
18
+ __version__ = "0.5.5"
@@ -15,7 +15,7 @@ from .agent import Agent, agent
15
15
  from .foreach import foreach
16
16
  from .ralph import cancel_ralph_loop, run_ralph_loop
17
17
  from .task import DEFAULT_MAX_ITERATIONS, TaskFailed, task
18
- from .taskfile import TaskFile
18
+ from .taskfile import TaskFile, load_task_file, task_def_uses_item
19
19
 
20
20
  _SESSION_ID_RE = re.compile(
21
21
  r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
@@ -62,6 +62,7 @@ _COLUMN_TITLES = {
62
62
  "perm": "PERM",
63
63
  "cwd": "CWD",
64
64
  }
65
+ _FOREACH_STATUS_MARKERS = {"⏳", "✅", "❌"}
65
66
 
66
67
 
67
68
  def _read_prompt(prompt):
@@ -871,6 +872,37 @@ def _print_top_once(show):
871
872
  print(_format_session(session, layout))
872
873
 
873
874
 
875
+ def _clean_foreach_list(path, retry_failed, retry_all):
876
+ with open(path, "r", encoding="utf-8") as handle:
877
+ data = handle.read()
878
+ ends_with_newline = data.endswith("\n")
879
+ lines = data.splitlines()
880
+
881
+ cleaned = []
882
+ changed = False
883
+ for line in lines:
884
+ new_line = line
885
+ if retry_all or (retry_failed and new_line.startswith("❌")):
886
+ if new_line and new_line[0] in _FOREACH_STATUS_MARKERS:
887
+ new_line = new_line[1:]
888
+ if new_line.startswith(" "):
889
+ new_line = new_line[1:]
890
+ pipe = new_line.find("|")
891
+ if pipe != -1:
892
+ new_line = new_line[:pipe].rstrip()
893
+ if new_line != line:
894
+ changed = True
895
+ cleaned.append(new_line)
896
+
897
+ if not changed:
898
+ return
899
+ text = "\n".join(cleaned)
900
+ if ends_with_newline:
901
+ text += "\n"
902
+ with open(path, "w", encoding="utf-8") as handle:
903
+ handle.write(text)
904
+
905
+
874
906
  def _run_top(argv):
875
907
  if argv and argv[0] in ("-h", "--help"):
876
908
  print("usage: codexapi top")
@@ -995,6 +1027,11 @@ def main(argv=None):
995
1027
  "--task-file",
996
1028
  help="YAML task file to run.",
997
1029
  )
1030
+ task_parser.add_argument(
1031
+ "-i",
1032
+ "--item",
1033
+ help="Item value for task files that use {{item}} placeholders.",
1034
+ )
998
1035
  task_parser.add_argument(
999
1036
  "prompt",
1000
1037
  nargs="?",
@@ -1148,6 +1185,17 @@ def main(argv=None):
1148
1185
  "task_file",
1149
1186
  help="Path to the YAML task file.",
1150
1187
  )
1188
+ foreach_retry_group = foreach_parser.add_mutually_exclusive_group()
1189
+ foreach_retry_group.add_argument(
1190
+ "--retry-failed",
1191
+ action="store_true",
1192
+ help="Reset failed (❌) items for re-run.",
1193
+ )
1194
+ foreach_retry_group.add_argument(
1195
+ "--retry-all",
1196
+ action="store_true",
1197
+ help="Reset all items for re-run.",
1198
+ )
1151
1199
  foreach_parser.add_argument(
1152
1200
  "-n",
1153
1201
  type=int,
@@ -1181,6 +1229,12 @@ def main(argv=None):
1181
1229
  if args.command == "foreach":
1182
1230
  if args.n is not None and args.n < 1:
1183
1231
  raise SystemExit("-n must be >= 1.")
1232
+ if args.retry_failed or args.retry_all:
1233
+ _clean_foreach_list(
1234
+ args.list_file,
1235
+ args.retry_failed,
1236
+ args.retry_all,
1237
+ )
1184
1238
  result = foreach(
1185
1239
  args.list_file,
1186
1240
  args.task_file,
@@ -1225,13 +1279,19 @@ def main(argv=None):
1225
1279
  if args.command == "task" and args.task_file:
1226
1280
  if args.prompt:
1227
1281
  raise SystemExit("task -f does not take a prompt.")
1282
+ if args.item is not None:
1283
+ task_def = load_task_file(args.task_file)
1284
+ if not task_def_uses_item(task_def):
1285
+ raise SystemExit(
1286
+ "task -f --item requires {{item}} in the task file."
1287
+ )
1228
1288
  if args.check is not None:
1229
1289
  raise SystemExit("--check is not allowed with -f.")
1230
1290
  if args.max_iterations is not None:
1231
1291
  raise SystemExit("--max-iterations is not allowed with -f.")
1232
1292
  task_runner = TaskFile(
1233
1293
  args.task_file,
1234
- None,
1294
+ args.item,
1235
1295
  cwd=args.cwd,
1236
1296
  yolo=args.yolo,
1237
1297
  thread_id=None,
@@ -1279,6 +1339,8 @@ def main(argv=None):
1279
1339
  )
1280
1340
  return
1281
1341
  if args.command == "task":
1342
+ if args.item is not None:
1343
+ raise SystemExit("--item is only supported with -f.")
1282
1344
  if args.max_iterations is None:
1283
1345
  args.max_iterations = DEFAULT_MAX_ITERATIONS
1284
1346
  if args.max_iterations < 0:
@@ -134,7 +134,17 @@ def _format_duration(seconds):
134
134
  return " ".join(parts)
135
135
 
136
136
 
137
- def _print_progress(
137
+ def _progress_round_label(attempt, total):
138
+ if not total:
139
+ return f"Round {attempt}/unlimited"
140
+ return f"Round {attempt}/{total}"
141
+
142
+
143
+ def _print_progress_start(attempt, total):
144
+ print(_progress_round_label(attempt, total), flush=True)
145
+
146
+
147
+ def _print_progress_result(
138
148
  attempt,
139
149
  total,
140
150
  start_time,
@@ -143,13 +153,13 @@ def _print_progress(
143
153
  cwd,
144
154
  yolo,
145
155
  flags,
156
+ success,
146
157
  ):
147
158
  elapsed = time.monotonic() - start_time
148
159
  remaining = 0
149
160
  remaining_text = "unknown"
150
- if total:
151
- if attempt:
152
- remaining = (elapsed / attempt) * (total - attempt)
161
+ if total and attempt:
162
+ remaining = (elapsed / attempt) * (total - attempt)
153
163
  remaining_text = _format_duration(remaining)
154
164
 
155
165
  summary_prompt = _build_progress_prompt(agent_output, check_output)
@@ -157,16 +167,13 @@ def _print_progress(
157
167
  agent_summary, check_summary = _progress_result(summary)
158
168
 
159
169
  elapsed_text = _format_duration(elapsed)
160
- if not total:
161
- round_text = f"Round {attempt}/unlimited"
162
- else:
163
- round_text = f"Round {attempt}/{total}"
170
+ print(f"Agent: {agent_summary}", flush=True)
171
+ print(f"Check: {check_summary}", flush=True)
172
+ verdict = "success" if success else "failure"
164
173
  print(
165
- f"{round_text} ({elapsed_text} elapsed, {remaining_text} remaining)",
174
+ f"Verdict: {verdict} ({elapsed_text} elapsed, {remaining_text} remaining)",
166
175
  flush=True,
167
176
  )
168
- print(f"Agent: {agent_summary}", flush=True)
169
- print(f"Check: {check_summary}", flush=True)
170
177
  print("", flush=True)
171
178
 
172
179
  def _fix_prompt(error):
@@ -443,6 +450,11 @@ class Task:
443
450
  attempt = 0
444
451
  while True:
445
452
  attempt += 1
453
+ if progress:
454
+ _print_progress_start(
455
+ attempt,
456
+ self.max_attempts,
457
+ )
446
458
  error = self.check(self.last_output)
447
459
  if debug:
448
460
  _logger.debug("Check error: %s", error)
@@ -451,7 +463,7 @@ class Task:
451
463
  check_output = self.last_check_output
452
464
  if self.check_skipped:
453
465
  check_output = "Verification skipped."
454
- _print_progress(
466
+ _print_progress_result(
455
467
  attempt,
456
468
  self.max_attempts,
457
469
  start_time,
@@ -460,6 +472,7 @@ class Task:
460
472
  self.cwd,
461
473
  self._yolo,
462
474
  self._flags,
475
+ not error,
463
476
  )
464
477
  if not error:
465
478
  summary = self.agent(self.success_prompt())
@@ -54,6 +54,17 @@ def _render(text, item):
54
54
  return text.replace(_ITEM_TOKEN, item)
55
55
 
56
56
 
57
+ def task_def_uses_item(task_def):
58
+ """Return True if a task definition includes the {{item}} placeholder."""
59
+ if not isinstance(task_def, dict):
60
+ raise TypeError("task definition must be a dict")
61
+ for key in ("prompt", "set_up", "tear_down", "check", "on_success", "on_failure"):
62
+ value = task_def.get(key)
63
+ if isinstance(value, str) and _ITEM_TOKEN in value:
64
+ return True
65
+ return False
66
+
67
+
57
68
  class TaskFile(AutoTask):
58
69
  """Task subclass that maps a YAML task file onto Task hooks."""
59
70
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.5.4
3
+ Version: 0.5.5
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -73,8 +73,10 @@ echo "Say hello." | codexapi run
73
73
  ```bash
74
74
  codexapi task "Fix the failing tests." --max-iterations 5
75
75
  codexapi task -f task.yaml
76
+ codexapi task -f task.yaml -i README.md
76
77
  ```
77
78
  Progress is shown by default for `codexapi task`; use `--quiet` to suppress it.
79
+ When using `--item`, the task file must include at least one `{{item}}` placeholder.
78
80
 
79
81
  Task files default to using the standard check prompt for the task. Set `check: "None"` to skip verification.
80
82
  Use `max_iterations` in the task file to override the default attempt cap (0 means unlimited).
@@ -120,6 +122,8 @@ Run a task file across a list file:
120
122
  ```bash
121
123
  codexapi foreach list.txt task.yaml
122
124
  codexapi foreach list.txt task.yaml -n 4
125
+ codexapi foreach list.txt task.yaml --retry-failed
126
+ codexapi foreach list.txt task.yaml --retry-all
123
127
  ```
124
128
 
125
129
  ## API
File without changes
File without changes
File without changes
File without changes