codex-autorunner 0.1.1__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/__init__.py +20 -0
- codex_autorunner/agents/base.py +2 -2
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/__init__.py +4 -0
- codex_autorunner/agents/opencode/agent_config.py +104 -0
- codex_autorunner/agents/opencode/client.py +305 -28
- codex_autorunner/agents/opencode/harness.py +71 -20
- codex_autorunner/agents/opencode/logging.py +225 -0
- codex_autorunner/agents/opencode/run_prompt.py +261 -0
- codex_autorunner/agents/opencode/runtime.py +1202 -132
- codex_autorunner/agents/opencode/supervisor.py +194 -68
- codex_autorunner/agents/registry.py +258 -0
- codex_autorunner/agents/types.py +2 -2
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +19 -40
- codex_autorunner/cli.py +234 -151
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_events.py +15 -6
- codex_autorunner/core/app_server_logging.py +55 -15
- codex_autorunner/core/app_server_prompts.py +28 -259
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/circuit_breaker.py +183 -0
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +555 -133
- codex_autorunner/core/docs.py +54 -9
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +828 -274
- codex_autorunner/core/exceptions.py +60 -0
- 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 +21 -13
- codex_autorunner/core/locks.py +118 -1
- codex_autorunner/core/logging_utils.py +9 -6
- codex_autorunner/core/path_utils.py +123 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/retry.py +61 -0
- codex_autorunner/core/review.py +888 -0
- codex_autorunner/core/review_context.py +161 -0
- codex_autorunner/core/run_index.py +223 -0
- codex_autorunner/core/runner_controller.py +44 -1
- codex_autorunner/core/runner_process.py +30 -1
- codex_autorunner/core/sqlite_utils.py +32 -0
- codex_autorunner/core/state.py +273 -44
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +43 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +107 -75
- codex_autorunner/core/utils.py +167 -3
- codex_autorunner/discovery.py +3 -3
- 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 +708 -153
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +474 -185
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +239 -1
- codex_autorunner/integrations/telegram/constants.py +19 -1
- codex_autorunner/integrations/telegram/dispatch.py +44 -8
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
- codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
- codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
- codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
- codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
- codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
- codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
- codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
- codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
- codex_autorunner/integrations/telegram/helpers.py +90 -18
- codex_autorunner/integrations/telegram/notifications.py +126 -35
- codex_autorunner/integrations/telegram/outbox.py +214 -43
- codex_autorunner/integrations/telegram/progress_stream.py +42 -19
- codex_autorunner/integrations/telegram/runtime.py +24 -13
- codex_autorunner/integrations/telegram/service.py +500 -129
- codex_autorunner/integrations/telegram/state.py +1278 -330
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +37 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/integrations/telegram/types.py +22 -2
- codex_autorunner/integrations/telegram/voice.py +14 -15
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +25 -14
- codex_autorunner/routes/agents.py +18 -78
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +142 -113
- 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/repos.py +17 -0
- codex_autorunner/routes/review.py +148 -0
- codex_autorunner/routes/sessions.py +16 -8
- codex_autorunner/routes/settings.py +22 -0
- codex_autorunner/routes/shared.py +33 -3
- codex_autorunner/routes/system.py +22 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/voice.py +5 -13
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +9 -1
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +27 -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 -150
- 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 +67 -126
- codex_autorunner/static/index.html +788 -807
- codex_autorunner/static/liveUpdates.js +59 -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 -205
- codex_autorunner/static/styles.css +7577 -3758
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +53 -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 +21 -7
- 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/voice/capture.py +7 -7
- codex_autorunner/voice/service.py +51 -9
- codex_autorunner/web/app.py +419 -199
- codex_autorunner/web/hub_jobs.py +13 -2
- codex_autorunner/web/middleware.py +47 -13
- codex_autorunner/web/pty_session.py +26 -13
- codex_autorunner/web/schemas.py +114 -109
- codex_autorunner/web/static_assets.py +55 -42
- codex_autorunner/web/static_refresh.py +86 -0
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/core/doc_chat.py +0 -1415
- 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 -118
- codex_autorunner/spec_ingest.py +0 -788
- codex_autorunner/static/docChatActions.js +0 -279
- 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 -274
- 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 -442
- codex_autorunner/static/logs.js +0 -640
- codex_autorunner/static/runs.js +0 -409
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -86
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.1.dist-info/RECORD +0 -191
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..core.logging_utils import safe_log
|
|
8
|
+
from .static_assets import (
|
|
9
|
+
asset_version,
|
|
10
|
+
materialize_static_assets,
|
|
11
|
+
missing_static_assets,
|
|
12
|
+
require_static_assets,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _update_static_files(static_files: object, static_dir: Path) -> None:
|
|
17
|
+
try:
|
|
18
|
+
static_files.directory = static_dir
|
|
19
|
+
static_files.all_directories = static_files.get_directories( # type: ignore[attr-defined]
|
|
20
|
+
static_dir,
|
|
21
|
+
static_files.packages, # type: ignore[attr-defined]
|
|
22
|
+
)
|
|
23
|
+
static_files.config_checked = False
|
|
24
|
+
except Exception:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def refresh_static_assets(app: object) -> bool:
|
|
29
|
+
lock = getattr(getattr(app, "state", None), "static_assets_lock", None)
|
|
30
|
+
if lock is None or not lock.acquire(blocking=False):
|
|
31
|
+
return False
|
|
32
|
+
try:
|
|
33
|
+
state = getattr(app, "state", None)
|
|
34
|
+
if state is None:
|
|
35
|
+
return False
|
|
36
|
+
current_dir = getattr(state, "static_dir", None)
|
|
37
|
+
if isinstance(current_dir, Path) and not missing_static_assets(current_dir):
|
|
38
|
+
return True
|
|
39
|
+
config = getattr(state, "config", None)
|
|
40
|
+
logger = getattr(state, "logger", None)
|
|
41
|
+
static_candidates = []
|
|
42
|
+
if config is not None:
|
|
43
|
+
static_candidates.append(config.static_assets)
|
|
44
|
+
hub_static = getattr(state, "hub_static_assets", None)
|
|
45
|
+
if hub_static is not None and (
|
|
46
|
+
not static_candidates
|
|
47
|
+
or hub_static.cache_root != static_candidates[0].cache_root
|
|
48
|
+
):
|
|
49
|
+
static_candidates.append(hub_static)
|
|
50
|
+
for static_cfg in static_candidates:
|
|
51
|
+
try:
|
|
52
|
+
static_dir, static_context = materialize_static_assets(
|
|
53
|
+
static_cfg.cache_root,
|
|
54
|
+
max_cache_entries=static_cfg.max_cache_entries,
|
|
55
|
+
max_cache_age_days=static_cfg.max_cache_age_days,
|
|
56
|
+
logger=logger,
|
|
57
|
+
)
|
|
58
|
+
require_static_assets(static_dir, logger)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
if logger is not None:
|
|
61
|
+
safe_log(
|
|
62
|
+
logger,
|
|
63
|
+
logging.WARNING,
|
|
64
|
+
"Static assets refresh failed for cache root %s",
|
|
65
|
+
static_cfg.cache_root,
|
|
66
|
+
exc=exc,
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
old_context: Optional[object] = getattr(
|
|
70
|
+
state, "static_assets_context", None
|
|
71
|
+
)
|
|
72
|
+
if old_context is not None:
|
|
73
|
+
try:
|
|
74
|
+
old_context.close()
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
state.static_dir = static_dir
|
|
78
|
+
state.static_assets_context = static_context
|
|
79
|
+
state.asset_version = asset_version(static_dir)
|
|
80
|
+
static_files = getattr(state, "static_files", None)
|
|
81
|
+
if static_files is not None:
|
|
82
|
+
_update_static_files(static_files, static_dir)
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
finally:
|
|
86
|
+
lock.release()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Workspace docs helpers (active context, decisions, spec).
|
|
2
|
+
|
|
3
|
+
Workspace docs are optional and live under `.codex-autorunner/workspace/`.
|
|
4
|
+
They are distinct from tickets, which live under `.codex-autorunner/tickets/`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..core.utils import canonicalize_path
|
|
11
|
+
from .paths import (
|
|
12
|
+
WORKSPACE_DOC_KINDS,
|
|
13
|
+
WorkspaceDocKind,
|
|
14
|
+
read_workspace_doc,
|
|
15
|
+
workspace_doc_path,
|
|
16
|
+
write_workspace_doc,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
WORKSPACE_ID_HEX_LEN = 12
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def canonical_workspace_root(path: Path) -> Path:
|
|
23
|
+
return canonicalize_path(path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def workspace_id_for_path(path: Path) -> str:
|
|
27
|
+
canonical = canonical_workspace_root(path)
|
|
28
|
+
digest = hashlib.sha256(str(canonical).encode("utf-8")).hexdigest()
|
|
29
|
+
return digest[:WORKSPACE_ID_HEX_LEN]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"WORKSPACE_DOC_KINDS",
|
|
34
|
+
"WorkspaceDocKind",
|
|
35
|
+
"workspace_doc_path",
|
|
36
|
+
"read_workspace_doc",
|
|
37
|
+
"write_workspace_doc",
|
|
38
|
+
"canonical_workspace_root",
|
|
39
|
+
"workspace_id_for_path",
|
|
40
|
+
]
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from typing import Literal, cast
|
|
8
|
+
|
|
9
|
+
from ..core import drafts as draft_utils
|
|
10
|
+
|
|
11
|
+
WorkspaceDocKind = Literal["active_context", "decisions", "spec"]
|
|
12
|
+
WORKSPACE_DOC_KINDS: tuple[WorkspaceDocKind, ...] = (
|
|
13
|
+
"active_context",
|
|
14
|
+
"decisions",
|
|
15
|
+
"spec",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class WorkspaceFile:
|
|
21
|
+
name: str
|
|
22
|
+
path: str # path relative to the workspace directory (POSIX)
|
|
23
|
+
is_pinned: bool = False
|
|
24
|
+
modified_at: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_kind(kind: str) -> WorkspaceDocKind:
|
|
28
|
+
key = (kind or "").strip().lower()
|
|
29
|
+
if key not in WORKSPACE_DOC_KINDS:
|
|
30
|
+
raise ValueError("invalid workspace doc kind")
|
|
31
|
+
return cast(WorkspaceDocKind, key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def workspace_dir(repo_root: Path) -> Path:
|
|
35
|
+
return repo_root / ".codex-autorunner" / "workspace"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PINNED_DOC_FILENAMES = {f"{kind}.md" for kind in WORKSPACE_DOC_KINDS}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class WorkspaceNode:
|
|
43
|
+
name: str
|
|
44
|
+
path: str # relative to workspace dir
|
|
45
|
+
type: Literal["file", "folder"]
|
|
46
|
+
is_pinned: bool = False
|
|
47
|
+
modified_at: str | None = None
|
|
48
|
+
size: int | None = None # files only
|
|
49
|
+
children: list["WorkspaceNode"] | None = None # folders only
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_workspace_rel_path(repo_root: Path, rel_path: str) -> tuple[Path, str]:
|
|
53
|
+
"""Normalize a user-supplied workspace path and ensure it stays in-tree.
|
|
54
|
+
|
|
55
|
+
We accept POSIX-style relative paths only, then resolve the full path and
|
|
56
|
+
verify the result is still under the workspace directory. This guards
|
|
57
|
+
against ".." traversal and symlink escapes that CodeQL flagged.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
base = workspace_dir(repo_root)
|
|
61
|
+
base_real = os.path.realpath(base)
|
|
62
|
+
cleaned = (rel_path or "").strip()
|
|
63
|
+
if not cleaned:
|
|
64
|
+
raise ValueError("invalid workspace file path")
|
|
65
|
+
|
|
66
|
+
relative = PurePosixPath(cleaned)
|
|
67
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
68
|
+
raise ValueError("invalid workspace file path")
|
|
69
|
+
|
|
70
|
+
# Normalize the relative path to collapse any sneaky segments
|
|
71
|
+
norm_relative = os.path.normpath(relative.as_posix())
|
|
72
|
+
if norm_relative in {".", ""}:
|
|
73
|
+
normalized = ""
|
|
74
|
+
else:
|
|
75
|
+
normalized = norm_relative
|
|
76
|
+
|
|
77
|
+
# Reject traversal or absolute inputs after normalization
|
|
78
|
+
if (
|
|
79
|
+
normalized.startswith("..")
|
|
80
|
+
or normalized.startswith("/")
|
|
81
|
+
or normalized.startswith("\\")
|
|
82
|
+
):
|
|
83
|
+
raise ValueError("invalid workspace file path")
|
|
84
|
+
|
|
85
|
+
candidate_str = os.path.realpath(os.path.join(base_real, normalized))
|
|
86
|
+
# Ensure the resolved path stays under the workspace directory
|
|
87
|
+
if not (candidate_str == base_real or candidate_str.startswith(base_real + os.sep)):
|
|
88
|
+
raise ValueError("invalid workspace file path")
|
|
89
|
+
|
|
90
|
+
candidate = Path(candidate_str)
|
|
91
|
+
rel_posix = candidate.relative_to(base_real).as_posix()
|
|
92
|
+
return candidate, rel_posix
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sanitize_workspace_filename(filename: str) -> str:
|
|
96
|
+
"""Return a safe filename for workspace uploads.
|
|
97
|
+
|
|
98
|
+
We strip any directory components, collapse whitespace, and guard against
|
|
99
|
+
empty names. Caller is responsible for applying any per-workspace policy
|
|
100
|
+
(e.g., overwrite vs. reject).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
cleaned = (filename or "").strip()
|
|
104
|
+
# Drop any path fragments that may be embedded in the upload
|
|
105
|
+
base = PurePosixPath(cleaned).name
|
|
106
|
+
# Remove remaining separators/backslashes that PurePosixPath.name could keep
|
|
107
|
+
base = base.replace("/", "").replace("\\", "")
|
|
108
|
+
if base in {".", ".."}:
|
|
109
|
+
base = ""
|
|
110
|
+
# Collapse whitespace to single spaces to keep names readable
|
|
111
|
+
base = " ".join(base.split())
|
|
112
|
+
if not base:
|
|
113
|
+
return "upload"
|
|
114
|
+
return base
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def workspace_doc_path(repo_root: Path, kind: str) -> Path:
|
|
118
|
+
key = _normalize_kind(kind)
|
|
119
|
+
return workspace_dir(repo_root) / f"{key}.md"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def read_workspace_file(
|
|
123
|
+
repo_root: Path, rel_path: str
|
|
124
|
+
) -> str: # codeql[py/path-injection]
|
|
125
|
+
path, _ = normalize_workspace_rel_path(repo_root, rel_path)
|
|
126
|
+
if (
|
|
127
|
+
path.is_dir()
|
|
128
|
+
): # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
129
|
+
raise ValueError("path points to a directory")
|
|
130
|
+
if (
|
|
131
|
+
not path.exists()
|
|
132
|
+
): # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
133
|
+
return ""
|
|
134
|
+
return path.read_text(
|
|
135
|
+
encoding="utf-8"
|
|
136
|
+
) # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def write_workspace_file( # codeql[py/path-injection]
|
|
140
|
+
repo_root: Path, rel_path: str, content: str
|
|
141
|
+
) -> str:
|
|
142
|
+
path, rel_posix = normalize_workspace_rel_path(repo_root, rel_path)
|
|
143
|
+
if (
|
|
144
|
+
path.exists() and path.is_dir()
|
|
145
|
+
): # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
146
|
+
raise ValueError("path points to a directory")
|
|
147
|
+
path.parent.mkdir(
|
|
148
|
+
parents=True, exist_ok=True
|
|
149
|
+
) # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
150
|
+
path.write_text(
|
|
151
|
+
content or "", encoding="utf-8"
|
|
152
|
+
) # codeql[py/path-injection] validated by normalize_workspace_rel_path
|
|
153
|
+
try:
|
|
154
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
155
|
+
draft_utils.invalidate_drafts_for_path(repo_root, rel)
|
|
156
|
+
state_key = f"workspace_{rel_posix.replace('/', '_')}"
|
|
157
|
+
draft_utils.remove_draft(repo_root, state_key)
|
|
158
|
+
except Exception:
|
|
159
|
+
# best effort; do not block writes
|
|
160
|
+
pass
|
|
161
|
+
return path.read_text(encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def read_workspace_doc(repo_root: Path, kind: str) -> str:
|
|
165
|
+
path = workspace_doc_path(repo_root, kind)
|
|
166
|
+
if not path.exists():
|
|
167
|
+
return ""
|
|
168
|
+
return path.read_text(encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def write_workspace_doc( # codeql[py/path-injection]
|
|
172
|
+
repo_root: Path, kind: str, content: str
|
|
173
|
+
) -> str:
|
|
174
|
+
path = workspace_doc_path(repo_root, kind)
|
|
175
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
path.write_text(
|
|
177
|
+
content or "", encoding="utf-8"
|
|
178
|
+
) # codeql[py/path-injection] workspace_doc_path is deterministic
|
|
179
|
+
try:
|
|
180
|
+
rel = path.relative_to(repo_root).as_posix()
|
|
181
|
+
draft_utils.invalidate_drafts_for_path(repo_root, rel)
|
|
182
|
+
state_key = f"workspace_{rel.replace('/', '_')}"
|
|
183
|
+
draft_utils.remove_draft(repo_root, state_key)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
return path.read_text(encoding="utf-8")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _format_mtime(path: Path) -> str | None:
|
|
190
|
+
if not path.exists():
|
|
191
|
+
return None
|
|
192
|
+
ts = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
|
|
193
|
+
return ts.isoformat()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def list_workspace_files(
|
|
197
|
+
repo_root: Path,
|
|
198
|
+
) -> list[WorkspaceFile]: # codeql[py/path-injection]
|
|
199
|
+
base = workspace_dir(repo_root)
|
|
200
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
|
|
202
|
+
pinned: list[WorkspaceFile] = []
|
|
203
|
+
for kind in WORKSPACE_DOC_KINDS:
|
|
204
|
+
path = workspace_doc_path(repo_root, kind)
|
|
205
|
+
rel = path.relative_to(base).as_posix()
|
|
206
|
+
pinned.append(
|
|
207
|
+
WorkspaceFile(
|
|
208
|
+
name=path.name,
|
|
209
|
+
path=rel,
|
|
210
|
+
is_pinned=True,
|
|
211
|
+
modified_at=_format_mtime(path),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
others: list[WorkspaceFile] = []
|
|
216
|
+
if base.exists():
|
|
217
|
+
for file_path in base.rglob("*"):
|
|
218
|
+
if file_path.is_dir():
|
|
219
|
+
continue
|
|
220
|
+
try:
|
|
221
|
+
rel = file_path.relative_to(base).as_posix()
|
|
222
|
+
except ValueError:
|
|
223
|
+
continue
|
|
224
|
+
if any(rel == pinned_file.path for pinned_file in pinned):
|
|
225
|
+
continue
|
|
226
|
+
others.append(
|
|
227
|
+
WorkspaceFile(
|
|
228
|
+
name=file_path.name,
|
|
229
|
+
path=rel,
|
|
230
|
+
is_pinned=False,
|
|
231
|
+
modified_at=_format_mtime(file_path),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
others.sort(key=lambda f: f.path)
|
|
236
|
+
return [*pinned, *others]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _sort_workspace_children(path: Path) -> tuple[int, str]:
|
|
240
|
+
# Folders first, then files, both alphabetized (case-insensitive)
|
|
241
|
+
return (0 if path.is_dir() else 1, path.name.lower())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _is_within_workspace(base_real: Path, candidate: Path) -> bool:
|
|
245
|
+
try:
|
|
246
|
+
candidate.resolve().relative_to(base_real)
|
|
247
|
+
return True
|
|
248
|
+
except Exception:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _file_node(base: Path, path: Path, is_pinned: bool = False) -> WorkspaceNode:
|
|
253
|
+
rel = path.relative_to(base).as_posix()
|
|
254
|
+
size: int | None = None
|
|
255
|
+
if path.exists() and path.is_file():
|
|
256
|
+
try:
|
|
257
|
+
size = path.stat().st_size
|
|
258
|
+
except OSError:
|
|
259
|
+
size = None
|
|
260
|
+
return WorkspaceNode(
|
|
261
|
+
name=path.name,
|
|
262
|
+
path=rel,
|
|
263
|
+
type="file",
|
|
264
|
+
is_pinned=is_pinned,
|
|
265
|
+
modified_at=_format_mtime(path),
|
|
266
|
+
size=size,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _build_workspace_tree(base: Path, path: Path) -> WorkspaceNode:
|
|
271
|
+
is_symlink = path.is_symlink()
|
|
272
|
+
is_folder = path.is_dir() and not is_symlink
|
|
273
|
+
is_pinned = path.name in PINNED_DOC_FILENAMES and path.parent == base
|
|
274
|
+
|
|
275
|
+
if not is_folder:
|
|
276
|
+
return _file_node(base, path, is_pinned=is_pinned)
|
|
277
|
+
|
|
278
|
+
children: list[WorkspaceNode] = []
|
|
279
|
+
for child in sorted(path.iterdir(), key=_sort_workspace_children):
|
|
280
|
+
# Avoid duplicating pinned docs surfaced at the root list
|
|
281
|
+
if child.parent == base and child.name in PINNED_DOC_FILENAMES:
|
|
282
|
+
continue
|
|
283
|
+
# Skip symlink escapes that resolve outside the workspace
|
|
284
|
+
if child.is_symlink() and not _is_within_workspace(base.resolve(), child):
|
|
285
|
+
continue
|
|
286
|
+
children.append(_build_workspace_tree(base, child))
|
|
287
|
+
|
|
288
|
+
return WorkspaceNode(
|
|
289
|
+
name=path.name,
|
|
290
|
+
path=path.relative_to(base).as_posix(),
|
|
291
|
+
type="folder",
|
|
292
|
+
is_pinned=False,
|
|
293
|
+
modified_at=_format_mtime(path),
|
|
294
|
+
children=children,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def list_workspace_tree(repo_root: Path) -> list[WorkspaceNode]:
|
|
299
|
+
"""Return hierarchical workspace structure (folders + files)."""
|
|
300
|
+
|
|
301
|
+
base = workspace_dir(repo_root)
|
|
302
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
base_real = base.resolve()
|
|
304
|
+
|
|
305
|
+
nodes: list[WorkspaceNode] = []
|
|
306
|
+
|
|
307
|
+
# Pinned docs first (even if missing)
|
|
308
|
+
for name in sorted(PINNED_DOC_FILENAMES):
|
|
309
|
+
pinned_path = base / name
|
|
310
|
+
nodes.append(_file_node(base, pinned_path, is_pinned=True))
|
|
311
|
+
|
|
312
|
+
for child in sorted(base.iterdir(), key=_sort_workspace_children):
|
|
313
|
+
if child.parent == base and child.name in PINNED_DOC_FILENAMES:
|
|
314
|
+
continue
|
|
315
|
+
if child.is_symlink() and not _is_within_workspace(base_real, child):
|
|
316
|
+
continue
|
|
317
|
+
nodes.append(_build_workspace_tree(base, child))
|
|
318
|
+
|
|
319
|
+
return nodes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-autorunner
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Codex autorunner CLI per DESIGN-V1
|
|
5
5
|
Author: Codex
|
|
6
6
|
License: MIT License
|
|
@@ -45,10 +45,12 @@ Requires-Dist: ptyprocess>=0.7
|
|
|
45
45
|
Requires-Dist: python-multipart>=0.0.9
|
|
46
46
|
Requires-Dist: python-dotenv>=1.0
|
|
47
47
|
Requires-Dist: httpx>=0.27
|
|
48
|
+
Requires-Dist: tenacity>=8.0
|
|
48
49
|
Provides-Extra: dev
|
|
49
50
|
Requires-Dist: black==25.11.0; extra == "dev"
|
|
50
51
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
51
52
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
52
54
|
Requires-Dist: pytest-timeout>=2.0; extra == "dev"
|
|
53
55
|
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
54
56
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
@@ -63,11 +65,17 @@ Dynamic: license-file
|
|
|
63
65
|
# codex-autorunner
|
|
64
66
|
[](https://pypi.org/project/codex-autorunner/)
|
|
65
67
|
|
|
66
|
-
An opinionated autorunner that uses the Codex
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
An opinionated autorunner that uses the Codex app-server as the primary execution backend with OpenCode support to work on large tasks via a simple loop. On each loop we feed the Codex instance the last one's final output along with core documents.
|
|
69
|
+
|
|
70
|
+
In the current model, the primary work surface is **tickets**:
|
|
71
|
+
|
|
72
|
+
- `.codex-autorunner/tickets/TICKET-###.md`
|
|
73
|
+
|
|
74
|
+
Optionally, you can maintain lightweight **workspace docs** (auto-created on write; missing is OK):
|
|
75
|
+
|
|
76
|
+
- `.codex-autorunner/workspace/active_context.md`
|
|
77
|
+
- `.codex-autorunner/workspace/decisions.md`
|
|
78
|
+
- `.codex-autorunner/workspace/spec.md`
|
|
71
79
|
|
|
72
80
|
## Sneak Peak
|
|
73
81
|
Run multiple agents on many repositories, with git worktree support
|
|
@@ -79,8 +87,7 @@ See the progress of your long running tasks with a high level overview
|
|
|
79
87
|
Dive deep into specific agent execution with a rich but readable log
|
|
80
88
|

|
|
81
89
|
|
|
82
|
-
|
|
83
|
-

|
|
90
|
+
Tickets and workspace docs are markdown files. Edit them directly or use the web UI’s file chat to iterate with the agent.
|
|
84
91
|
|
|
85
92
|
Use codex CLI directly for multi-shot problem solving or `/review`
|
|
86
93
|

|
|
@@ -90,11 +97,10 @@ Mobile-first experience, code on the go with Whisper support (BYOK)
|
|
|
90
97
|
|
|
91
98
|
## What it does
|
|
92
99
|
- Initializes a repo with Codex-friendly docs and config.
|
|
93
|
-
- Runs Codex in a loop against the repo, streaming logs.
|
|
100
|
+
- Runs Codex app-server in a loop against the repo, streaming logs via OpenCode runtime.
|
|
94
101
|
- Tracks state, logs, and config under `.codex-autorunner/`.
|
|
95
|
-
- Exposes a power-user HTTP API and web UI for docs, logs, runner control, and a Codex TUI terminal.
|
|
102
|
+
- Exposes a power-user HTTP API and web UI for tickets, workspace docs, file chat, logs, runner control, and a Codex TUI terminal.
|
|
96
103
|
- Optionally runs a Telegram bot for interactive, user-in-the-loop Codex sessions.
|
|
97
|
-
- Generates a pasteable repo snapshot (`.codex-autorunner/SNAPSHOT.md`) for sharing with other LLM chats.
|
|
98
104
|
|
|
99
105
|
CLI commands are available as `codex-autorunner` or the shorter `car`.
|
|
100
106
|
|
|
@@ -164,7 +170,7 @@ If you set a base path, prefix all checks with it.
|
|
|
164
170
|
|
|
165
171
|
## Quick start
|
|
166
172
|
1) Install (editable): `pip install -e .`
|
|
167
|
-
2) Initialize (hub + repo): `codex-autorunner init --git-init` (or `car init --git-init` if you prefer short). This creates the hub config at `.codex-autorunner/config.yml`, plus state/log files and
|
|
173
|
+
2) Initialize (hub + repo): `codex-autorunner init --git-init` (or `car init --git-init` if you prefer short). This creates the hub config at `.codex-autorunner/config.yml`, plus state/log files and starter content under `.codex-autorunner/` (tickets and optional workspace docs).
|
|
168
174
|
3) Run once: `codex-autorunner once` / `car once`
|
|
169
175
|
4) Continuous loop: `codex-autorunner run` / `car run`
|
|
170
176
|
5) If stuck: `codex-autorunner kill` then `codex-autorunner resume` (or the `car` equivalents)
|
|
@@ -229,19 +235,12 @@ If you set `server.auth_token_env`, the API requires `Authorization: Bearer <tok
|
|
|
229
235
|
- `run` / `once` — run the loop (continuous or single iteration).
|
|
230
236
|
- `resume` — clear stale lock/state and restart; `--once` for a single run.
|
|
231
237
|
- `kill` — SIGTERM the running loop and mark state error.
|
|
232
|
-
- `status` — show current state
|
|
238
|
+
- `status` — show current state.
|
|
233
239
|
- `sessions` — list terminal sessions (server-backed when available).
|
|
234
240
|
- `stop-session` — stop a terminal session by repo (`--repo`) or id (`--session`).
|
|
235
241
|
- `log` — view logs (tail or specific run).
|
|
236
|
-
- `edit` — open
|
|
237
|
-
- `ingest-spec` — generate TODO/PROGRESS/OPINIONS from SPEC using Codex (use `--force` to overwrite).
|
|
238
|
-
- `clear-docs` — reset TODO/PROGRESS/OPINIONS to empty templates (type CLEAR to confirm).
|
|
239
|
-
- `snapshot` — generate/update `.codex-autorunner/SNAPSHOT.md` (incremental by default when one exists; use `--from-scratch` to regenerate).
|
|
242
|
+
- `edit` — open `active_context|decisions|spec` in `$EDITOR`.
|
|
240
243
|
- `serve` — start the HTTP API (FastAPI) on host/port from config (defaults 127.0.0.1:4173).
|
|
241
244
|
|
|
242
|
-
## Snapshot (repo briefing)
|
|
243
|
-
- Web UI: open the Snapshot tab. If no snapshot exists, you’ll see “Generate snapshot”; otherwise you’ll see “Update snapshot (incremental)” and “Regenerate snapshot (from scratch)”, plus “Copy to clipboard”.
|
|
244
|
-
- CLI: `codex-autorunner snapshot` (or `car snapshot`) writes `.codex-autorunner/SNAPSHOT.md` and `.codex-autorunner/snapshot_state.json`.
|
|
245
|
-
|
|
246
245
|
## Star history
|
|
247
246
|
[](https://star-history.com/#Git-on-my-level/codex-autorunner&Date)
|