codex-autorunner 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +114 -1
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +236 -1
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +5 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +26 -1
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9125 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/file_chat.py +317 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +70 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/RECORD +125 -94
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -15,15 +15,16 @@ Commands:
|
|
|
15
15
|
lint Validate ticket filenames and frontmatter.
|
|
16
16
|
insert --before N Shift tickets >= N up by COUNT (default 1).
|
|
17
17
|
insert --after N Shift tickets > N up by COUNT (default 1).
|
|
18
|
+
Optionally create a ticket in the new slot.
|
|
18
19
|
move --start A --to B Move ticket/block starting at A (or A..END)
|
|
19
20
|
so it begins at position B (1-indexed).
|
|
20
|
-
create --title
|
|
21
|
+
create --title "..." Create a new ticket at the next or specified
|
|
21
22
|
index. Use --at to place into a gap.
|
|
22
23
|
|
|
23
24
|
Examples:
|
|
24
25
|
ticket_tool.py list
|
|
25
26
|
ticket_tool.py insert --before 3
|
|
26
|
-
ticket_tool.py create --title
|
|
27
|
+
ticket_tool.py create --title "Investigate flaky test" --at 3
|
|
27
28
|
ticket_tool.py move --start 5 --end 7 --to 2
|
|
28
29
|
ticket_tool.py lint
|
|
29
30
|
|
|
@@ -70,16 +71,18 @@ def _ticket_paths(ticket_dir: Path) -> Tuple[List[Path], List[str]]:
|
|
|
70
71
|
for path in sorted(ticket_dir.iterdir()):
|
|
71
72
|
if not path.is_file():
|
|
72
73
|
continue
|
|
74
|
+
if path.name == "AGENTS.md":
|
|
75
|
+
continue
|
|
73
76
|
m = _TICKET_NAME_RE.match(path.name)
|
|
74
77
|
if not m:
|
|
75
78
|
errors.append(
|
|
76
|
-
f
|
|
79
|
+
f"{path}: Invalid ticket filename; expected TICKET-<number>[suffix].md"
|
|
77
80
|
)
|
|
78
81
|
continue
|
|
79
82
|
try:
|
|
80
83
|
idx = int(m.group(1))
|
|
81
84
|
except ValueError:
|
|
82
|
-
errors.append(f
|
|
85
|
+
errors.append(f"{path}: Invalid ticket filename; number must be digits")
|
|
83
86
|
continue
|
|
84
87
|
tickets.append((idx, path, m.group(2)))
|
|
85
88
|
tickets.sort(key=lambda t: t[0])
|
|
@@ -87,71 +90,71 @@ def _ticket_paths(ticket_dir: Path) -> Tuple[List[Path], List[str]]:
|
|
|
87
90
|
|
|
88
91
|
|
|
89
92
|
def _split_frontmatter(text: str):
|
|
90
|
-
if not text or not text.lstrip().startswith(
|
|
91
|
-
return None, [
|
|
93
|
+
if not text or not text.lstrip().startswith("---"):
|
|
94
|
+
return None, ["Missing YAML frontmatter (expected leading '---')."]
|
|
92
95
|
lines = text.splitlines()
|
|
93
96
|
end_idx = None
|
|
94
97
|
for idx in range(1, len(lines)):
|
|
95
|
-
if lines[idx].strip() in (
|
|
98
|
+
if lines[idx].strip() in ("---", "..."):
|
|
96
99
|
end_idx = idx
|
|
97
100
|
break
|
|
98
101
|
if end_idx is None:
|
|
99
|
-
return None, [
|
|
100
|
-
fm_yaml =
|
|
102
|
+
return None, ["Frontmatter is not closed (missing trailing '---')."]
|
|
103
|
+
fm_yaml = "\\n".join(lines[1:end_idx])
|
|
101
104
|
return fm_yaml, []
|
|
102
105
|
|
|
103
106
|
|
|
104
107
|
def _parse_yaml(fm_yaml: Optional[str]):
|
|
105
108
|
if fm_yaml is None:
|
|
106
|
-
return {}, [
|
|
109
|
+
return {}, ["Missing or invalid YAML frontmatter (expected a mapping)."]
|
|
107
110
|
if yaml is None:
|
|
108
111
|
return {}, [
|
|
109
|
-
|
|
112
|
+
"PyYAML is required to lint tickets. Install with: python3 -m pip install --user pyyaml"
|
|
110
113
|
]
|
|
111
114
|
try:
|
|
112
115
|
loaded = yaml.safe_load(fm_yaml)
|
|
113
116
|
except Exception as exc: # noqa: BLE001
|
|
114
|
-
return {}, [f
|
|
117
|
+
return {}, [f"YAML parse error: {exc}"]
|
|
115
118
|
if loaded is None or not isinstance(loaded, dict):
|
|
116
|
-
return {}, [
|
|
119
|
+
return {}, ["Invalid YAML frontmatter (expected a mapping)."]
|
|
117
120
|
return loaded, []
|
|
118
121
|
|
|
119
122
|
|
|
120
123
|
def _lint_frontmatter(data: dict):
|
|
121
124
|
errors: List[str] = []
|
|
122
|
-
agent = data.get(
|
|
125
|
+
agent = data.get("agent")
|
|
123
126
|
if not isinstance(agent, str) or not agent.strip():
|
|
124
|
-
errors.append(
|
|
125
|
-
done = data.get(
|
|
127
|
+
errors.append("frontmatter.agent is required and must be a non-empty string.")
|
|
128
|
+
done = data.get("done")
|
|
126
129
|
if not isinstance(done, bool):
|
|
127
|
-
errors.append(
|
|
130
|
+
errors.append("frontmatter.done is required and must be a boolean.")
|
|
128
131
|
return errors
|
|
129
132
|
|
|
130
133
|
|
|
131
134
|
def _read_ticket(path: Path) -> Tuple[Optional[TicketFile], List[str]]:
|
|
132
135
|
try:
|
|
133
|
-
raw = path.read_text(encoding
|
|
136
|
+
raw = path.read_text(encoding="utf-8")
|
|
134
137
|
except OSError as exc:
|
|
135
|
-
return None, [f
|
|
138
|
+
return None, [f"{path}: Unable to read file ({exc})."]
|
|
136
139
|
|
|
137
140
|
fm_yaml, fm_errors = _split_frontmatter(raw)
|
|
138
141
|
if fm_errors:
|
|
139
|
-
return None, [f
|
|
142
|
+
return None, [f"{path}: {msg}" for msg in fm_errors]
|
|
140
143
|
|
|
141
144
|
data, parse_errors = _parse_yaml(fm_yaml)
|
|
142
145
|
if parse_errors:
|
|
143
|
-
return None, [f
|
|
146
|
+
return None, [f"{path}: {msg}" for msg in parse_errors]
|
|
144
147
|
|
|
145
148
|
lint_errors = _lint_frontmatter(data)
|
|
146
149
|
if lint_errors:
|
|
147
|
-
return None, [f
|
|
150
|
+
return None, [f"{path}: {msg}" for msg in lint_errors]
|
|
148
151
|
|
|
149
|
-
title = data.get(
|
|
150
|
-
done_val = data.get(
|
|
152
|
+
title = data.get("title") if isinstance(data, dict) else None
|
|
153
|
+
done_val = data.get("done") if isinstance(data, dict) else None
|
|
151
154
|
|
|
152
155
|
m = _TICKET_NAME_RE.match(path.name)
|
|
153
156
|
idx = int(m.group(1)) if m else 0
|
|
154
|
-
suffix = m.group(2) if m else
|
|
157
|
+
suffix = m.group(2) if m else ""
|
|
155
158
|
return TicketFile(index=idx, path=path, suffix=suffix, title=title, done=done_val), []
|
|
156
159
|
|
|
157
160
|
|
|
@@ -175,7 +178,7 @@ def _pad_width(indices: Sequence[int]) -> int:
|
|
|
175
178
|
|
|
176
179
|
|
|
177
180
|
def _fmt_name(index: int, suffix: str, width: int) -> str:
|
|
178
|
-
return f
|
|
181
|
+
return f"TICKET-{index:0{width}d}{suffix}.md"
|
|
179
182
|
|
|
180
183
|
|
|
181
184
|
def _safe_renames(mapping: Sequence[tuple[Path, Path]]) -> None:
|
|
@@ -183,11 +186,11 @@ def _safe_renames(mapping: Sequence[tuple[Path, Path]]) -> None:
|
|
|
183
186
|
for src, dst in mapping:
|
|
184
187
|
if src == dst:
|
|
185
188
|
continue
|
|
186
|
-
temp = src.with_name(src.name +
|
|
189
|
+
temp = src.with_name(src.name + ".tmp-move")
|
|
187
190
|
counter = 0
|
|
188
191
|
while temp.exists():
|
|
189
192
|
counter += 1
|
|
190
|
-
temp = src.with_name(f
|
|
193
|
+
temp = src.with_name(f"{src.name}.tmp-move-{counter}")
|
|
191
194
|
src.rename(temp)
|
|
192
195
|
temp_pairs.append((temp, dst))
|
|
193
196
|
|
|
@@ -200,12 +203,12 @@ def cmd_list(ticket_dir: Path) -> int:
|
|
|
200
203
|
tickets, errors = _ticket_files(ticket_dir)
|
|
201
204
|
if errors:
|
|
202
205
|
for msg in errors:
|
|
203
|
-
sys.stderr.write(msg +
|
|
206
|
+
sys.stderr.write(msg + "\\n")
|
|
204
207
|
width = _pad_width([t.index for t in tickets])
|
|
205
208
|
for t in tickets:
|
|
206
|
-
status =
|
|
207
|
-
title = f
|
|
208
|
-
sys.stdout.write(f
|
|
209
|
+
status = "done" if t.done else "open"
|
|
210
|
+
title = f" - {t.title}" if t.title else ""
|
|
211
|
+
sys.stdout.write(f"{t.index:0{width}d} [{status}] {t.path.name}{title}\\n")
|
|
209
212
|
if errors:
|
|
210
213
|
return 1
|
|
211
214
|
return 0
|
|
@@ -220,9 +223,9 @@ def cmd_lint(ticket_dir: Path) -> int:
|
|
|
220
223
|
|
|
221
224
|
if errors:
|
|
222
225
|
for msg in errors:
|
|
223
|
-
sys.stderr.write(msg +
|
|
226
|
+
sys.stderr.write(msg + "\\n")
|
|
224
227
|
return 1
|
|
225
|
-
sys.stdout.write(f
|
|
228
|
+
sys.stdout.write(f"OK: {len(paths)} ticket(s) linted.\\n")
|
|
226
229
|
return 0
|
|
227
230
|
|
|
228
231
|
|
|
@@ -231,7 +234,7 @@ def _shift(ticket_dir: Path, start_idx: int, delta: int) -> None:
|
|
|
231
234
|
return
|
|
232
235
|
paths, errors = _ticket_paths(ticket_dir)
|
|
233
236
|
if errors:
|
|
234
|
-
raise ValueError(
|
|
237
|
+
raise ValueError("Cannot shift while filenames are invalid; run lint first.")
|
|
235
238
|
iterable = reversed(paths) if delta > 0 else paths
|
|
236
239
|
width = _pad_width([_parse_index(p.name) for p in paths] + [start_idx + delta])
|
|
237
240
|
mapping: list[tuple[Path, Path]] = []
|
|
@@ -241,7 +244,7 @@ def _shift(ticket_dir: Path, start_idx: int, delta: int) -> None:
|
|
|
241
244
|
continue
|
|
242
245
|
new_idx = idx + delta
|
|
243
246
|
if new_idx <= 0:
|
|
244
|
-
raise ValueError(
|
|
247
|
+
raise ValueError("Shift would create non-positive ticket index")
|
|
245
248
|
suffix = _parse_suffix(path.name)
|
|
246
249
|
target = path.with_name(_fmt_name(new_idx, suffix, width))
|
|
247
250
|
mapping.append((path, target))
|
|
@@ -255,22 +258,73 @@ def _parse_index(name: str) -> Optional[int]:
|
|
|
255
258
|
|
|
256
259
|
def _parse_suffix(name: str) -> str:
|
|
257
260
|
m = _TICKET_NAME_RE.match(name)
|
|
258
|
-
return m.group(2) if m else
|
|
261
|
+
return m.group(2) if m else ""
|
|
259
262
|
|
|
260
263
|
|
|
261
|
-
def
|
|
264
|
+
def _create_ticket_file(ticket_dir: Path, *, index: int, title: str, agent: str, existing_indices: List[int]) -> Path:
|
|
265
|
+
width = _pad_width(existing_indices + [index])
|
|
266
|
+
name = _fmt_name(index, "", width)
|
|
267
|
+
path = ticket_dir / name
|
|
268
|
+
if path.exists():
|
|
269
|
+
raise ValueError(f"Ticket index {index} already exists: {path}")
|
|
270
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
title_scalar = _yaml_scalar(title)
|
|
272
|
+
agent_scalar = _yaml_scalar(agent)
|
|
273
|
+
body = (
|
|
274
|
+
f"---\\n"
|
|
275
|
+
f"title: {title_scalar}\\n"
|
|
276
|
+
f"agent: {agent_scalar}\\n"
|
|
277
|
+
f"done: false\\n"
|
|
278
|
+
f"---\\n\\n"
|
|
279
|
+
f"## Goal\\n- \\n"
|
|
280
|
+
)
|
|
281
|
+
path.write_text(body, encoding="utf-8")
|
|
282
|
+
return path
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_insert(
|
|
286
|
+
ticket_dir: Path,
|
|
287
|
+
*,
|
|
288
|
+
before: Optional[int],
|
|
289
|
+
after: Optional[int],
|
|
290
|
+
count: int,
|
|
291
|
+
title: Optional[str],
|
|
292
|
+
agent: str,
|
|
293
|
+
) -> int:
|
|
262
294
|
if (before is None) == (after is None):
|
|
263
|
-
sys.stderr.write(
|
|
295
|
+
sys.stderr.write("Specify exactly one of --before or --after.\\n")
|
|
296
|
+
return 2
|
|
297
|
+
if title and count != 1:
|
|
298
|
+
sys.stderr.write("--title is only supported with --count 1.\\n")
|
|
264
299
|
return 2
|
|
265
300
|
anchor = before if before is not None else after + 1 # type: ignore[operator]
|
|
266
301
|
if anchor is None or anchor < 1:
|
|
267
|
-
sys.stderr.write(
|
|
302
|
+
sys.stderr.write("Anchor index must be >= 1.\\n")
|
|
268
303
|
return 2
|
|
269
304
|
try:
|
|
270
305
|
_shift(ticket_dir, anchor, count)
|
|
271
306
|
except ValueError as exc:
|
|
272
|
-
sys.stderr.write(str(exc) +
|
|
307
|
+
sys.stderr.write(str(exc) + "\\n")
|
|
273
308
|
return 1
|
|
309
|
+
if title:
|
|
310
|
+
tickets, errors = _ticket_files(ticket_dir)
|
|
311
|
+
if errors:
|
|
312
|
+
for msg in errors:
|
|
313
|
+
sys.stderr.write(msg + "\\n")
|
|
314
|
+
return 1
|
|
315
|
+
existing_indices = [t.index for t in tickets]
|
|
316
|
+
try:
|
|
317
|
+
path = _create_ticket_file(
|
|
318
|
+
ticket_dir, index=anchor, title=title, agent=agent, existing_indices=existing_indices
|
|
319
|
+
)
|
|
320
|
+
except ValueError as exc:
|
|
321
|
+
sys.stderr.write(str(exc) + "\\n")
|
|
322
|
+
return 1
|
|
323
|
+
sys.stdout.write(f"Inserted gap and created {path}\\n")
|
|
324
|
+
else:
|
|
325
|
+
sys.stdout.write(
|
|
326
|
+
f"Inserted gap at index {anchor}; run create --at {anchor} to add a ticket.\\n"
|
|
327
|
+
)
|
|
274
328
|
return 0
|
|
275
329
|
|
|
276
330
|
|
|
@@ -282,7 +336,7 @@ def _yaml_scalar(value: str) -> str:
|
|
|
282
336
|
|
|
283
337
|
escaped = (
|
|
284
338
|
value.replace("\\\\", "\\\\\\\\")
|
|
285
|
-
.replace('"', '
|
|
339
|
+
.replace('"', '\\\\"')
|
|
286
340
|
.replace("\\n", "\\\\n")
|
|
287
341
|
)
|
|
288
342
|
return f'"{escaped}"'
|
|
@@ -292,60 +346,52 @@ def cmd_create(ticket_dir: Path, *, title: str, agent: str, at: Optional[int]) -
|
|
|
292
346
|
tickets, errors = _ticket_files(ticket_dir)
|
|
293
347
|
if errors:
|
|
294
348
|
for msg in errors:
|
|
295
|
-
sys.stderr.write(msg +
|
|
349
|
+
sys.stderr.write(msg + "\\n")
|
|
296
350
|
return 1
|
|
297
351
|
existing_indices = [t.index for t in tickets]
|
|
298
352
|
next_index = max(existing_indices) + 1 if existing_indices else 1
|
|
299
353
|
index = at or next_index
|
|
300
354
|
if index in existing_indices:
|
|
301
355
|
sys.stderr.write(
|
|
302
|
-
f
|
|
356
|
+
f"Ticket index {index} already exists. Use insert to open a gap or choose --at another index.\\n"
|
|
303
357
|
)
|
|
304
358
|
return 1
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
f\"title: {title_scalar}\\n\"
|
|
314
|
-
f\"agent: {agent_scalar}\\n\"
|
|
315
|
-
f\"done: false\\n\"
|
|
316
|
-
f\"---\\n\\n\"
|
|
317
|
-
f\"## Goal\\n- \\n\"
|
|
318
|
-
)
|
|
319
|
-
path.write_text(body, encoding=\"utf-8\")
|
|
320
|
-
sys.stdout.write(f\"Created {path}\\n\")
|
|
359
|
+
try:
|
|
360
|
+
path = _create_ticket_file(
|
|
361
|
+
ticket_dir, index=index, title=title, agent=agent, existing_indices=existing_indices
|
|
362
|
+
)
|
|
363
|
+
except ValueError as exc:
|
|
364
|
+
sys.stderr.write(str(exc) + "\\n")
|
|
365
|
+
return 1
|
|
366
|
+
sys.stdout.write(f"Created {path}\\n")
|
|
321
367
|
return 0
|
|
322
368
|
|
|
323
369
|
|
|
324
370
|
def cmd_move(ticket_dir: Path, *, start: int, end: Optional[int], to: int) -> int:
|
|
325
371
|
if start < 1 or to < 1:
|
|
326
|
-
sys.stderr.write(
|
|
372
|
+
sys.stderr.write("Indices must be >= 1.\\n")
|
|
327
373
|
return 2
|
|
328
374
|
tickets, errors = _ticket_files(ticket_dir)
|
|
329
375
|
if errors:
|
|
330
376
|
for msg in errors:
|
|
331
|
-
sys.stderr.write(msg +
|
|
377
|
+
sys.stderr.write(msg + "\\n")
|
|
332
378
|
return 1
|
|
333
379
|
indices = [t.index for t in tickets]
|
|
334
380
|
if start not in indices:
|
|
335
|
-
sys.stderr.write(f
|
|
381
|
+
sys.stderr.write(f"No ticket at index {start}.\\n")
|
|
336
382
|
return 1
|
|
337
383
|
end_idx = end if end is not None else start
|
|
338
384
|
if end_idx < start:
|
|
339
|
-
sys.stderr.write(
|
|
385
|
+
sys.stderr.write("--end must be >= --start.\\n")
|
|
340
386
|
return 2
|
|
341
387
|
block = [t for t in tickets if start <= t.index <= end_idx]
|
|
342
388
|
if not block:
|
|
343
|
-
sys.stderr.write(
|
|
389
|
+
sys.stderr.write("No tickets in the specified move range.\\n")
|
|
344
390
|
return 1
|
|
345
391
|
remaining = [t for t in tickets if t not in block]
|
|
346
392
|
insert_pos = to - 1
|
|
347
393
|
if insert_pos < 0 or insert_pos > len(remaining):
|
|
348
|
-
sys.stderr.write(
|
|
394
|
+
sys.stderr.write("Target position is out of range.\\n")
|
|
349
395
|
return 1
|
|
350
396
|
new_order = remaining[:insert_pos] + block + remaining[insert_pos:]
|
|
351
397
|
width = _pad_width([t.index for t in new_order])
|
|
@@ -359,54 +405,70 @@ def cmd_move(ticket_dir: Path, *, start: int, end: Optional[int], to: int) -> in
|
|
|
359
405
|
|
|
360
406
|
|
|
361
407
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
362
|
-
parser = argparse.ArgumentParser(description
|
|
363
|
-
sub = parser.add_subparsers(dest
|
|
408
|
+
parser = argparse.ArgumentParser(description="Manage Codex Autorunner tickets.")
|
|
409
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
364
410
|
|
|
365
|
-
sub.add_parser(
|
|
366
|
-
sub.add_parser(
|
|
411
|
+
sub.add_parser("list", help="List tickets in order")
|
|
412
|
+
sub.add_parser("lint", help="Validate ticket filenames and frontmatter")
|
|
367
413
|
|
|
368
|
-
insert_p = sub.add_parser(
|
|
414
|
+
insert_p = sub.add_parser("insert", help="Insert gap by shifting tickets")
|
|
369
415
|
insert_group = insert_p.add_mutually_exclusive_group(required=True)
|
|
370
|
-
insert_group.add_argument(
|
|
371
|
-
insert_group.add_argument(
|
|
372
|
-
insert_p.add_argument(
|
|
416
|
+
insert_group.add_argument("--before", type=int, help="First index to shift upward")
|
|
417
|
+
insert_group.add_argument("--after", type=int, help="Shift tickets after this index")
|
|
418
|
+
insert_p.add_argument("--count", type=int, default=1, help="How many slots to insert (default 1)")
|
|
419
|
+
insert_p.add_argument(
|
|
420
|
+
"--title",
|
|
421
|
+
help="Create a ticket in the new slot (requires --count 1)",
|
|
422
|
+
)
|
|
423
|
+
insert_p.add_argument(
|
|
424
|
+
"--agent",
|
|
425
|
+
default="codex",
|
|
426
|
+
help="Frontmatter agent when creating with --title (default: codex)",
|
|
427
|
+
)
|
|
373
428
|
|
|
374
|
-
create_p = sub.add_parser(
|
|
375
|
-
create_p.add_argument(
|
|
376
|
-
create_p.add_argument(
|
|
429
|
+
create_p = sub.add_parser("create", help="Create a new ticket")
|
|
430
|
+
create_p.add_argument("--title", required=True, help="Ticket title")
|
|
431
|
+
create_p.add_argument("--agent", default="codex", help="Frontmatter agent (default: codex)")
|
|
377
432
|
create_p.add_argument(
|
|
378
|
-
|
|
433
|
+
"--at",
|
|
379
434
|
type=int,
|
|
380
|
-
help
|
|
435
|
+
help="Index to use (must be unused). Defaults to next available index.",
|
|
381
436
|
)
|
|
382
437
|
|
|
383
|
-
move_p = sub.add_parser(
|
|
384
|
-
move_p.add_argument(
|
|
385
|
-
move_p.add_argument(
|
|
386
|
-
move_p.add_argument(
|
|
438
|
+
move_p = sub.add_parser("move", help="Move a ticket or block to a new position")
|
|
439
|
+
move_p.add_argument("--start", type=int, required=True, help="First index in the block to move")
|
|
440
|
+
move_p.add_argument("--end", type=int, help="Last index in the block (defaults to start)")
|
|
441
|
+
move_p.add_argument("--to", type=int, required=True, help="Destination position (1-indexed)")
|
|
387
442
|
|
|
388
443
|
args = parser.parse_args(argv)
|
|
389
444
|
repo_root = Path.cwd()
|
|
390
445
|
ticket_dir = _ticket_dir(repo_root)
|
|
391
446
|
if not ticket_dir.exists():
|
|
392
|
-
sys.stderr.write(f
|
|
447
|
+
sys.stderr.write(f"Tickets directory not found: {ticket_dir}\\n")
|
|
393
448
|
return 2
|
|
394
449
|
|
|
395
|
-
if args.cmd ==
|
|
450
|
+
if args.cmd == "list":
|
|
396
451
|
return cmd_list(ticket_dir)
|
|
397
|
-
if args.cmd ==
|
|
452
|
+
if args.cmd == "lint":
|
|
398
453
|
return cmd_lint(ticket_dir)
|
|
399
|
-
if args.cmd ==
|
|
400
|
-
return cmd_insert(
|
|
401
|
-
|
|
454
|
+
if args.cmd == "insert":
|
|
455
|
+
return cmd_insert(
|
|
456
|
+
ticket_dir,
|
|
457
|
+
before=args.before,
|
|
458
|
+
after=args.after,
|
|
459
|
+
count=args.count,
|
|
460
|
+
title=args.title,
|
|
461
|
+
agent=args.agent,
|
|
462
|
+
)
|
|
463
|
+
if args.cmd == "create":
|
|
402
464
|
return cmd_create(ticket_dir, title=args.title, agent=args.agent, at=args.at)
|
|
403
|
-
if args.cmd ==
|
|
465
|
+
if args.cmd == "move":
|
|
404
466
|
return cmd_move(ticket_dir, start=args.start, end=args.end, to=args.to)
|
|
405
|
-
parser.error(
|
|
467
|
+
parser.error("Unknown command")
|
|
406
468
|
return 2
|
|
407
469
|
|
|
408
470
|
|
|
409
|
-
if __name__ ==
|
|
471
|
+
if __name__ == "__main__": # pragma: no cover
|
|
410
472
|
sys.exit(main())
|
|
411
473
|
"""
|
|
412
474
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Core type definitions.
|
|
2
|
+
|
|
3
|
+
This module provides type definitions that were previously in Engine.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
# Type aliases for factory functions
|
|
9
|
+
BackendFactory = Callable[[str, Any, Optional[Callable[[dict[str, Any]], Any]]], Any]
|
|
10
|
+
AppServerSupervisorFactory = Callable[
|
|
11
|
+
[str, Optional[Callable[[dict[str, Any]], Any]]], Any
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BackendFactory",
|
|
17
|
+
"AppServerSupervisorFactory",
|
|
18
|
+
]
|
codex_autorunner/core/utils.py
CHANGED
|
@@ -147,12 +147,21 @@ def is_within(root: Path, target: Path) -> bool:
|
|
|
147
147
|
return False
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
def atomic_write(path: Path, content: str) -> None:
|
|
150
|
+
def atomic_write(path: Path, content: str, durable: bool = False) -> None:
|
|
151
151
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
152
|
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
153
153
|
with tmp_path.open("w", encoding="utf-8") as f:
|
|
154
154
|
f.write(content)
|
|
155
|
+
if durable:
|
|
156
|
+
f.flush()
|
|
157
|
+
os.fsync(f.fileno())
|
|
155
158
|
tmp_path.replace(path)
|
|
159
|
+
if durable:
|
|
160
|
+
dir_fd = os.open(path.parent, os.O_RDONLY)
|
|
161
|
+
try:
|
|
162
|
+
os.fsync(dir_fd)
|
|
163
|
+
finally:
|
|
164
|
+
os.close(dir_fd)
|
|
156
165
|
|
|
157
166
|
|
|
158
167
|
def read_json(path: Path) -> Optional[dict]:
|
|
@@ -175,7 +184,28 @@ def _default_path_prefixes() -> list[str]:
|
|
|
175
184
|
str(home / ".opencode" / "bin"), # OpenCode default install
|
|
176
185
|
str(home / ".local" / "bin"), # Common user-local installs
|
|
177
186
|
]
|
|
178
|
-
|
|
187
|
+
repo_candidates: list[str] = []
|
|
188
|
+
repo_root = get_repo_root_context()
|
|
189
|
+
if repo_root is None:
|
|
190
|
+
try:
|
|
191
|
+
repo_root = find_repo_root()
|
|
192
|
+
except RepoNotFoundError:
|
|
193
|
+
repo_root = None
|
|
194
|
+
if repo_root is not None:
|
|
195
|
+
bin_dir = "Scripts" if os.name == "nt" else "bin"
|
|
196
|
+
repo_candidates = [
|
|
197
|
+
str(repo_root / ".venv" / bin_dir),
|
|
198
|
+
str(repo_root / ".codex-autorunner" / "bin"),
|
|
199
|
+
str(repo_root / "bin"),
|
|
200
|
+
]
|
|
201
|
+
car_shim = repo_root / "car"
|
|
202
|
+
if car_shim.exists():
|
|
203
|
+
repo_candidates.append(str(repo_root))
|
|
204
|
+
return [
|
|
205
|
+
p
|
|
206
|
+
for p in (repo_candidates + candidates)
|
|
207
|
+
if (os.path.isdir(p) or os.path.isfile(p))
|
|
208
|
+
]
|
|
179
209
|
|
|
180
210
|
|
|
181
211
|
def augmented_path(path: Optional[str] = None) -> str:
|
|
@@ -227,10 +257,6 @@ def resolve_executable(
|
|
|
227
257
|
return resolved
|
|
228
258
|
|
|
229
259
|
|
|
230
|
-
def ensure_executable(binary: str) -> bool:
|
|
231
|
-
return resolve_executable(binary) is not None
|
|
232
|
-
|
|
233
|
-
|
|
234
260
|
def default_editor(*, fallback: str = "vi") -> str:
|
|
235
261
|
return os.environ.get("EDITOR") or fallback
|
|
236
262
|
|
|
@@ -295,6 +321,7 @@ def build_opencode_supervisor(
|
|
|
295
321
|
max_handles: Optional[int] = None,
|
|
296
322
|
idle_ttl_seconds: Optional[float] = None,
|
|
297
323
|
session_stall_timeout_seconds: Optional[float] = None,
|
|
324
|
+
max_text_chars: Optional[int] = None,
|
|
298
325
|
base_env: Optional[MutableMapping[str, str]] = None,
|
|
299
326
|
subagent_models: Optional[Mapping[str, str]] = None,
|
|
300
327
|
) -> Optional[Any]:
|
|
@@ -352,6 +379,7 @@ def build_opencode_supervisor(
|
|
|
352
379
|
max_handles=max_handles,
|
|
353
380
|
idle_ttl_seconds=idle_ttl_seconds,
|
|
354
381
|
session_stall_timeout_seconds=session_stall_timeout_seconds,
|
|
382
|
+
max_text_chars=max_text_chars,
|
|
355
383
|
username=username if password else None,
|
|
356
384
|
password=password if password else None,
|
|
357
385
|
base_env=base_env,
|