codexapi 0.6.2__tar.gz → 0.6.4__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.
Files changed (25) hide show
  1. {codexapi-0.6.2/src/codexapi.egg-info → codexapi-0.6.4}/PKG-INFO +23 -2
  2. {codexapi-0.6.2 → codexapi-0.6.4}/README.md +22 -1
  3. {codexapi-0.6.2 → codexapi-0.6.4}/pyproject.toml +1 -1
  4. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/__init__.py +3 -1
  5. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/cli.py +38 -1
  6. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/pushover.py +1 -1
  7. codexapi-0.6.4/src/codexapi/watch.py +279 -0
  8. {codexapi-0.6.2 → codexapi-0.6.4/src/codexapi.egg-info}/PKG-INFO +23 -2
  9. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/SOURCES.txt +1 -0
  10. {codexapi-0.6.2 → codexapi-0.6.4}/LICENSE +0 -0
  11. {codexapi-0.6.2 → codexapi-0.6.4}/setup.cfg +0 -0
  12. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/__main__.py +0 -0
  13. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/agent.py +0 -0
  14. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/foreach.py +0 -0
  15. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/gh_integration.py +0 -0
  16. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/ralph.py +0 -0
  17. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/rate_limits.py +0 -0
  18. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/science.py +0 -0
  19. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/task.py +0 -0
  20. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/taskfile.py +0 -0
  21. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/welfare.py +0 -0
  22. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/dependency_links.txt +0 -0
  23. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/entry_points.txt +0 -0
  24. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/requires.txt +0 -0
  25. {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -126,6 +126,18 @@ codexapi run --thread-id THREAD_ID --print-thread-id "Continue where we left off
126
126
 
127
127
  Use `--no-yolo` to run Codex with `--full-auto` instead.
128
128
 
129
+ Watch mode periodically ticks a long-running agent session with the current time
130
+ and prints JSON status updates. The agent controls the loop by setting
131
+ `continue` to true/false in its JSON response. Each tick expects JSON keys:
132
+ `status` (one line), `continue` (bool), and optional `comments` (string). If the
133
+ JSON is invalid, watch asks the agent once to retry before stopping with an
134
+ error. When `~/.pushover` is configured, watch sends a notification when it
135
+ stops.
136
+
137
+ ```bash
138
+ codexapi watch 5 "Run the benchmark and wait for results."
139
+ ```
140
+
129
141
  Ralph loop mode repeats the same prompt until a completion promise or a max
130
142
  iteration cap is hit (0 means unlimited). Cancel by deleting
131
143
  `.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
@@ -154,7 +166,8 @@ Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
154
166
  Line 1 is your user or group key, line 2 is the app API token. When this file
155
167
  exists, Science will send a notification whenever it detects a new best result,
156
168
  including the metric values and percent improvement. Task runs will also send a
157
- ✅/❌ notification with the task summary.
169
+ ✅/❌ notification with the task summary. Watch runs send a notification when the
170
+ loop stops.
158
171
 
159
172
  Run a task file across a list file:
160
173
 
@@ -189,6 +202,14 @@ the same conversation and returns only the agent's message.
189
202
  - `welfare` (bool): when true, append welfare stop instructions to each prompt
190
203
  and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
191
204
 
205
+ ### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
206
+
207
+ Runs a long-lived agent session and periodically "ticks" it with the current
208
+ local time and a reminder of `prompt`. Each tick expects JSON with keys:
209
+ `status` (one line), `continue` (bool), and optional `comments` (string). If the
210
+ JSON is invalid, watch asks the agent once to retry. The loop stops when
211
+ `continue` is false and sends a Pushover notification (when configured).
212
+
192
213
  ### `task(prompt, check=None, max_iterations=10, cwd=None, yolo=True, flags=None, progress=False, set_up=None, tear_down=None, on_success=None, on_failure=None) -> str`
193
214
 
194
215
  Runs a task with checker-driven retries and returns the success summary.
@@ -111,6 +111,18 @@ codexapi run --thread-id THREAD_ID --print-thread-id "Continue where we left off
111
111
 
112
112
  Use `--no-yolo` to run Codex with `--full-auto` instead.
113
113
 
114
+ Watch mode periodically ticks a long-running agent session with the current time
115
+ and prints JSON status updates. The agent controls the loop by setting
116
+ `continue` to true/false in its JSON response. Each tick expects JSON keys:
117
+ `status` (one line), `continue` (bool), and optional `comments` (string). If the
118
+ JSON is invalid, watch asks the agent once to retry before stopping with an
119
+ error. When `~/.pushover` is configured, watch sends a notification when it
120
+ stops.
121
+
122
+ ```bash
123
+ codexapi watch 5 "Run the benchmark and wait for results."
124
+ ```
125
+
114
126
  Ralph loop mode repeats the same prompt until a completion promise or a max
115
127
  iteration cap is hit (0 means unlimited). Cancel by deleting
116
128
  `.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
@@ -139,7 +151,8 @@ Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
139
151
  Line 1 is your user or group key, line 2 is the app API token. When this file
140
152
  exists, Science will send a notification whenever it detects a new best result,
141
153
  including the metric values and percent improvement. Task runs will also send a
142
- ✅/❌ notification with the task summary.
154
+ ✅/❌ notification with the task summary. Watch runs send a notification when the
155
+ loop stops.
143
156
 
144
157
  Run a task file across a list file:
145
158
 
@@ -174,6 +187,14 @@ the same conversation and returns only the agent's message.
174
187
  - `welfare` (bool): when true, append welfare stop instructions to each prompt
175
188
  and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
176
189
 
190
+ ### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
191
+
192
+ Runs a long-lived agent session and periodically "ticks" it with the current
193
+ local time and a reminder of `prompt`. Each tick expects JSON with keys:
194
+ `status` (one line), `continue` (bool), and optional `comments` (string). If the
195
+ JSON is invalid, watch asks the agent once to retry. The loop stops when
196
+ `continue` is false and sends a Pushover notification (when configured).
197
+
177
198
  ### `task(prompt, check=None, max_iterations=10, cwd=None, yolo=True, flags=None, progress=False, set_up=None, tear_down=None, on_success=None, on_failure=None) -> str`
178
199
 
179
200
  Runs a task with checker-driven retries and returns the success summary.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codexapi"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Minimal Python API for running the Codex CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -7,6 +7,7 @@ from .rate_limits import quota_line, rate_limits
7
7
  from .ralph import Ralph
8
8
  from .science import Science
9
9
  from .task import Task, TaskFailed, TaskResult, task, task_result
10
+ from .watch import watch
10
11
 
11
12
  __all__ = [
12
13
  "Agent",
@@ -24,5 +25,6 @@ __all__ = [
24
25
  "foreach",
25
26
  "task",
26
27
  "task_result",
28
+ "watch",
27
29
  ]
28
- __version__ = "0.6.2"
30
+ __version__ = "0.6.4"
@@ -19,6 +19,7 @@ from .science import Science
19
19
  from .task import DEFAULT_MAX_ITERATIONS, TaskFailed, task
20
20
  from .taskfile import TaskFile, load_task_file, task_def_uses_item
21
21
  from .rate_limits import quota_line
22
+ from .watch import watch
22
23
 
23
24
  _SESSION_ID_RE = re.compile(
24
25
  r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
@@ -1039,6 +1040,32 @@ def main(argv=None):
1039
1040
  "--flags",
1040
1041
  help="Additional raw CLI flags to pass to Codex (quoted as needed).",
1041
1042
  )
1043
+
1044
+ watch_parser = subparsers.add_parser(
1045
+ "watch",
1046
+ help="Periodically tick an agent for long-running work.",
1047
+ )
1048
+ watch_parser.add_argument(
1049
+ "minutes",
1050
+ type=int,
1051
+ help="Tick interval in minutes (integer, >= 1).",
1052
+ )
1053
+ watch_parser.add_argument(
1054
+ "prompt",
1055
+ nargs="?",
1056
+ help="Prompt to send. Use '-' or omit to read from stdin.",
1057
+ )
1058
+ watch_parser.add_argument("--cwd", help="Working directory for the Codex session.")
1059
+ watch_parser.add_argument(
1060
+ "--no-yolo",
1061
+ action="store_false",
1062
+ dest="yolo",
1063
+ help="Disable --yolo and use --full-auto.",
1064
+ )
1065
+ watch_parser.add_argument(
1066
+ "--flags",
1067
+ help="Additional raw CLI flags to pass to Codex (quoted as needed).",
1068
+ )
1042
1069
  run_parser.add_argument(
1043
1070
  "--thread-id",
1044
1071
  help="Resume an existing Codex thread id.",
@@ -1474,7 +1501,7 @@ def main(argv=None):
1474
1501
 
1475
1502
  prompt_source = None
1476
1503
  prompt = None
1477
- if args.command in ("run", "ralph"):
1504
+ if args.command in ("run", "ralph", "watch"):
1478
1505
  prompt_source = args.prompt
1479
1506
  elif args.command == "science":
1480
1507
  prompt_source = args.task
@@ -1509,6 +1536,16 @@ def main(argv=None):
1509
1536
  args.ralph_fresh,
1510
1537
  )()
1511
1538
  return
1539
+ if args.command == "watch":
1540
+ if args.minutes < 1:
1541
+ raise SystemExit("watch minutes must be >= 1.")
1542
+ try:
1543
+ watch(args.minutes, prompt, args.cwd, args.yolo, args.flags)
1544
+ except KeyboardInterrupt:
1545
+ raise SystemExit(130)
1546
+ except Exception as exc:
1547
+ raise SystemExit(str(exc) or "watch failed") from None
1548
+ return
1512
1549
  if args.command == "task":
1513
1550
  if args.project:
1514
1551
  raise SystemExit("task --project already handled earlier.")
@@ -15,7 +15,7 @@ _PUSHOVER_URL = "https://api.pushover.net/1/messages.json"
15
15
  _MAX_MESSAGE = 1024
16
16
 
17
17
  _STARTUP_MESSAGE = (
18
- "Pushover user and app keys read, notifications for task and science enabled."
18
+ "Pushover user and app keys read, notifications for task/science/watch enabled."
19
19
  )
20
20
 
21
21
 
@@ -0,0 +1,279 @@
1
+ """Periodic watch loop for long-running Codex work.
2
+
3
+ watch keeps a single Codex thread alive and periodically "ticks" it with the
4
+ current time and a reminder of the original instructions. Each tick expects a
5
+ small JSON status payload so the loop can decide whether to continue.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ import time
11
+ from datetime import datetime
12
+
13
+ from .agent import Agent
14
+ from .pushover import Pushover
15
+
16
+ _JSON_INSTRUCTIONS = (
17
+ "Respond with JSON only (no markdown/backticks/extra text).\n"
18
+ "Return a single JSON object with keys:\n"
19
+ " status: string (one line)\n"
20
+ " continue: boolean\n"
21
+ " comments: string (optional)\n"
22
+ "To stop this watch loop, set continue to false."
23
+ )
24
+
25
+
26
+ def watch(minutes, prompt, cwd=None, yolo=True, flags=None):
27
+ """Run a periodic watch loop.
28
+
29
+ Args:
30
+ minutes: Tick interval in whole minutes (>= 1).
31
+ prompt: The original instruction prompt.
32
+ cwd: Optional working directory for the Codex session.
33
+ yolo: Whether to pass --yolo to Codex.
34
+ flags: Additional raw CLI flags to pass to Codex.
35
+
36
+ Returns:
37
+ The last parsed JSON status object.
38
+ """
39
+ if not isinstance(minutes, int):
40
+ raise TypeError("minutes must be an integer")
41
+ if minutes < 1:
42
+ raise ValueError("minutes must be >= 1")
43
+ if not isinstance(prompt, str) or not prompt.strip():
44
+ raise ValueError("prompt must be a non-empty string")
45
+
46
+ interval = minutes * 60
47
+ session = Agent(cwd, yolo, None, flags)
48
+ pushover = Pushover()
49
+ pushover.ensure_ready()
50
+ title = _format_title(prompt)
51
+
52
+ last_sent = None
53
+ last_result = None
54
+ tick = 0
55
+
56
+ while True:
57
+ tick += 1
58
+ sent_at = time.monotonic()
59
+ elapsed = None if last_sent is None else sent_at - last_sent
60
+ last_sent = sent_at
61
+
62
+ now = datetime.now().astimezone().isoformat(timespec="seconds")
63
+ message = _build_tick_prompt(prompt, now, elapsed, tick)
64
+ output = session(message)
65
+ try:
66
+ result = _parse_status(output)
67
+ except ValueError as exc:
68
+ print(
69
+ f"[watch {tick} {now}] Invalid JSON from agent, requesting retry: {exc}",
70
+ file=sys.stderr,
71
+ )
72
+ retry_prompt = _json_retry_prompt(prompt, tick, str(exc), output)
73
+ retry_output = session(retry_prompt)
74
+ try:
75
+ result = _parse_status(retry_output)
76
+ except ValueError as exc2:
77
+ details = _format_json_double_failure(
78
+ str(exc),
79
+ output,
80
+ str(exc2),
81
+ retry_output,
82
+ )
83
+ pushover.send(title, f"Watch stopped (invalid JSON).\n{details}")
84
+ raise RuntimeError(
85
+ "Agent was unable to provide valid JSON output after retry.\n"
86
+ + details
87
+ ) from None
88
+ last_result = result
89
+ _print_status(now, elapsed, tick, result)
90
+
91
+ if not result["continue"]:
92
+ pushover.send(title, _format_stop_message(tick, now, result))
93
+ return last_result
94
+
95
+ next_tick = sent_at + interval
96
+ sleep_seconds = next_tick - time.monotonic()
97
+ if sleep_seconds > 0:
98
+ time.sleep(sleep_seconds)
99
+
100
+
101
+ def _build_tick_prompt(prompt, now, elapsed, tick):
102
+ lines = [
103
+ f"Tick {tick}.",
104
+ f"Local time now: {now}",
105
+ ]
106
+ if elapsed is not None:
107
+ lines.append(
108
+ "Time since last tick: "
109
+ f"{_format_minutes_seconds(elapsed)} ({int(round(elapsed))}s)"
110
+ )
111
+ lines.extend(
112
+ [
113
+ "",
114
+ "A reminder: your instructions are:",
115
+ prompt.strip(),
116
+ "",
117
+ _JSON_INSTRUCTIONS,
118
+ ]
119
+ )
120
+ return "\n".join(lines).strip()
121
+
122
+
123
+ def _format_minutes_seconds(seconds):
124
+ if seconds is None:
125
+ return ""
126
+ seconds = int(round(seconds))
127
+ if seconds < 0:
128
+ seconds = 0
129
+ minutes, seconds = divmod(seconds, 60)
130
+ return f"{minutes}m{seconds:02d}s"
131
+
132
+
133
+ def _parse_status(output):
134
+ text = _maybe_strip_code_fence(str(output or "").strip())
135
+ data = _try_parse_json(text)
136
+ if data is None:
137
+ snippet = text[:200].replace("\n", "\\n")
138
+ raise ValueError(f"Invalid JSON response. Snippet: {snippet}")
139
+ if not isinstance(data, dict):
140
+ raise ValueError("Status JSON must be an object.")
141
+
142
+ status = data.get("status")
143
+ cont = data.get("continue")
144
+ comments = data.get("comments")
145
+
146
+ if not isinstance(status, str):
147
+ raise ValueError("Status JSON missing string 'status'.")
148
+ if not isinstance(cont, bool):
149
+ raise ValueError("Status JSON missing boolean 'continue'.")
150
+ if comments is None:
151
+ comments = ""
152
+ if not isinstance(comments, str):
153
+ raise ValueError("Status JSON missing string 'comments'.")
154
+
155
+ return {
156
+ "status": _single_line(status),
157
+ "continue": cont,
158
+ "comments": comments,
159
+ }
160
+
161
+
162
+ def _json_retry_prompt(prompt, tick, error, output):
163
+ snippet = _snippet(output, 600)
164
+ lines = [
165
+ f"Your last message (tick {tick}) was not valid JSON.",
166
+ f"Error: {error}",
167
+ "",
168
+ "Here is your previous output (truncated):",
169
+ snippet,
170
+ "",
171
+ "Please try again and respond with JSON only.",
172
+ "",
173
+ "A reminder: your instructions are:",
174
+ prompt.strip(),
175
+ "",
176
+ _JSON_INSTRUCTIONS,
177
+ ]
178
+ return "\n".join(lines).strip()
179
+
180
+
181
+ def _format_title(prompt):
182
+ text = _single_line(prompt).strip() or "codexapi watch"
183
+ if len(text) > 60:
184
+ text = text[:57] + "..."
185
+ return f"Watch: {text}"
186
+
187
+
188
+ def _format_stop_message(tick, now, result):
189
+ status = _single_line(result.get("status") or "").strip()
190
+ header = f"Watch stopped at tick {tick} ({now})."
191
+ if status:
192
+ header = f"{header} {status}"
193
+ comments = (result.get("comments") or "").strip()
194
+ if comments:
195
+ return f"{header}\n{comments}"
196
+ return header
197
+
198
+
199
+ def _format_json_failure(error, output):
200
+ snippet = _snippet(output, 600)
201
+ return "\n".join(
202
+ [
203
+ f"Error: {error}",
204
+ "",
205
+ "Last output (truncated):",
206
+ snippet,
207
+ ]
208
+ ).strip()
209
+
210
+
211
+ def _format_json_double_failure(error_1, output_1, error_2, output_2):
212
+ first = _format_json_failure(error_1, output_1)
213
+ second = _format_json_failure(error_2, output_2)
214
+ return "\n".join(
215
+ [
216
+ "First attempt:",
217
+ first,
218
+ "",
219
+ "Second attempt:",
220
+ second,
221
+ ]
222
+ ).strip()
223
+
224
+
225
+ def _snippet(text, limit):
226
+ text = str(text or "").strip()
227
+ if not text:
228
+ return "(empty)"
229
+ if len(text) <= limit:
230
+ return text
231
+ return text[:limit].rstrip() + "..."
232
+
233
+
234
+ def _maybe_strip_code_fence(text):
235
+ if not text.startswith("```"):
236
+ return text
237
+ lines = text.splitlines()
238
+ if not lines:
239
+ return text
240
+ if lines[0].startswith("```"):
241
+ lines = lines[1:]
242
+ if lines and lines[-1].strip() == "```":
243
+ lines = lines[:-1]
244
+ return "\n".join(lines).strip()
245
+
246
+
247
+ def _try_parse_json(text):
248
+ if not text:
249
+ return None
250
+ try:
251
+ return json.loads(text)
252
+ except json.JSONDecodeError:
253
+ pass
254
+
255
+ start = text.find("{")
256
+ end = text.rfind("}")
257
+ if start == -1 or end == -1 or end <= start:
258
+ return None
259
+ try:
260
+ return json.loads(text[start : end + 1])
261
+ except json.JSONDecodeError:
262
+ return None
263
+
264
+
265
+ def _single_line(text):
266
+ return " ".join(text.replace("\r", " ").split())
267
+
268
+
269
+ def _print_status(now, elapsed, tick, result):
270
+ delta = ""
271
+ if elapsed is not None:
272
+ delta = f" +{_format_minutes_seconds(elapsed)}"
273
+ status = result.get("status", "")
274
+ cont = result.get("continue")
275
+ line = f"[watch {tick} {now}{delta}] {status} (continue={cont})".rstrip()
276
+ print(line)
277
+ comments = result.get("comments") or ""
278
+ if comments.strip():
279
+ print(comments.rstrip())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -126,6 +126,18 @@ codexapi run --thread-id THREAD_ID --print-thread-id "Continue where we left off
126
126
 
127
127
  Use `--no-yolo` to run Codex with `--full-auto` instead.
128
128
 
129
+ Watch mode periodically ticks a long-running agent session with the current time
130
+ and prints JSON status updates. The agent controls the loop by setting
131
+ `continue` to true/false in its JSON response. Each tick expects JSON keys:
132
+ `status` (one line), `continue` (bool), and optional `comments` (string). If the
133
+ JSON is invalid, watch asks the agent once to retry before stopping with an
134
+ error. When `~/.pushover` is configured, watch sends a notification when it
135
+ stops.
136
+
137
+ ```bash
138
+ codexapi watch 5 "Run the benchmark and wait for results."
139
+ ```
140
+
129
141
  Ralph loop mode repeats the same prompt until a completion promise or a max
130
142
  iteration cap is hit (0 means unlimited). Cancel by deleting
131
143
  `.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
@@ -154,7 +166,8 @@ Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
154
166
  Line 1 is your user or group key, line 2 is the app API token. When this file
155
167
  exists, Science will send a notification whenever it detects a new best result,
156
168
  including the metric values and percent improvement. Task runs will also send a
157
- ✅/❌ notification with the task summary.
169
+ ✅/❌ notification with the task summary. Watch runs send a notification when the
170
+ loop stops.
158
171
 
159
172
  Run a task file across a list file:
160
173
 
@@ -189,6 +202,14 @@ the same conversation and returns only the agent's message.
189
202
  - `welfare` (bool): when true, append welfare stop instructions to each prompt
190
203
  and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
191
204
 
205
+ ### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
206
+
207
+ Runs a long-lived agent session and periodically "ticks" it with the current
208
+ local time and a reminder of `prompt`. Each tick expects JSON with keys:
209
+ `status` (one line), `continue` (bool), and optional `comments` (string). If the
210
+ JSON is invalid, watch asks the agent once to retry. The loop stops when
211
+ `continue` is false and sends a Pushover notification (when configured).
212
+
192
213
  ### `task(prompt, check=None, max_iterations=10, cwd=None, yolo=True, flags=None, progress=False, set_up=None, tear_down=None, on_success=None, on_failure=None) -> str`
193
214
 
194
215
  Runs a task with checker-driven retries and returns the success summary.
@@ -13,6 +13,7 @@ src/codexapi/rate_limits.py
13
13
  src/codexapi/science.py
14
14
  src/codexapi/task.py
15
15
  src/codexapi/taskfile.py
16
+ src/codexapi/watch.py
16
17
  src/codexapi/welfare.py
17
18
  src/codexapi.egg-info/PKG-INFO
18
19
  src/codexapi.egg-info/SOURCES.txt
File without changes
File without changes
File without changes
File without changes
File without changes