codex-autorunner 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- 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/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- 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 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -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 +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -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 +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -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 +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -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 +469 -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 +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
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, ticket_is_done
|
|
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 ticket_progress(repo_root: Path) -> dict[str, int]:
|
|
62
|
+
ticket_dir = _ticket_dir(repo_root)
|
|
63
|
+
ticket_paths = list_ticket_paths(ticket_dir)
|
|
64
|
+
total = len(ticket_paths)
|
|
65
|
+
done = 0
|
|
66
|
+
if total:
|
|
67
|
+
for path in ticket_paths:
|
|
68
|
+
if ticket_is_done(path):
|
|
69
|
+
done += 1
|
|
70
|
+
return {"done": done, "total": total}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bootstrap_check(
|
|
74
|
+
repo_root: Path,
|
|
75
|
+
github_service_factory: Optional[Callable[[Path], GitHubServiceProtocol]] = None,
|
|
76
|
+
) -> BootstrapCheckResult:
|
|
77
|
+
if list_ticket_paths(_ticket_dir(repo_root)):
|
|
78
|
+
return BootstrapCheckResult(status="ready")
|
|
79
|
+
|
|
80
|
+
if issue_md_has_content(repo_root):
|
|
81
|
+
return BootstrapCheckResult(status="ready")
|
|
82
|
+
|
|
83
|
+
gh_available = False
|
|
84
|
+
repo_slug: Optional[str] = None
|
|
85
|
+
if github_service_factory is not None:
|
|
86
|
+
try:
|
|
87
|
+
gh = github_service_factory(repo_root)
|
|
88
|
+
gh_available = gh.gh_available() and gh.gh_authenticated()
|
|
89
|
+
if gh_available:
|
|
90
|
+
repo_info = gh.repo_info()
|
|
91
|
+
repo_slug = getattr(repo_info, "name_with_owner", None)
|
|
92
|
+
except Exception:
|
|
93
|
+
gh_available = False
|
|
94
|
+
repo_slug = None
|
|
95
|
+
|
|
96
|
+
return BootstrapCheckResult(
|
|
97
|
+
status="needs_issue", github_available=gh_available, repo_slug=repo_slug
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def format_issue_as_markdown(issue: dict, repo_slug: Optional[str] = None) -> str:
|
|
102
|
+
number = issue.get("number")
|
|
103
|
+
title = issue.get("title") or ""
|
|
104
|
+
url = issue.get("url") or ""
|
|
105
|
+
state = issue.get("state") or ""
|
|
106
|
+
author = issue.get("author") or {}
|
|
107
|
+
author_name = (
|
|
108
|
+
author.get("login") if isinstance(author, dict) else str(author or "unknown")
|
|
109
|
+
)
|
|
110
|
+
labels = issue.get("labels")
|
|
111
|
+
label_names: list[str] = []
|
|
112
|
+
if isinstance(labels, list):
|
|
113
|
+
for label in labels:
|
|
114
|
+
if isinstance(label, dict):
|
|
115
|
+
name = label.get("name")
|
|
116
|
+
else:
|
|
117
|
+
name = label
|
|
118
|
+
if name:
|
|
119
|
+
label_names.append(str(name))
|
|
120
|
+
comments = issue.get("comments")
|
|
121
|
+
comment_count = None
|
|
122
|
+
if isinstance(comments, dict):
|
|
123
|
+
total = comments.get("totalCount")
|
|
124
|
+
if isinstance(total, int):
|
|
125
|
+
comment_count = total
|
|
126
|
+
|
|
127
|
+
body = issue.get("body") or "(no description)"
|
|
128
|
+
lines = [
|
|
129
|
+
f"# Issue #{number}: {title}".strip(),
|
|
130
|
+
"",
|
|
131
|
+
f"**Repo:** {repo_slug or 'unknown'}",
|
|
132
|
+
f"**URL:** {url}",
|
|
133
|
+
f"**State:** {state}",
|
|
134
|
+
f"**Author:** {author_name}",
|
|
135
|
+
]
|
|
136
|
+
if label_names:
|
|
137
|
+
lines.append(f"**Labels:** {', '.join(label_names)}")
|
|
138
|
+
if comment_count is not None:
|
|
139
|
+
lines.append(f"**Comments:** {comment_count}")
|
|
140
|
+
lines.extend(["", "## Description", "", str(body).strip(), ""])
|
|
141
|
+
return "\n".join(lines)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def seed_issue_from_github(
|
|
145
|
+
repo_root: Path,
|
|
146
|
+
issue_ref: str,
|
|
147
|
+
github_service_factory: Optional[Callable[[Path], GitHubServiceProtocol]] = None,
|
|
148
|
+
) -> IssueSeedResult:
|
|
149
|
+
if github_service_factory is None:
|
|
150
|
+
raise RuntimeError("GitHub service unavailable.")
|
|
151
|
+
gh = github_service_factory(repo_root)
|
|
152
|
+
if not (gh.gh_available() and gh.gh_authenticated()):
|
|
153
|
+
raise RuntimeError("GitHub CLI is not available or not authenticated.")
|
|
154
|
+
number = gh.validate_issue_same_repo(issue_ref)
|
|
155
|
+
issue = gh.issue_view(number=number)
|
|
156
|
+
repo_info = gh.repo_info()
|
|
157
|
+
content = format_issue_as_markdown(issue, repo_info.name_with_owner)
|
|
158
|
+
return IssueSeedResult(
|
|
159
|
+
content=content, issue_number=number, repo_slug=repo_info.name_with_owner
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def seed_issue_from_text(plan_text: str) -> str:
|
|
164
|
+
return f"# Issue\n\n{plan_text.strip()}\n"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _derive_effective_current_ticket(
|
|
168
|
+
record: FlowRunRecord, store: Optional[FlowStore]
|
|
169
|
+
) -> Optional[str]:
|
|
170
|
+
if store is None:
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
if (
|
|
174
|
+
getattr(record, "flow_type", None) != "ticket_flow"
|
|
175
|
+
or not record.status.is_active()
|
|
176
|
+
):
|
|
177
|
+
return None
|
|
178
|
+
last_started = store.get_last_event_seq_by_types(
|
|
179
|
+
record.id, [FlowEventType.STEP_STARTED]
|
|
180
|
+
)
|
|
181
|
+
last_finished = store.get_last_event_seq_by_types(
|
|
182
|
+
record.id, [FlowEventType.STEP_COMPLETED, FlowEventType.STEP_FAILED]
|
|
183
|
+
)
|
|
184
|
+
in_progress = bool(
|
|
185
|
+
last_started is not None
|
|
186
|
+
and (last_finished is None or last_started > last_finished)
|
|
187
|
+
)
|
|
188
|
+
if not in_progress:
|
|
189
|
+
return None
|
|
190
|
+
return store.get_latest_step_progress_current_ticket(
|
|
191
|
+
record.id, after_seq=last_finished
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_flow_status_snapshot(
|
|
198
|
+
repo_root: Path, record: FlowRunRecord, store: Optional[FlowStore]
|
|
199
|
+
) -> dict:
|
|
200
|
+
last_event_seq = None
|
|
201
|
+
last_event_at = None
|
|
202
|
+
if store:
|
|
203
|
+
try:
|
|
204
|
+
last_event_seq, last_event_at = store.get_last_event_meta(record.id)
|
|
205
|
+
except Exception:
|
|
206
|
+
last_event_seq, last_event_at = None, None
|
|
207
|
+
health = check_worker_health(repo_root, record.id)
|
|
208
|
+
|
|
209
|
+
state = record.state or {}
|
|
210
|
+
current_ticket = None
|
|
211
|
+
if isinstance(state, dict):
|
|
212
|
+
ticket_engine = state.get("ticket_engine")
|
|
213
|
+
if isinstance(ticket_engine, dict):
|
|
214
|
+
current_ticket = ticket_engine.get("current_ticket")
|
|
215
|
+
if not (isinstance(current_ticket, str) and current_ticket.strip()):
|
|
216
|
+
current_ticket = None
|
|
217
|
+
effective_ticket = current_ticket
|
|
218
|
+
if not effective_ticket:
|
|
219
|
+
effective_ticket = _derive_effective_current_ticket(record, store)
|
|
220
|
+
|
|
221
|
+
updated_state: Optional[dict] = None
|
|
222
|
+
if effective_ticket and not current_ticket and isinstance(state, dict):
|
|
223
|
+
ticket_engine = state.get("ticket_engine")
|
|
224
|
+
ticket_engine = dict(ticket_engine) if isinstance(ticket_engine, dict) else {}
|
|
225
|
+
ticket_engine["current_ticket"] = effective_ticket
|
|
226
|
+
updated_state = dict(state)
|
|
227
|
+
updated_state["ticket_engine"] = ticket_engine
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"last_event_seq": last_event_seq,
|
|
231
|
+
"last_event_at": last_event_at,
|
|
232
|
+
"worker_health": health,
|
|
233
|
+
"effective_current_ticket": effective_ticket,
|
|
234
|
+
"ticket_progress": ticket_progress(repo_root),
|
|
235
|
+
"state": updated_state,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def ensure_worker(repo_root: Path, run_id: str, is_terminal: bool = False) -> dict:
|
|
240
|
+
health = check_worker_health(repo_root, run_id)
|
|
241
|
+
# Only clear metadata for dead/mismatch/invalid workers if not terminal
|
|
242
|
+
if not is_terminal and health.status in {"dead", "mismatch", "invalid"}:
|
|
243
|
+
try:
|
|
244
|
+
clear_worker_metadata(health.artifact_path.parent)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
if health.is_alive:
|
|
248
|
+
return {"status": "reused", "health": health}
|
|
249
|
+
|
|
250
|
+
proc, stdout_handle, stderr_handle = spawn_flow_worker(repo_root, run_id)
|
|
251
|
+
return {
|
|
252
|
+
"status": "spawned",
|
|
253
|
+
"health": health,
|
|
254
|
+
"proc": proc,
|
|
255
|
+
"stdout": stdout_handle,
|
|
256
|
+
"stderr": stderr_handle,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
__all__ = [
|
|
261
|
+
"BootstrapCheckResult",
|
|
262
|
+
"IssueSeedResult",
|
|
263
|
+
"bootstrap_check",
|
|
264
|
+
"build_flow_status_snapshot",
|
|
265
|
+
"ensure_worker",
|
|
266
|
+
"format_issue_as_markdown",
|
|
267
|
+
"issue_md_has_content",
|
|
268
|
+
"issue_md_path",
|
|
269
|
+
"ticket_progress",
|
|
270
|
+
"seed_issue_from_github",
|
|
271
|
+
"seed_issue_from_text",
|
|
272
|
+
]
|
|
@@ -152,7 +152,8 @@ def check_worker_health(
|
|
|
152
152
|
try:
|
|
153
153
|
data = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
154
154
|
pid = int(data.get("pid")) if data.get("pid") is not None else None
|
|
155
|
-
|
|
155
|
+
raw_cmd = data.get("cmd") or []
|
|
156
|
+
cmd = [str(part) for part in raw_cmd] if isinstance(raw_cmd, list) else []
|
|
156
157
|
except Exception:
|
|
157
158
|
return FlowWorkerHealth(
|
|
158
159
|
status="invalid",
|
|
@@ -166,7 +167,7 @@ def check_worker_health(
|
|
|
166
167
|
return FlowWorkerHealth(
|
|
167
168
|
status="invalid",
|
|
168
169
|
pid=pid,
|
|
169
|
-
cmdline=cmd
|
|
170
|
+
cmdline=cmd,
|
|
170
171
|
artifact_path=metadata_path,
|
|
171
172
|
message="missing or invalid PID",
|
|
172
173
|
)
|
|
@@ -175,19 +176,19 @@ def check_worker_health(
|
|
|
175
176
|
return FlowWorkerHealth(
|
|
176
177
|
status="dead",
|
|
177
178
|
pid=pid,
|
|
178
|
-
cmdline=cmd
|
|
179
|
+
cmdline=cmd,
|
|
179
180
|
artifact_path=metadata_path,
|
|
180
181
|
message="worker PID not running",
|
|
181
182
|
)
|
|
182
183
|
|
|
183
|
-
expected_cmd = _build_worker_cmd(entrypoint, run_id)
|
|
184
|
+
expected_cmd = cmd or _build_worker_cmd(entrypoint, run_id)
|
|
184
185
|
actual_cmd = _read_process_cmdline(pid)
|
|
185
186
|
if actual_cmd is None:
|
|
186
187
|
# Can't inspect cmdline; trust the PID check.
|
|
187
188
|
return FlowWorkerHealth(
|
|
188
189
|
status="alive",
|
|
189
190
|
pid=pid,
|
|
190
|
-
cmdline=cmd
|
|
191
|
+
cmdline=cmd,
|
|
191
192
|
artifact_path=metadata_path,
|
|
192
193
|
message="worker running (cmdline unknown)",
|
|
193
194
|
)
|
|
@@ -198,7 +199,7 @@ def check_worker_health(
|
|
|
198
199
|
pid=pid,
|
|
199
200
|
cmdline=actual_cmd,
|
|
200
201
|
artifact_path=metadata_path,
|
|
201
|
-
message="worker PID command does not match
|
|
202
|
+
message="worker PID command does not match stored metadata",
|
|
202
203
|
)
|
|
203
204
|
|
|
204
205
|
return FlowWorkerHealth(
|
|
@@ -210,6 +211,31 @@ def check_worker_health(
|
|
|
210
211
|
)
|
|
211
212
|
|
|
212
213
|
|
|
214
|
+
def register_worker_metadata(
|
|
215
|
+
repo_root: Path,
|
|
216
|
+
run_id: str,
|
|
217
|
+
*,
|
|
218
|
+
artifacts_root: Optional[Path] = None,
|
|
219
|
+
pid: Optional[int] = None,
|
|
220
|
+
cmd: Optional[list[str]] = None,
|
|
221
|
+
entrypoint: str = "codex_autorunner",
|
|
222
|
+
) -> Path:
|
|
223
|
+
normalized_run_id = _normalized_run_id(run_id)
|
|
224
|
+
artifacts_dir = _worker_artifacts_dir(repo_root, normalized_run_id, artifacts_root)
|
|
225
|
+
|
|
226
|
+
resolved_pid = pid or os.getpid()
|
|
227
|
+
resolved_cmd = cmd or _read_process_cmdline(resolved_pid)
|
|
228
|
+
if not resolved_cmd:
|
|
229
|
+
resolved_cmd = _build_worker_cmd(entrypoint, normalized_run_id)
|
|
230
|
+
|
|
231
|
+
_write_worker_metadata(
|
|
232
|
+
_worker_metadata_path(artifacts_dir),
|
|
233
|
+
resolved_pid,
|
|
234
|
+
resolved_cmd,
|
|
235
|
+
)
|
|
236
|
+
return artifacts_dir
|
|
237
|
+
|
|
238
|
+
|
|
213
239
|
def spawn_flow_worker(
|
|
214
240
|
repo_root: Path,
|
|
215
241
|
run_id: str,
|
|
@@ -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
|
+
}
|