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.
Files changed (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,268 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import json
5
- import logging
6
- import re
7
- import shlex
8
- from pathlib import Path
9
- from typing import Any, Optional
10
-
11
- from ...core.logging_utils import log_event
12
- from ...core.state import now_iso
13
- from ...core.utils import atomic_write, read_json
14
- from .pr_flow import PrFlowError, PrFlowManager
15
- from .service import GitHubService
16
-
17
- COMMANDS = {"implement", "fix", "status", "stop", "resume"}
18
- ISSUE_URL_RE = re.compile(r"/issues/(?P<num>\d+)")
19
-
20
-
21
- def _chatops_state_path(repo_root: Path) -> Path:
22
- return repo_root / ".codex-autorunner" / "pr_flow" / "chatops_state.json"
23
-
24
-
25
- def _parse_command(text: str) -> Optional[tuple[str, list[str]]]:
26
- for line in (text or "").splitlines():
27
- if "@car" not in line and "/car" not in line:
28
- continue
29
- try:
30
- tokens = [tok for tok in shlex.split(line) if tok]
31
- except ValueError:
32
- tokens = [tok for tok in line.split() if tok]
33
- for idx, token in enumerate(tokens):
34
- raw = token.strip().rstrip(":,")
35
- if raw.startswith("@car") or raw == "/car":
36
- if idx + 1 >= len(tokens):
37
- return None
38
- cmd = tokens[idx + 1].strip().lower()
39
- args = tokens[idx + 2 :]
40
- if cmd in COMMANDS:
41
- return cmd, args
42
- return None
43
-
44
-
45
- def _parse_flags(args: list[str]) -> dict[str, Any]:
46
- flags: dict[str, Any] = {}
47
- idx = 0
48
- while idx < len(args):
49
- token = args[idx]
50
- if token == "--until" and idx + 1 < len(args):
51
- flags["stop_condition"] = args[idx + 1]
52
- idx += 2
53
- continue
54
- if token == "--draft":
55
- flags["draft"] = True
56
- idx += 1
57
- continue
58
- if token == "--ready":
59
- flags["draft"] = False
60
- idx += 1
61
- continue
62
- if token == "--base" and idx + 1 < len(args):
63
- flags["base_branch"] = args[idx + 1]
64
- idx += 2
65
- continue
66
- if token in ("--max-cycles", "--max_cycles") and idx + 1 < len(args):
67
- try:
68
- flags["max_cycles"] = int(args[idx + 1])
69
- except ValueError:
70
- pass
71
- idx += 2
72
- continue
73
- idx += 1
74
- return flags
75
-
76
-
77
- def _extract_issue_number(issue_url: str) -> Optional[int]:
78
- if not issue_url:
79
- return None
80
- match = ISSUE_URL_RE.search(issue_url)
81
- if not match:
82
- return None
83
- try:
84
- return int(match.group("num"))
85
- except ValueError:
86
- return None
87
-
88
-
89
- class GitHubChatOpsPoller:
90
- def __init__(
91
- self,
92
- repo_root: Path,
93
- pr_flow: PrFlowManager,
94
- *,
95
- logger: Optional[logging.Logger] = None,
96
- ) -> None:
97
- self._repo_root = repo_root
98
- self._pr_flow = pr_flow
99
- self._logger = logger or logging.getLogger("codex_autorunner.github_chatops")
100
- self._stop_event = asyncio.Event()
101
-
102
- async def run(self) -> None:
103
- cfg = self._pr_flow.chatops_config()
104
- if not cfg.get("enabled", False):
105
- return
106
- poll_interval = int(cfg.get("poll_interval_seconds", 60))
107
- while not self._stop_event.is_set():
108
- try:
109
- await self.poll_once()
110
- except Exception as exc:
111
- log_event(
112
- self._logger,
113
- logging.WARNING,
114
- "github.chatops.poll.failed",
115
- exc=exc,
116
- )
117
- try:
118
- await asyncio.wait_for(self._stop_event.wait(), timeout=poll_interval)
119
- except asyncio.TimeoutError:
120
- continue
121
-
122
- async def stop(self) -> None:
123
- self._stop_event.set()
124
-
125
- async def poll_once(self) -> None:
126
- gh = GitHubService(self._repo_root)
127
- if not gh.gh_available() or not gh.gh_authenticated():
128
- return
129
- repo = gh.repo_info()
130
- owner, repo_name = repo.name_with_owner.split("/", 1)
131
- state = self._load_state()
132
- since = state.get("last_seen")
133
- comments = gh.issue_comments(
134
- owner=owner, repo=repo_name, since=since, limit=100
135
- )
136
- if not comments:
137
- return
138
- comments.sort(key=lambda item: item.get("created_at") or "")
139
- processed = set(state.get("processed_ids") or [])
140
- max_seen = since or ""
141
- for comment in comments:
142
- comment_id = comment.get("id")
143
- if comment_id in processed:
144
- continue
145
- processed.add(comment_id)
146
- created_at = comment.get("created_at") or ""
147
- if created_at and created_at > max_seen:
148
- max_seen = created_at
149
- if not self._authorized(comment):
150
- continue
151
- parsed = _parse_command(comment.get("body") or "")
152
- if not parsed:
153
- continue
154
- command, args = parsed
155
- issue_number = _extract_issue_number(comment.get("issue_url") or "")
156
- if not issue_number:
157
- continue
158
- issue_meta = gh.issue_meta(owner=owner, repo=repo_name, number=issue_number)
159
- is_pr = bool(issue_meta.get("pull_request"))
160
- response = await self._handle_command(
161
- gh,
162
- command,
163
- args,
164
- issue_number=issue_number,
165
- is_pr=is_pr,
166
- )
167
- if response:
168
- gh.create_issue_comment(
169
- owner=owner,
170
- repo=repo_name,
171
- number=issue_number,
172
- body=response,
173
- )
174
- self._save_state(
175
- {
176
- "processed_ids": list(processed)[-500:],
177
- "last_seen": max_seen or now_iso(),
178
- }
179
- )
180
-
181
- def _authorized(self, comment: dict[str, Any]) -> bool:
182
- cfg = self._pr_flow.chatops_config()
183
- user = comment.get("user") or {}
184
- login = user.get("login") if isinstance(user, dict) else None
185
- if cfg.get("ignore_bots", True):
186
- if isinstance(user, dict) and user.get("type") == "Bot":
187
- return False
188
- if isinstance(login, str) and login.endswith("[bot]"):
189
- return False
190
- allow_users = cfg.get("allow_users") or []
191
- allow_assoc = cfg.get("allow_associations") or []
192
- allowed = False
193
- if allow_users:
194
- allowed = allowed or (login in allow_users)
195
- if allow_assoc:
196
- assoc = str(comment.get("author_association") or "").upper()
197
- allowed = allowed or (assoc in {str(a).upper() for a in allow_assoc})
198
- return allowed
199
-
200
- async def _handle_command(
201
- self,
202
- gh: GitHubService,
203
- command: str,
204
- args: list[str],
205
- *,
206
- issue_number: int,
207
- is_pr: bool,
208
- ) -> Optional[str]:
209
- flags = _parse_flags(args)
210
- try:
211
- if command == "status":
212
- flow = self._pr_flow.status()
213
- return self._format_status(flow)
214
- if command == "stop":
215
- flow = self._pr_flow.stop()
216
- return f"Stopped workflow {flow.get('id') or '(unknown)'}."
217
- if command == "resume":
218
- flow = self._pr_flow.resume()
219
- return self._format_status(flow, prefix="Resumed")
220
- if command == "implement":
221
- if is_pr:
222
- return "Command ignored: implement is for issues, not PRs."
223
- payload = {
224
- "mode": "issue",
225
- "issue": str(issue_number),
226
- **flags,
227
- }
228
- flow = self._pr_flow.start(payload=payload)
229
- return self._format_status(flow, prefix="Started")
230
- if command == "fix":
231
- if not is_pr:
232
- return "Command ignored: fix is for PRs."
233
- payload = {
234
- "mode": "pr",
235
- "pr": str(issue_number),
236
- **flags,
237
- }
238
- flow = self._pr_flow.start(payload=payload)
239
- return self._format_status(flow, prefix="Started")
240
- except PrFlowError as exc:
241
- return f"PR flow error: {exc}"
242
- except Exception as exc:
243
- return f"PR flow error: {exc}"
244
- return None
245
-
246
- def _format_status(self, flow: dict[str, Any], *, prefix: str = "Status") -> str:
247
- status = flow.get("status") or "unknown"
248
- step = flow.get("step") or "unknown"
249
- wf_id = flow.get("id") or "unknown"
250
- pr_url = flow.get("pr_url")
251
- line = f"{prefix} workflow {wf_id}: {status} (step: {step})"
252
- if pr_url:
253
- line = f"{line}\nPR: {pr_url}"
254
- return line
255
-
256
- def _load_state(self) -> dict[str, Any]:
257
- path = _chatops_state_path(self._repo_root)
258
- data = read_json(path) or {}
259
- if not isinstance(data, dict):
260
- data = {}
261
- data.setdefault("processed_ids", [])
262
- data.setdefault("last_seen", "")
263
- return data
264
-
265
- def _save_state(self, state: dict[str, Any]) -> None:
266
- path = _chatops_state_path(self._repo_root)
267
- path.parent.mkdir(parents=True, exist_ok=True)
268
- atomic_write(path, json.dumps(state, indent=2) + "\n")