codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -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 +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -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/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -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 +1364 -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 +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- 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 +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- 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 +344 -325
- 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 +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -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 +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +417 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.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/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Optional, Protocol
|
|
6
|
+
|
|
7
|
+
from ...tickets.files import list_ticket_paths
|
|
8
|
+
from .models import FlowEventType, FlowRunRecord
|
|
9
|
+
from .store import FlowStore
|
|
10
|
+
from .worker_process import (
|
|
11
|
+
check_worker_health,
|
|
12
|
+
clear_worker_metadata,
|
|
13
|
+
spawn_flow_worker,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BootstrapCheckResult:
|
|
19
|
+
status: str
|
|
20
|
+
github_available: Optional[bool] = None
|
|
21
|
+
repo_slug: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class IssueSeedResult:
|
|
26
|
+
content: str
|
|
27
|
+
issue_number: int
|
|
28
|
+
repo_slug: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GitHubServiceProtocol(Protocol):
|
|
32
|
+
def gh_available(self) -> bool: ...
|
|
33
|
+
|
|
34
|
+
def gh_authenticated(self) -> bool: ...
|
|
35
|
+
|
|
36
|
+
def repo_info(self) -> Any: ...
|
|
37
|
+
|
|
38
|
+
def validate_issue_same_repo(self, issue_ref: str) -> int: ...
|
|
39
|
+
|
|
40
|
+
def issue_view(self, number: int) -> dict: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def issue_md_path(repo_root: Path) -> Path:
|
|
44
|
+
return repo_root.resolve() / ".codex-autorunner" / "ISSUE.md"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def issue_md_has_content(repo_root: Path) -> bool:
|
|
48
|
+
issue_path = issue_md_path(repo_root)
|
|
49
|
+
if not issue_path.exists():
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
return bool(issue_path.read_text(encoding="utf-8").strip())
|
|
53
|
+
except OSError:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ticket_dir(repo_root: Path) -> Path:
|
|
58
|
+
return repo_root.resolve() / ".codex-autorunner" / "tickets"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def bootstrap_check(
|
|
62
|
+
repo_root: Path,
|
|
63
|
+
github_service_factory: Optional[Callable[[Path], GitHubServiceProtocol]] = None,
|
|
64
|
+
) -> BootstrapCheckResult:
|
|
65
|
+
if list_ticket_paths(_ticket_dir(repo_root)):
|
|
66
|
+
return BootstrapCheckResult(status="ready")
|
|
67
|
+
|
|
68
|
+
if issue_md_has_content(repo_root):
|
|
69
|
+
return BootstrapCheckResult(status="ready")
|
|
70
|
+
|
|
71
|
+
gh_available = False
|
|
72
|
+
repo_slug: Optional[str] = None
|
|
73
|
+
if github_service_factory is not None:
|
|
74
|
+
try:
|
|
75
|
+
gh = github_service_factory(repo_root)
|
|
76
|
+
gh_available = gh.gh_available() and gh.gh_authenticated()
|
|
77
|
+
if gh_available:
|
|
78
|
+
repo_info = gh.repo_info()
|
|
79
|
+
repo_slug = getattr(repo_info, "name_with_owner", None)
|
|
80
|
+
except Exception:
|
|
81
|
+
gh_available = False
|
|
82
|
+
repo_slug = None
|
|
83
|
+
|
|
84
|
+
return BootstrapCheckResult(
|
|
85
|
+
status="needs_issue", github_available=gh_available, repo_slug=repo_slug
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_issue_as_markdown(issue: dict, repo_slug: Optional[str] = None) -> str:
|
|
90
|
+
number = issue.get("number")
|
|
91
|
+
title = issue.get("title") or ""
|
|
92
|
+
url = issue.get("url") or ""
|
|
93
|
+
state = issue.get("state") or ""
|
|
94
|
+
author = issue.get("author") or {}
|
|
95
|
+
author_name = (
|
|
96
|
+
author.get("login") if isinstance(author, dict) else str(author or "unknown")
|
|
97
|
+
)
|
|
98
|
+
labels = issue.get("labels")
|
|
99
|
+
label_names: list[str] = []
|
|
100
|
+
if isinstance(labels, list):
|
|
101
|
+
for label in labels:
|
|
102
|
+
if isinstance(label, dict):
|
|
103
|
+
name = label.get("name")
|
|
104
|
+
else:
|
|
105
|
+
name = label
|
|
106
|
+
if name:
|
|
107
|
+
label_names.append(str(name))
|
|
108
|
+
comments = issue.get("comments")
|
|
109
|
+
comment_count = None
|
|
110
|
+
if isinstance(comments, dict):
|
|
111
|
+
total = comments.get("totalCount")
|
|
112
|
+
if isinstance(total, int):
|
|
113
|
+
comment_count = total
|
|
114
|
+
|
|
115
|
+
body = issue.get("body") or "(no description)"
|
|
116
|
+
lines = [
|
|
117
|
+
f"# Issue #{number}: {title}".strip(),
|
|
118
|
+
"",
|
|
119
|
+
f"**Repo:** {repo_slug or 'unknown'}",
|
|
120
|
+
f"**URL:** {url}",
|
|
121
|
+
f"**State:** {state}",
|
|
122
|
+
f"**Author:** {author_name}",
|
|
123
|
+
]
|
|
124
|
+
if label_names:
|
|
125
|
+
lines.append(f"**Labels:** {', '.join(label_names)}")
|
|
126
|
+
if comment_count is not None:
|
|
127
|
+
lines.append(f"**Comments:** {comment_count}")
|
|
128
|
+
lines.extend(["", "## Description", "", str(body).strip(), ""])
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def seed_issue_from_github(
|
|
133
|
+
repo_root: Path,
|
|
134
|
+
issue_ref: str,
|
|
135
|
+
github_service_factory: Optional[Callable[[Path], GitHubServiceProtocol]] = None,
|
|
136
|
+
) -> IssueSeedResult:
|
|
137
|
+
if github_service_factory is None:
|
|
138
|
+
raise RuntimeError("GitHub service unavailable.")
|
|
139
|
+
gh = github_service_factory(repo_root)
|
|
140
|
+
if not (gh.gh_available() and gh.gh_authenticated()):
|
|
141
|
+
raise RuntimeError("GitHub CLI is not available or not authenticated.")
|
|
142
|
+
number = gh.validate_issue_same_repo(issue_ref)
|
|
143
|
+
issue = gh.issue_view(number=number)
|
|
144
|
+
repo_info = gh.repo_info()
|
|
145
|
+
content = format_issue_as_markdown(issue, repo_info.name_with_owner)
|
|
146
|
+
return IssueSeedResult(
|
|
147
|
+
content=content, issue_number=number, repo_slug=repo_info.name_with_owner
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def seed_issue_from_text(plan_text: str) -> str:
|
|
152
|
+
return f"# Issue\n\n{plan_text.strip()}\n"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _derive_effective_current_ticket(
|
|
156
|
+
record: FlowRunRecord, store: Optional[FlowStore]
|
|
157
|
+
) -> Optional[str]:
|
|
158
|
+
if store is None:
|
|
159
|
+
return None
|
|
160
|
+
try:
|
|
161
|
+
if (
|
|
162
|
+
getattr(record, "flow_type", None) != "ticket_flow"
|
|
163
|
+
or not record.status.is_active()
|
|
164
|
+
):
|
|
165
|
+
return None
|
|
166
|
+
last_started = store.get_last_event_seq_by_types(
|
|
167
|
+
record.id, [FlowEventType.STEP_STARTED]
|
|
168
|
+
)
|
|
169
|
+
last_finished = store.get_last_event_seq_by_types(
|
|
170
|
+
record.id, [FlowEventType.STEP_COMPLETED, FlowEventType.STEP_FAILED]
|
|
171
|
+
)
|
|
172
|
+
in_progress = bool(
|
|
173
|
+
last_started is not None
|
|
174
|
+
and (last_finished is None or last_started > last_finished)
|
|
175
|
+
)
|
|
176
|
+
if not in_progress:
|
|
177
|
+
return None
|
|
178
|
+
return store.get_latest_step_progress_current_ticket(
|
|
179
|
+
record.id, after_seq=last_finished
|
|
180
|
+
)
|
|
181
|
+
except Exception:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_flow_status_snapshot(
|
|
186
|
+
repo_root: Path, record: FlowRunRecord, store: Optional[FlowStore]
|
|
187
|
+
) -> dict:
|
|
188
|
+
last_event_seq = None
|
|
189
|
+
last_event_at = None
|
|
190
|
+
if store:
|
|
191
|
+
try:
|
|
192
|
+
last_event_seq, last_event_at = store.get_last_event_meta(record.id)
|
|
193
|
+
except Exception:
|
|
194
|
+
last_event_seq, last_event_at = None, None
|
|
195
|
+
health = check_worker_health(repo_root, record.id)
|
|
196
|
+
|
|
197
|
+
state = record.state or {}
|
|
198
|
+
current_ticket = None
|
|
199
|
+
if isinstance(state, dict):
|
|
200
|
+
ticket_engine = state.get("ticket_engine")
|
|
201
|
+
if isinstance(ticket_engine, dict):
|
|
202
|
+
current_ticket = ticket_engine.get("current_ticket")
|
|
203
|
+
if not (isinstance(current_ticket, str) and current_ticket.strip()):
|
|
204
|
+
current_ticket = None
|
|
205
|
+
effective_ticket = current_ticket
|
|
206
|
+
if not effective_ticket:
|
|
207
|
+
effective_ticket = _derive_effective_current_ticket(record, store)
|
|
208
|
+
|
|
209
|
+
updated_state: Optional[dict] = None
|
|
210
|
+
if effective_ticket and not current_ticket and isinstance(state, dict):
|
|
211
|
+
ticket_engine = state.get("ticket_engine")
|
|
212
|
+
ticket_engine = dict(ticket_engine) if isinstance(ticket_engine, dict) else {}
|
|
213
|
+
ticket_engine["current_ticket"] = effective_ticket
|
|
214
|
+
updated_state = dict(state)
|
|
215
|
+
updated_state["ticket_engine"] = ticket_engine
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"last_event_seq": last_event_seq,
|
|
219
|
+
"last_event_at": last_event_at,
|
|
220
|
+
"worker_health": health,
|
|
221
|
+
"effective_current_ticket": effective_ticket,
|
|
222
|
+
"state": updated_state,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def ensure_worker(repo_root: Path, run_id: str) -> dict:
|
|
227
|
+
health = check_worker_health(repo_root, run_id)
|
|
228
|
+
if health.status in {"dead", "mismatch", "invalid"}:
|
|
229
|
+
try:
|
|
230
|
+
clear_worker_metadata(health.artifact_path.parent)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
if health.is_alive:
|
|
234
|
+
return {"status": "reused", "health": health}
|
|
235
|
+
|
|
236
|
+
proc, stdout_handle, stderr_handle = spawn_flow_worker(repo_root, run_id)
|
|
237
|
+
return {
|
|
238
|
+
"status": "spawned",
|
|
239
|
+
"health": health,
|
|
240
|
+
"proc": proc,
|
|
241
|
+
"stdout": stdout_handle,
|
|
242
|
+
"stderr": stderr_handle,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__all__ = [
|
|
247
|
+
"BootstrapCheckResult",
|
|
248
|
+
"IssueSeedResult",
|
|
249
|
+
"bootstrap_check",
|
|
250
|
+
"build_flow_status_snapshot",
|
|
251
|
+
"ensure_worker",
|
|
252
|
+
"format_issue_as_markdown",
|
|
253
|
+
"issue_md_has_content",
|
|
254
|
+
"issue_md_path",
|
|
255
|
+
"seed_issue_from_github",
|
|
256
|
+
"seed_issue_from_text",
|
|
257
|
+
]
|
|
@@ -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
|
|
@@ -232,3 +232,65 @@ def git_default_branch(repo_root: Path) -> Optional[str]:
|
|
|
232
232
|
if raw.startswith("origin/"):
|
|
233
233
|
return raw.split("/", 1)[1]
|
|
234
234
|
return raw
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def git_diff_stats(
|
|
238
|
+
repo_root: Path, from_ref: Optional[str] = None, *, include_staged: bool = True
|
|
239
|
+
) -> Optional[dict]:
|
|
240
|
+
"""
|
|
241
|
+
Get diff statistics (insertions/deletions) for changes.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
repo_root: Repository root path
|
|
245
|
+
from_ref: Compare against this ref (e.g., a commit SHA). If None, compares
|
|
246
|
+
working tree against HEAD.
|
|
247
|
+
include_staged: When from_ref is None, include staged changes in the diff.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dict with insertions, deletions, files_changed, or None on error.
|
|
251
|
+
Example: {"insertions": 47, "deletions": 12, "files_changed": 5}
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
if from_ref:
|
|
255
|
+
# Compare from_ref to working tree (includes all changes: committed + staged + unstaged)
|
|
256
|
+
proc = run_git(["diff", "--numstat", from_ref], repo_root)
|
|
257
|
+
elif include_staged:
|
|
258
|
+
# Working tree + staged vs HEAD
|
|
259
|
+
proc = run_git(["diff", "--numstat", "HEAD"], repo_root)
|
|
260
|
+
else:
|
|
261
|
+
# Only unstaged changes
|
|
262
|
+
proc = run_git(["diff", "--numstat"], repo_root)
|
|
263
|
+
except GitError:
|
|
264
|
+
return None
|
|
265
|
+
if proc.returncode != 0:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
insertions = 0
|
|
269
|
+
deletions = 0
|
|
270
|
+
files_changed = 0
|
|
271
|
+
|
|
272
|
+
for line in (proc.stdout or "").strip().splitlines():
|
|
273
|
+
if not line:
|
|
274
|
+
continue
|
|
275
|
+
parts = line.split("\t")
|
|
276
|
+
if len(parts) < 2:
|
|
277
|
+
continue
|
|
278
|
+
# Binary files show "-" for both counts
|
|
279
|
+
add_str, del_str = parts[0], parts[1]
|
|
280
|
+
if add_str != "-":
|
|
281
|
+
try:
|
|
282
|
+
insertions += int(add_str)
|
|
283
|
+
except ValueError:
|
|
284
|
+
pass
|
|
285
|
+
if del_str != "-":
|
|
286
|
+
try:
|
|
287
|
+
deletions += int(del_str)
|
|
288
|
+
except ValueError:
|
|
289
|
+
pass
|
|
290
|
+
files_changed += 1
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"insertions": insertions,
|
|
294
|
+
"deletions": deletions,
|
|
295
|
+
"files_changed": files_changed,
|
|
296
|
+
}
|