codex-autorunner 0.1.2__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +118 -30
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +136 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +16 -35
- codex_autorunner/cli.py +157 -139
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +390 -100
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +278 -262
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +15 -9
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/utils.py +29 -2
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +576 -92
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +141 -167
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +175 -0
- codex_autorunner/integrations/telegram/constants.py +16 -1
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
- codex_autorunner/integrations/telegram/helpers.py +88 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +214 -40
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +36 -3
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +23 -14
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +81 -109
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/system.py +6 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +1 -0
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +25 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +162 -196
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +41 -118
- codex_autorunner/static/index.html +787 -858
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -211
- codex_autorunner/static/styles.css +7567 -3865
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/app.py +269 -91
- codex_autorunner/web/middleware.py +3 -4
- codex_autorunner/web/schemas.py +89 -109
- codex_autorunner/web/static_assets.py +1 -44
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import IO, Literal, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
_WORKER_METADATA_FILENAME = "worker.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FlowWorkerHealth:
|
|
17
|
+
status: Literal["absent", "alive", "dead", "invalid", "mismatch"]
|
|
18
|
+
pid: Optional[int]
|
|
19
|
+
cmdline: list[str]
|
|
20
|
+
artifact_path: Path
|
|
21
|
+
message: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_alive(self) -> bool:
|
|
25
|
+
return self.status == "alive"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalized_run_id(run_id: str) -> str:
|
|
29
|
+
return str(uuid.UUID(str(run_id)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _worker_artifacts_dir(
|
|
33
|
+
repo_root: Path, run_id: str, artifacts_root: Optional[Path] = None
|
|
34
|
+
) -> Path:
|
|
35
|
+
repo_root = repo_root.resolve()
|
|
36
|
+
base_artifacts = (
|
|
37
|
+
artifacts_root
|
|
38
|
+
if artifacts_root is not None
|
|
39
|
+
else repo_root / ".codex-autorunner" / "flows"
|
|
40
|
+
)
|
|
41
|
+
artifacts_dir = base_artifacts / _normalized_run_id(run_id)
|
|
42
|
+
artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
return artifacts_dir
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _worker_metadata_path(artifacts_dir: Path) -> Path:
|
|
47
|
+
return artifacts_dir / _WORKER_METADATA_FILENAME
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _build_worker_cmd(entrypoint: str, run_id: str) -> list[str]:
|
|
51
|
+
normalized_run_id = _normalized_run_id(run_id)
|
|
52
|
+
return [
|
|
53
|
+
sys.executable,
|
|
54
|
+
"-m",
|
|
55
|
+
entrypoint,
|
|
56
|
+
"flow",
|
|
57
|
+
"worker",
|
|
58
|
+
"--run-id",
|
|
59
|
+
normalized_run_id,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pid_is_running(pid: int) -> bool:
|
|
64
|
+
try:
|
|
65
|
+
os.kill(pid, 0)
|
|
66
|
+
except ProcessLookupError:
|
|
67
|
+
return False
|
|
68
|
+
except PermissionError:
|
|
69
|
+
# Process exists but we may not own it.
|
|
70
|
+
return True
|
|
71
|
+
except OSError:
|
|
72
|
+
return False
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_process_cmdline(pid: int) -> list[str] | None:
|
|
77
|
+
proc_path = Path(f"/proc/{pid}/cmdline")
|
|
78
|
+
if proc_path.exists():
|
|
79
|
+
try:
|
|
80
|
+
raw = proc_path.read_bytes()
|
|
81
|
+
return [part for part in raw.decode().split("\0") if part]
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
out = subprocess.check_output(
|
|
87
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
88
|
+
stderr=subprocess.DEVNULL,
|
|
89
|
+
)
|
|
90
|
+
cmd = out.decode().strip()
|
|
91
|
+
if cmd:
|
|
92
|
+
return cmd.split()
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cmdline_matches(expected: list[str], actual: list[str]) -> bool:
|
|
99
|
+
if not expected or not actual:
|
|
100
|
+
return False
|
|
101
|
+
if len(actual) >= len(expected) and actual[-len(expected) :] == expected:
|
|
102
|
+
return True
|
|
103
|
+
expected_str = " ".join(expected)
|
|
104
|
+
actual_str = " ".join(actual)
|
|
105
|
+
return expected_str in actual_str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _write_worker_metadata(path: Path, pid: int, cmd: list[str]) -> None:
|
|
109
|
+
data = {
|
|
110
|
+
"pid": pid,
|
|
111
|
+
"cmd": cmd,
|
|
112
|
+
"cwd": os.getcwd(),
|
|
113
|
+
}
|
|
114
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
115
|
+
# Also emit a plain PID file for quick inspection.
|
|
116
|
+
pid_path = path.with_suffix(".pid")
|
|
117
|
+
pid_path.write_text(str(pid), encoding="utf-8")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def clear_worker_metadata(artifacts_dir: Path) -> None:
|
|
121
|
+
for name in (
|
|
122
|
+
_WORKER_METADATA_FILENAME,
|
|
123
|
+
f"{Path(_WORKER_METADATA_FILENAME).stem}.pid",
|
|
124
|
+
):
|
|
125
|
+
try:
|
|
126
|
+
(artifacts_dir / name).unlink()
|
|
127
|
+
except FileNotFoundError:
|
|
128
|
+
pass
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check_worker_health(
|
|
134
|
+
repo_root: Path,
|
|
135
|
+
run_id: str,
|
|
136
|
+
*,
|
|
137
|
+
artifacts_root: Optional[Path] = None,
|
|
138
|
+
entrypoint: str = "codex_autorunner",
|
|
139
|
+
) -> FlowWorkerHealth:
|
|
140
|
+
artifacts_dir = _worker_artifacts_dir(repo_root, run_id, artifacts_root)
|
|
141
|
+
metadata_path = _worker_metadata_path(artifacts_dir)
|
|
142
|
+
|
|
143
|
+
if not metadata_path.exists():
|
|
144
|
+
return FlowWorkerHealth(
|
|
145
|
+
status="absent",
|
|
146
|
+
pid=None,
|
|
147
|
+
cmdline=[],
|
|
148
|
+
artifact_path=metadata_path,
|
|
149
|
+
message="worker metadata missing",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
data = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
154
|
+
pid = int(data.get("pid")) if data.get("pid") is not None else None
|
|
155
|
+
cmd = data.get("cmd") or []
|
|
156
|
+
except Exception:
|
|
157
|
+
return FlowWorkerHealth(
|
|
158
|
+
status="invalid",
|
|
159
|
+
pid=None,
|
|
160
|
+
cmdline=[],
|
|
161
|
+
artifact_path=metadata_path,
|
|
162
|
+
message="worker metadata unreadable",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if not pid or pid <= 0:
|
|
166
|
+
return FlowWorkerHealth(
|
|
167
|
+
status="invalid",
|
|
168
|
+
pid=pid,
|
|
169
|
+
cmdline=cmd if isinstance(cmd, list) else [],
|
|
170
|
+
artifact_path=metadata_path,
|
|
171
|
+
message="missing or invalid PID",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not _pid_is_running(pid):
|
|
175
|
+
return FlowWorkerHealth(
|
|
176
|
+
status="dead",
|
|
177
|
+
pid=pid,
|
|
178
|
+
cmdline=cmd if isinstance(cmd, list) else [],
|
|
179
|
+
artifact_path=metadata_path,
|
|
180
|
+
message="worker PID not running",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
expected_cmd = _build_worker_cmd(entrypoint, run_id)
|
|
184
|
+
actual_cmd = _read_process_cmdline(pid)
|
|
185
|
+
if actual_cmd is None:
|
|
186
|
+
# Can't inspect cmdline; trust the PID check.
|
|
187
|
+
return FlowWorkerHealth(
|
|
188
|
+
status="alive",
|
|
189
|
+
pid=pid,
|
|
190
|
+
cmdline=cmd if isinstance(cmd, list) else [],
|
|
191
|
+
artifact_path=metadata_path,
|
|
192
|
+
message="worker running (cmdline unknown)",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if not _cmdline_matches(expected_cmd, actual_cmd):
|
|
196
|
+
return FlowWorkerHealth(
|
|
197
|
+
status="mismatch",
|
|
198
|
+
pid=pid,
|
|
199
|
+
cmdline=actual_cmd,
|
|
200
|
+
artifact_path=metadata_path,
|
|
201
|
+
message="worker PID command does not match expected",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return FlowWorkerHealth(
|
|
205
|
+
status="alive",
|
|
206
|
+
pid=pid,
|
|
207
|
+
cmdline=actual_cmd,
|
|
208
|
+
artifact_path=metadata_path,
|
|
209
|
+
message="worker running",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def spawn_flow_worker(
|
|
214
|
+
repo_root: Path,
|
|
215
|
+
run_id: str,
|
|
216
|
+
*,
|
|
217
|
+
artifacts_root: Optional[Path] = None,
|
|
218
|
+
entrypoint: str = "codex_autorunner",
|
|
219
|
+
) -> Tuple[subprocess.Popen, IO[bytes], IO[bytes]]:
|
|
220
|
+
"""Spawn a detached flow worker with consistent artifacts/log layout."""
|
|
221
|
+
|
|
222
|
+
normalized_run_id = _normalized_run_id(run_id)
|
|
223
|
+
repo_root = repo_root.resolve()
|
|
224
|
+
artifacts_dir = _worker_artifacts_dir(repo_root, normalized_run_id, artifacts_root)
|
|
225
|
+
|
|
226
|
+
stdout_path = artifacts_dir / "worker.out.log"
|
|
227
|
+
stderr_path = artifacts_dir / "worker.err.log"
|
|
228
|
+
|
|
229
|
+
stdout_handle = stdout_path.open("ab")
|
|
230
|
+
stderr_handle = stderr_path.open("ab")
|
|
231
|
+
|
|
232
|
+
cmd = _build_worker_cmd(entrypoint, normalized_run_id)
|
|
233
|
+
|
|
234
|
+
proc = subprocess.Popen(
|
|
235
|
+
cmd,
|
|
236
|
+
cwd=repo_root,
|
|
237
|
+
stdout=stdout_handle,
|
|
238
|
+
stderr=stderr_handle,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
_write_worker_metadata(_worker_metadata_path(artifacts_dir), proc.pid, cmd)
|
|
242
|
+
return proc, stdout_handle, stderr_handle
|
codex_autorunner/core/hub.py
CHANGED
|
@@ -137,7 +137,8 @@ def load_hub_state(state_path: Path, hub_root: Path) -> HubState:
|
|
|
137
137
|
import json
|
|
138
138
|
|
|
139
139
|
payload = json.loads(data)
|
|
140
|
-
except Exception:
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
logger.warning("Failed to parse hub state from %s: %s", state_path, exc)
|
|
141
142
|
return HubState(last_scan_at=None, repos=[])
|
|
142
143
|
last_scan_at = payload.get("last_scan_at")
|
|
143
144
|
repos_payload = payload.get("repos") or []
|
|
@@ -168,7 +169,13 @@ def load_hub_state(state_path: Path, hub_root: Path) -> HubState:
|
|
|
168
169
|
runner_pid=entry.get("runner_pid"),
|
|
169
170
|
)
|
|
170
171
|
repos.append(repo)
|
|
171
|
-
except Exception:
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
repo_id = entry.get("id", "unknown")
|
|
174
|
+
logger.warning(
|
|
175
|
+
"Failed to load repo snapshot for id=%s from hub state: %s",
|
|
176
|
+
repo_id,
|
|
177
|
+
exc,
|
|
178
|
+
)
|
|
172
179
|
continue
|
|
173
180
|
return HubState(last_scan_at=last_scan_at, repos=repos)
|
|
174
181
|
|
|
@@ -759,10 +766,10 @@ class HubSupervisor:
|
|
|
759
766
|
if not repo:
|
|
760
767
|
raise ValueError(f"Repo {repo_id} not found in manifest")
|
|
761
768
|
repo_root = (self.hub_config.root / repo.path).resolve()
|
|
762
|
-
|
|
763
|
-
if not allow_uninitialized and not
|
|
769
|
+
tickets_dir = repo_root / ".codex-autorunner" / "tickets"
|
|
770
|
+
if not allow_uninitialized and not tickets_dir.exists():
|
|
764
771
|
raise ValueError(f"Repo {repo_id} is not initialized")
|
|
765
|
-
if not
|
|
772
|
+
if not tickets_dir.exists():
|
|
766
773
|
return None
|
|
767
774
|
repo_config = derive_repo_config(self.hub_config, repo_root, load_env=False)
|
|
768
775
|
runner = RepoRunner(
|
|
@@ -781,7 +788,7 @@ class HubSupervisor:
|
|
|
781
788
|
records: List[DiscoveryRecord] = []
|
|
782
789
|
for entry in manifest.repos:
|
|
783
790
|
repo_path = (self.hub_config.root / entry.path).resolve()
|
|
784
|
-
initialized = (repo_path / ".codex-autorunner" / "
|
|
791
|
+
initialized = (repo_path / ".codex-autorunner" / "tickets").exists()
|
|
785
792
|
records.append(
|
|
786
793
|
DiscoveryRecord(
|
|
787
794
|
repo=entry,
|
|
@@ -821,9 +828,8 @@ class HubSupervisor:
|
|
|
821
828
|
lock_status = read_lock_status(lock_path)
|
|
822
829
|
|
|
823
830
|
runner_state: Optional[RunnerState] = None
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
runner_state = load_state(state_path)
|
|
831
|
+
if record.initialized:
|
|
832
|
+
runner_state = load_state(repo_path / ".codex-autorunner" / "state.sqlite3")
|
|
827
833
|
|
|
828
834
|
is_clean: Optional[bool] = None
|
|
829
835
|
if record.exists_on_disk and git_available(repo_path):
|
codex_autorunner/core/locks.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import errno
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import socket
|
|
5
6
|
import subprocess
|
|
@@ -27,6 +28,7 @@ class LockAssessment:
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
DEFAULT_RUNNER_CMD_HINTS = ("codex_autorunner.cli", "codex-autorunner", "car ")
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def process_alive(pid: int) -> bool:
|
|
@@ -48,6 +50,7 @@ def process_is_zombie(pid: int) -> bool:
|
|
|
48
50
|
check=False,
|
|
49
51
|
)
|
|
50
52
|
except Exception:
|
|
53
|
+
logger.debug("Failed to check process status for pid %s", pid, exc_info=True)
|
|
51
54
|
return False
|
|
52
55
|
if result.returncode != 0:
|
|
53
56
|
return False
|
|
@@ -65,6 +68,7 @@ def process_command(pid: int) -> Optional[str]:
|
|
|
65
68
|
check=False,
|
|
66
69
|
)
|
|
67
70
|
except Exception:
|
|
71
|
+
logger.debug("Failed to inspect process command for pid %s", pid, exc_info=True)
|
|
68
72
|
return None
|
|
69
73
|
if result.returncode != 0:
|
|
70
74
|
return None
|
codex_autorunner/core/prompt.py
CHANGED
|
@@ -15,12 +15,20 @@ def _display_path(root: Path, path: Path) -> str:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def build_doc_paths(config: Config) -> Mapping[str, str]:
|
|
18
|
+
def _safe_path(*keys: str) -> str:
|
|
19
|
+
for key in keys:
|
|
20
|
+
try:
|
|
21
|
+
return _display_path(config.root, config.doc_path(key))
|
|
22
|
+
except KeyError:
|
|
23
|
+
continue
|
|
24
|
+
return ""
|
|
25
|
+
|
|
18
26
|
return {
|
|
19
|
-
"todo":
|
|
20
|
-
"progress":
|
|
21
|
-
"opinions":
|
|
22
|
-
"spec":
|
|
23
|
-
"summary":
|
|
27
|
+
"todo": _safe_path("todo", "active_context"),
|
|
28
|
+
"progress": _safe_path("progress", "decisions"),
|
|
29
|
+
"opinions": _safe_path("opinions"),
|
|
30
|
+
"spec": _safe_path("spec"),
|
|
31
|
+
"summary": _safe_path("summary"),
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
|
|
@@ -64,8 +72,8 @@ def build_final_summary_prompt(
|
|
|
64
72
|
|
|
65
73
|
doc_paths = build_doc_paths(config)
|
|
66
74
|
doc_contents = {
|
|
67
|
-
"todo": docs.read_doc("todo"),
|
|
68
|
-
"progress": docs.read_doc("progress"),
|
|
75
|
+
"todo": docs.read_doc("todo") or docs.read_doc("active_context"),
|
|
76
|
+
"progress": docs.read_doc("progress") or docs.read_doc("decisions"),
|
|
69
77
|
"opinions": docs.read_doc("opinions"),
|
|
70
78
|
"spec": docs.read_doc("spec"),
|
|
71
79
|
"summary": docs.read_doc("summary"),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
|
|
4
|
+
_REDACTIONS: List[Tuple[re.Pattern[str], str]] = [
|
|
5
|
+
# OpenAI-like keys.
|
|
6
|
+
(re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), "sk-[REDACTED]"),
|
|
7
|
+
# GitHub personal access tokens.
|
|
8
|
+
(re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}\b"), "gh_[REDACTED]"),
|
|
9
|
+
# AWS access key ids (best-effort).
|
|
10
|
+
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "AKIA[REDACTED]"),
|
|
11
|
+
# JWT-ish blobs.
|
|
12
|
+
(
|
|
13
|
+
re.compile(
|
|
14
|
+
r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"
|
|
15
|
+
),
|
|
16
|
+
"[JWT_REDACTED]",
|
|
17
|
+
),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def redact_text(text: str) -> str:
|
|
22
|
+
redacted = text
|
|
23
|
+
for pattern, replacement in _REDACTIONS:
|
|
24
|
+
redacted = pattern.sub(replacement, redacted)
|
|
25
|
+
return redacted
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_redaction_patterns() -> List[str]:
|
|
29
|
+
return [pattern.pattern for pattern, _ in _REDACTIONS]
|
|
@@ -97,13 +97,10 @@ def build_spec_progress_review_context(
|
|
|
97
97
|
remaining = 0
|
|
98
98
|
|
|
99
99
|
def doc_label(name: str) -> str:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"summary": "SUMMARY.md",
|
|
105
|
-
}
|
|
106
|
-
return mapping.get(name.lower(), name)
|
|
100
|
+
try:
|
|
101
|
+
return engine.config.doc_path(name).relative_to(engine.repo_root).as_posix()
|
|
102
|
+
except Exception:
|
|
103
|
+
return name
|
|
107
104
|
|
|
108
105
|
def read_doc(name: str) -> str:
|
|
109
106
|
try:
|
|
@@ -122,7 +119,7 @@ def build_spec_progress_review_context(
|
|
|
122
119
|
|
|
123
120
|
primary_list = [doc for doc in primary_docs if isinstance(doc, str)] or [
|
|
124
121
|
"spec",
|
|
125
|
-
"
|
|
122
|
+
"active_context",
|
|
126
123
|
]
|
|
127
124
|
primary_set = {doc.lower() for doc in primary_list}
|
|
128
125
|
|
|
@@ -197,6 +197,8 @@ class RunIndexStore:
|
|
|
197
197
|
*,
|
|
198
198
|
log_path: str,
|
|
199
199
|
run_log_path: str,
|
|
200
|
+
actor: Optional[dict[str, Any]] = None,
|
|
201
|
+
mode: Optional[dict[str, Any]] = None,
|
|
200
202
|
) -> dict[str, Any]:
|
|
201
203
|
with open_sqlite(self._path) as conn:
|
|
202
204
|
self._ensure_schema(conn)
|
|
@@ -207,6 +209,10 @@ class RunIndexStore:
|
|
|
207
209
|
entry["started_at"] = now_iso()
|
|
208
210
|
entry["log_path"] = log_path
|
|
209
211
|
entry["run_log_path"] = run_log_path
|
|
212
|
+
if actor is not None:
|
|
213
|
+
entry["actor"] = actor
|
|
214
|
+
if mode is not None:
|
|
215
|
+
entry["mode"] = mode
|
|
210
216
|
elif marker == "end":
|
|
211
217
|
entry["end_offset"] = offset[1] if offset else None
|
|
212
218
|
entry["finished_at"] = now_iso()
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import atexit
|
|
4
|
+
import logging
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Set
|
|
8
9
|
|
|
9
10
|
_process_registry: Set[subprocess.Popen] = set()
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def build_runner_cmd(repo_root: Path, *, action: str, once: bool = False) -> list[str]:
|
|
@@ -41,14 +43,15 @@ def cleanup_processes() -> None:
|
|
|
41
43
|
try:
|
|
42
44
|
proc.terminate()
|
|
43
45
|
except Exception:
|
|
44
|
-
|
|
46
|
+
logger.debug("Failed to terminate runner process", exc_info=True)
|
|
45
47
|
try:
|
|
46
48
|
proc.wait(timeout=5)
|
|
47
49
|
except Exception:
|
|
50
|
+
logger.debug("Runner process wait timed out", exc_info=True)
|
|
48
51
|
try:
|
|
49
52
|
proc.kill()
|
|
50
53
|
except Exception:
|
|
51
|
-
|
|
54
|
+
logger.debug("Failed to kill runner process", exc_info=True)
|
|
52
55
|
_process_registry.clear()
|
|
53
56
|
|
|
54
57
|
|
codex_autorunner/core/state.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import json
|
|
3
|
-
import logging
|
|
4
3
|
from contextlib import contextmanager
|
|
5
4
|
from datetime import datetime, timezone
|
|
6
5
|
from pathlib import Path
|
|
@@ -9,8 +8,6 @@ from typing import Any, Iterator, Optional
|
|
|
9
8
|
from .locks import file_lock
|
|
10
9
|
from .sqlite_utils import open_sqlite
|
|
11
10
|
|
|
12
|
-
_logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
11
|
|
|
15
12
|
@dataclasses.dataclass
|
|
16
13
|
class RunnerState:
|
|
@@ -100,75 +97,6 @@ def now_iso() -> str:
|
|
|
100
97
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
101
98
|
|
|
102
99
|
|
|
103
|
-
def _coerce_int(value: Any) -> Optional[int]:
|
|
104
|
-
if isinstance(value, int) and not isinstance(value, bool):
|
|
105
|
-
return value
|
|
106
|
-
return None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _coerce_str(value: Any) -> Optional[str]:
|
|
110
|
-
if isinstance(value, str) and value:
|
|
111
|
-
return value
|
|
112
|
-
return None
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _load_legacy_state_json(path: Path) -> Optional[RunnerState]:
|
|
116
|
-
try:
|
|
117
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
118
|
-
except (OSError, json.JSONDecodeError):
|
|
119
|
-
return None
|
|
120
|
-
if not isinstance(payload, dict):
|
|
121
|
-
return None
|
|
122
|
-
state = RunnerState(
|
|
123
|
-
last_run_id=_coerce_int(payload.get("last_run_id")),
|
|
124
|
-
status=_coerce_str(payload.get("status")) or "idle",
|
|
125
|
-
last_exit_code=_coerce_int(payload.get("last_exit_code")),
|
|
126
|
-
last_run_started_at=_coerce_str(payload.get("last_run_started_at")),
|
|
127
|
-
last_run_finished_at=_coerce_str(payload.get("last_run_finished_at")),
|
|
128
|
-
autorunner_agent_override=_coerce_str(payload.get("autorunner_agent_override")),
|
|
129
|
-
autorunner_model_override=_coerce_str(payload.get("autorunner_model_override")),
|
|
130
|
-
autorunner_effort_override=_coerce_str(
|
|
131
|
-
payload.get("autorunner_effort_override")
|
|
132
|
-
),
|
|
133
|
-
autorunner_approval_policy=_coerce_str(
|
|
134
|
-
payload.get("autorunner_approval_policy")
|
|
135
|
-
),
|
|
136
|
-
autorunner_sandbox_mode=_coerce_str(payload.get("autorunner_sandbox_mode")),
|
|
137
|
-
autorunner_workspace_write_network=(
|
|
138
|
-
payload.get("autorunner_workspace_write_network")
|
|
139
|
-
if isinstance(payload.get("autorunner_workspace_write_network"), bool)
|
|
140
|
-
else None
|
|
141
|
-
),
|
|
142
|
-
runner_stop_after_runs=_coerce_int(payload.get("runner_stop_after_runs")),
|
|
143
|
-
runner_pid=_coerce_int(payload.get("runner_pid")),
|
|
144
|
-
)
|
|
145
|
-
sessions: dict[str, SessionRecord] = {}
|
|
146
|
-
sessions_payload = payload.get("sessions")
|
|
147
|
-
if isinstance(sessions_payload, dict):
|
|
148
|
-
for session_id, record_payload in sessions_payload.items():
|
|
149
|
-
if not isinstance(session_id, str) or not session_id:
|
|
150
|
-
continue
|
|
151
|
-
record = (
|
|
152
|
-
SessionRecord.from_dict(record_payload)
|
|
153
|
-
if isinstance(record_payload, dict)
|
|
154
|
-
else None
|
|
155
|
-
)
|
|
156
|
-
if record is not None:
|
|
157
|
-
sessions[session_id] = record
|
|
158
|
-
repo_to_session: dict[str, str] = {}
|
|
159
|
-
repo_payload = payload.get("repo_to_session")
|
|
160
|
-
if isinstance(repo_payload, dict):
|
|
161
|
-
for repo_key, session_id in repo_payload.items():
|
|
162
|
-
if not isinstance(repo_key, str) or not repo_key:
|
|
163
|
-
continue
|
|
164
|
-
if not isinstance(session_id, str) or not session_id:
|
|
165
|
-
continue
|
|
166
|
-
repo_to_session[repo_key] = session_id
|
|
167
|
-
state.sessions = sessions
|
|
168
|
-
state.repo_to_session = repo_to_session
|
|
169
|
-
return state
|
|
170
|
-
|
|
171
|
-
|
|
172
100
|
def _ensure_state_schema(conn) -> None:
|
|
173
101
|
with conn:
|
|
174
102
|
conn.execute(
|
|
@@ -270,22 +198,6 @@ def _apply_overrides(state: RunnerState, raw: Optional[str]) -> None:
|
|
|
270
198
|
|
|
271
199
|
|
|
272
200
|
def load_state(state_path: Path) -> RunnerState:
|
|
273
|
-
legacy_path = state_path.with_name("state.json")
|
|
274
|
-
# Legacy JSON migration (remove after old state.json is retired).
|
|
275
|
-
if not state_path.exists() and legacy_path.exists():
|
|
276
|
-
migrated = _load_legacy_state_json(legacy_path)
|
|
277
|
-
if migrated is not None:
|
|
278
|
-
try:
|
|
279
|
-
save_state(state_path, migrated)
|
|
280
|
-
return migrated
|
|
281
|
-
except Exception:
|
|
282
|
-
_logger.warning(
|
|
283
|
-
"Failed to migrate legacy state from %s to %s. The original JSON file is preserved.",
|
|
284
|
-
legacy_path,
|
|
285
|
-
state_path,
|
|
286
|
-
exc_info=True,
|
|
287
|
-
)
|
|
288
|
-
raise
|
|
289
201
|
with open_sqlite(state_path) as conn:
|
|
290
202
|
_ensure_state_schema(conn)
|
|
291
203
|
row = conn.execute(
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import ExitStack
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
# Keep the required asset list close to the core boundary so core modules do not
|
|
9
|
+
# import from codex_autorunner.web.*
|
|
10
|
+
_REQUIRED_STATIC_ASSETS = (
|
|
11
|
+
"index.html",
|
|
12
|
+
"styles.css",
|
|
13
|
+
"bootstrap.js",
|
|
14
|
+
"loader.js",
|
|
15
|
+
"app.js",
|
|
16
|
+
"vendor/xterm.js",
|
|
17
|
+
"vendor/xterm-addon-fit.js",
|
|
18
|
+
"vendor/xterm.css",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def missing_static_assets(static_dir: Path) -> list[str]:
|
|
23
|
+
missing: list[str] = []
|
|
24
|
+
for rel_path in _REQUIRED_STATIC_ASSETS:
|
|
25
|
+
try:
|
|
26
|
+
if not (static_dir / rel_path).exists():
|
|
27
|
+
missing.append(rel_path)
|
|
28
|
+
except OSError:
|
|
29
|
+
missing.append(rel_path)
|
|
30
|
+
return missing
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_static_dir() -> tuple[Path, Optional[ExitStack]]:
|
|
34
|
+
"""Locate packaged static assets without importing codex_autorunner.web."""
|
|
35
|
+
|
|
36
|
+
static_root = resources.files("codex_autorunner").joinpath("static")
|
|
37
|
+
if isinstance(static_root, Path):
|
|
38
|
+
if static_root.exists():
|
|
39
|
+
return static_root, None
|
|
40
|
+
fallback = Path(__file__).resolve().parent.parent / "static"
|
|
41
|
+
return fallback, None
|
|
42
|
+
|
|
43
|
+
stack = ExitStack()
|
|
44
|
+
try:
|
|
45
|
+
static_path = stack.enter_context(resources.as_file(static_root))
|
|
46
|
+
except Exception:
|
|
47
|
+
stack.close()
|
|
48
|
+
fallback = Path(__file__).resolve().parent.parent / "static"
|
|
49
|
+
return fallback, None
|
|
50
|
+
if static_path.exists():
|
|
51
|
+
return static_path, stack
|
|
52
|
+
|
|
53
|
+
stack.close()
|
|
54
|
+
fallback = Path(__file__).resolve().parent.parent / "static"
|
|
55
|
+
return fallback, None
|