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.
Files changed (226) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/__init__.py +20 -0
  3. codex_autorunner/agents/base.py +2 -2
  4. codex_autorunner/agents/codex/harness.py +1 -1
  5. codex_autorunner/agents/opencode/__init__.py +4 -0
  6. codex_autorunner/agents/opencode/agent_config.py +104 -0
  7. codex_autorunner/agents/opencode/client.py +305 -28
  8. codex_autorunner/agents/opencode/harness.py +71 -20
  9. codex_autorunner/agents/opencode/logging.py +225 -0
  10. codex_autorunner/agents/opencode/run_prompt.py +261 -0
  11. codex_autorunner/agents/opencode/runtime.py +1202 -132
  12. codex_autorunner/agents/opencode/supervisor.py +194 -68
  13. codex_autorunner/agents/registry.py +258 -0
  14. codex_autorunner/agents/types.py +2 -2
  15. codex_autorunner/api.py +25 -0
  16. codex_autorunner/bootstrap.py +19 -40
  17. codex_autorunner/cli.py +234 -151
  18. codex_autorunner/core/about_car.py +44 -32
  19. codex_autorunner/core/adapter_utils.py +21 -0
  20. codex_autorunner/core/app_server_events.py +15 -6
  21. codex_autorunner/core/app_server_logging.py +55 -15
  22. codex_autorunner/core/app_server_prompts.py +28 -259
  23. codex_autorunner/core/app_server_threads.py +15 -26
  24. codex_autorunner/core/circuit_breaker.py +183 -0
  25. codex_autorunner/core/codex_runner.py +6 -0
  26. codex_autorunner/core/config.py +555 -133
  27. codex_autorunner/core/docs.py +54 -9
  28. codex_autorunner/core/drafts.py +82 -0
  29. codex_autorunner/core/engine.py +828 -274
  30. codex_autorunner/core/exceptions.py +60 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +178 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +75 -0
  35. codex_autorunner/core/flows/runtime.py +351 -0
  36. codex_autorunner/core/flows/store.py +485 -0
  37. codex_autorunner/core/flows/transition.py +133 -0
  38. codex_autorunner/core/flows/worker_process.py +242 -0
  39. codex_autorunner/core/hub.py +21 -13
  40. codex_autorunner/core/locks.py +118 -1
  41. codex_autorunner/core/logging_utils.py +9 -6
  42. codex_autorunner/core/path_utils.py +123 -0
  43. codex_autorunner/core/prompt.py +15 -7
  44. codex_autorunner/core/redaction.py +29 -0
  45. codex_autorunner/core/retry.py +61 -0
  46. codex_autorunner/core/review.py +888 -0
  47. codex_autorunner/core/review_context.py +161 -0
  48. codex_autorunner/core/run_index.py +223 -0
  49. codex_autorunner/core/runner_controller.py +44 -1
  50. codex_autorunner/core/runner_process.py +30 -1
  51. codex_autorunner/core/sqlite_utils.py +32 -0
  52. codex_autorunner/core/state.py +273 -44
  53. codex_autorunner/core/static_assets.py +55 -0
  54. codex_autorunner/core/supervisor_utils.py +67 -0
  55. codex_autorunner/core/text_delta_coalescer.py +43 -0
  56. codex_autorunner/core/update.py +20 -11
  57. codex_autorunner/core/update_runner.py +2 -0
  58. codex_autorunner/core/usage.py +107 -75
  59. codex_autorunner/core/utils.py +167 -3
  60. codex_autorunner/discovery.py +3 -3
  61. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  62. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  63. codex_autorunner/integrations/agents/__init__.py +27 -0
  64. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  65. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  66. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  67. codex_autorunner/integrations/agents/run_event.py +71 -0
  68. codex_autorunner/integrations/app_server/client.py +708 -153
  69. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  70. codex_autorunner/integrations/telegram/adapter.py +474 -185
  71. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  72. codex_autorunner/integrations/telegram/config.py +239 -1
  73. codex_autorunner/integrations/telegram/constants.py +19 -1
  74. codex_autorunner/integrations/telegram/dispatch.py +44 -8
  75. codex_autorunner/integrations/telegram/doctor.py +47 -0
  76. codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
  77. codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
  78. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
  79. codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
  80. codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
  81. codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
  82. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  83. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
  84. codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
  85. codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
  86. codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
  87. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
  88. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
  89. codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
  90. codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
  91. codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
  92. codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
  93. codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
  94. codex_autorunner/integrations/telegram/helpers.py +90 -18
  95. codex_autorunner/integrations/telegram/notifications.py +126 -35
  96. codex_autorunner/integrations/telegram/outbox.py +214 -43
  97. codex_autorunner/integrations/telegram/progress_stream.py +42 -19
  98. codex_autorunner/integrations/telegram/runtime.py +24 -13
  99. codex_autorunner/integrations/telegram/service.py +500 -129
  100. codex_autorunner/integrations/telegram/state.py +1278 -330
  101. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  102. codex_autorunner/integrations/telegram/transport.py +37 -4
  103. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  104. codex_autorunner/integrations/telegram/types.py +22 -2
  105. codex_autorunner/integrations/telegram/voice.py +14 -15
  106. codex_autorunner/manifest.py +2 -0
  107. codex_autorunner/plugin_api.py +22 -0
  108. codex_autorunner/routes/__init__.py +25 -14
  109. codex_autorunner/routes/agents.py +18 -78
  110. codex_autorunner/routes/analytics.py +239 -0
  111. codex_autorunner/routes/base.py +142 -113
  112. codex_autorunner/routes/file_chat.py +836 -0
  113. codex_autorunner/routes/flows.py +980 -0
  114. codex_autorunner/routes/messages.py +459 -0
  115. codex_autorunner/routes/repos.py +17 -0
  116. codex_autorunner/routes/review.py +148 -0
  117. codex_autorunner/routes/sessions.py +16 -8
  118. codex_autorunner/routes/settings.py +22 -0
  119. codex_autorunner/routes/shared.py +33 -3
  120. codex_autorunner/routes/system.py +22 -1
  121. codex_autorunner/routes/usage.py +87 -0
  122. codex_autorunner/routes/voice.py +5 -13
  123. codex_autorunner/routes/workspace.py +271 -0
  124. codex_autorunner/server.py +2 -1
  125. codex_autorunner/static/agentControls.js +9 -1
  126. codex_autorunner/static/agentEvents.js +248 -0
  127. codex_autorunner/static/app.js +27 -22
  128. codex_autorunner/static/autoRefresh.js +29 -1
  129. codex_autorunner/static/bootstrap.js +1 -0
  130. codex_autorunner/static/bus.js +1 -0
  131. codex_autorunner/static/cache.js +1 -0
  132. codex_autorunner/static/constants.js +20 -4
  133. codex_autorunner/static/dashboard.js +162 -150
  134. codex_autorunner/static/diffRenderer.js +37 -0
  135. codex_autorunner/static/docChatCore.js +324 -0
  136. codex_autorunner/static/docChatStorage.js +65 -0
  137. codex_autorunner/static/docChatVoice.js +65 -0
  138. codex_autorunner/static/docEditor.js +133 -0
  139. codex_autorunner/static/env.js +1 -0
  140. codex_autorunner/static/eventSummarizer.js +166 -0
  141. codex_autorunner/static/fileChat.js +182 -0
  142. codex_autorunner/static/health.js +155 -0
  143. codex_autorunner/static/hub.js +67 -126
  144. codex_autorunner/static/index.html +788 -807
  145. codex_autorunner/static/liveUpdates.js +59 -0
  146. codex_autorunner/static/loader.js +1 -0
  147. codex_autorunner/static/messages.js +470 -0
  148. codex_autorunner/static/mobileCompact.js +2 -1
  149. codex_autorunner/static/settings.js +24 -205
  150. codex_autorunner/static/styles.css +7577 -3758
  151. codex_autorunner/static/tabs.js +28 -5
  152. codex_autorunner/static/terminal.js +14 -0
  153. codex_autorunner/static/terminalManager.js +53 -59
  154. codex_autorunner/static/ticketChatActions.js +333 -0
  155. codex_autorunner/static/ticketChatEvents.js +16 -0
  156. codex_autorunner/static/ticketChatStorage.js +16 -0
  157. codex_autorunner/static/ticketChatStream.js +264 -0
  158. codex_autorunner/static/ticketEditor.js +750 -0
  159. codex_autorunner/static/ticketVoice.js +9 -0
  160. codex_autorunner/static/tickets.js +1315 -0
  161. codex_autorunner/static/utils.js +32 -3
  162. codex_autorunner/static/voice.js +21 -7
  163. codex_autorunner/static/workspace.js +672 -0
  164. codex_autorunner/static/workspaceApi.js +53 -0
  165. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  166. codex_autorunner/tickets/__init__.py +20 -0
  167. codex_autorunner/tickets/agent_pool.py +377 -0
  168. codex_autorunner/tickets/files.py +85 -0
  169. codex_autorunner/tickets/frontmatter.py +55 -0
  170. codex_autorunner/tickets/lint.py +102 -0
  171. codex_autorunner/tickets/models.py +95 -0
  172. codex_autorunner/tickets/outbox.py +232 -0
  173. codex_autorunner/tickets/replies.py +179 -0
  174. codex_autorunner/tickets/runner.py +823 -0
  175. codex_autorunner/tickets/spec_ingest.py +77 -0
  176. codex_autorunner/voice/capture.py +7 -7
  177. codex_autorunner/voice/service.py +51 -9
  178. codex_autorunner/web/app.py +419 -199
  179. codex_autorunner/web/hub_jobs.py +13 -2
  180. codex_autorunner/web/middleware.py +47 -13
  181. codex_autorunner/web/pty_session.py +26 -13
  182. codex_autorunner/web/schemas.py +114 -109
  183. codex_autorunner/web/static_assets.py +55 -42
  184. codex_autorunner/web/static_refresh.py +86 -0
  185. codex_autorunner/workspace/__init__.py +40 -0
  186. codex_autorunner/workspace/paths.py +319 -0
  187. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
  188. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  189. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  190. codex_autorunner/core/doc_chat.py +0 -1415
  191. codex_autorunner/core/snapshot.py +0 -580
  192. codex_autorunner/integrations/github/chatops.py +0 -268
  193. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  194. codex_autorunner/routes/docs.py +0 -381
  195. codex_autorunner/routes/github.py +0 -327
  196. codex_autorunner/routes/runs.py +0 -118
  197. codex_autorunner/spec_ingest.py +0 -788
  198. codex_autorunner/static/docChatActions.js +0 -279
  199. codex_autorunner/static/docChatEvents.js +0 -300
  200. codex_autorunner/static/docChatRender.js +0 -205
  201. codex_autorunner/static/docChatStream.js +0 -361
  202. codex_autorunner/static/docs.js +0 -20
  203. codex_autorunner/static/docsClipboard.js +0 -69
  204. codex_autorunner/static/docsCrud.js +0 -257
  205. codex_autorunner/static/docsDocUpdates.js +0 -62
  206. codex_autorunner/static/docsDrafts.js +0 -16
  207. codex_autorunner/static/docsElements.js +0 -69
  208. codex_autorunner/static/docsInit.js +0 -274
  209. codex_autorunner/static/docsParse.js +0 -160
  210. codex_autorunner/static/docsSnapshot.js +0 -87
  211. codex_autorunner/static/docsSpecIngest.js +0 -263
  212. codex_autorunner/static/docsState.js +0 -127
  213. codex_autorunner/static/docsThreadRegistry.js +0 -44
  214. codex_autorunner/static/docsUi.js +0 -153
  215. codex_autorunner/static/docsVoice.js +0 -56
  216. codex_autorunner/static/github.js +0 -442
  217. codex_autorunner/static/logs.js +0 -640
  218. codex_autorunner/static/runs.js +0 -409
  219. codex_autorunner/static/snapshot.js +0 -124
  220. codex_autorunner/static/state.js +0 -86
  221. codex_autorunner/static/todoPreview.js +0 -27
  222. codex_autorunner/workspace.py +0 -16
  223. codex_autorunner-0.1.1.dist-info/RECORD +0 -191
  224. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  225. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  226. {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -10,45 +10,22 @@ from .core.config import (
10
10
  ConfigError,
11
11
  resolve_hub_config_data,
12
12
  )
13
+ from .core.state import RunnerState, save_state
13
14
  from .core.utils import atomic_write
14
15
  from .manifest import load_manifest
15
16
 
16
17
  GITIGNORE_CONTENT = "*"
18
+ GENERATED_CONFIG_HEADER = "# GENERATED by CAR - DO NOT EDIT\n"
17
19
 
18
20
 
19
21
  def sample_todo() -> str:
20
22
  return """# TODO\n\n- [ ] Replace this item with your first task\n- [ ] Add another task\n- [x] Example completed item\n"""
21
23
 
22
24
 
23
- def sample_opinions() -> str:
24
- return """# Opinions\n\n- Prefer small, well-tested changes.\n- Keep docs in sync with code.\n- Avoid unnecessary dependencies.\n"""
25
-
26
-
27
25
  def sample_spec() -> str:
28
26
  return """# Spec\n\n## Context\n- Add project background and goals here.\n\n## Requirements\n- Requirement 1\n- Requirement 2\n\n## Non-goals\n- Out of scope items\n"""
29
27
 
30
28
 
31
- def sample_summary() -> str:
32
- return """# Summary
33
-
34
- This doc is the **user-facing report and handoff** for work done by CAR agents.
35
-
36
- Use it for:
37
- - Anything that requires **user action** or an **external party** (not agents).
38
- - Unresolved decisions or blockers that agents can’t finish autonomously.
39
- - A final condensed report once TODO is complete.
40
-
41
- ## External/user actions
42
- - (none)
43
-
44
- ## Open questions / blockers
45
- - (none)
46
-
47
- ## Final report
48
- - (pending)
49
- """
50
-
51
-
52
29
  def _seed_doc(path: Path, force: bool, content: str) -> None:
53
30
  if path.exists() and not force:
54
31
  return
@@ -81,6 +58,7 @@ def write_hub_config(hub_root: Path, force: bool = False) -> Path:
81
58
  return config_path
82
59
  config_path.parent.mkdir(parents=True, exist_ok=True)
83
60
  with config_path.open("w", encoding="utf-8") as f:
61
+ f.write(GENERATED_CONFIG_HEADER)
84
62
  yaml.safe_dump(
85
63
  resolve_hub_config_data(hub_root),
86
64
  f,
@@ -107,32 +85,33 @@ def seed_repo_files(
107
85
  if not gitignore_path.exists() or force:
108
86
  gitignore_path.write_text(GITIGNORE_CONTENT, encoding="utf-8")
109
87
 
110
- state_path = ca_dir / "state.json"
88
+ state_path = ca_dir / "state.sqlite3"
111
89
  if not state_path.exists() or force:
112
- atomic_write(
113
- state_path,
114
- '{\n "last_run_id": null,\n "status": "idle",\n "last_exit_code": null,\n "last_run_started_at": null,\n "last_run_finished_at": null,\n "runner_pid": null\n}\n',
115
- )
90
+ save_state(state_path, RunnerState(None, "idle", None, None, None))
116
91
 
117
92
  log_path = ca_dir / "codex-autorunner.log"
118
93
  if not log_path.exists() or force:
119
94
  log_path.write_text("", encoding="utf-8")
120
95
 
121
- _seed_doc(ca_dir / "TODO.md", force, sample_todo())
122
- _seed_doc(ca_dir / "PROGRESS.md", force, "# Progress\n\n")
123
- _seed_doc(ca_dir / "OPINIONS.md", force, sample_opinions())
124
- _seed_doc(ca_dir / "SPEC.md", force, sample_spec())
125
- _seed_doc(ca_dir / "SUMMARY.md", force, sample_summary())
96
+ tickets_dir = ca_dir / "tickets"
97
+ if not tickets_dir.exists() or force:
98
+ tickets_dir.mkdir(parents=True, exist_ok=True)
99
+
100
+ workspace_dir = ca_dir / "workspace"
101
+ if not workspace_dir.exists() or force:
102
+ workspace_dir.mkdir(parents=True, exist_ok=True)
103
+
104
+ _seed_doc(workspace_dir / "active_context.md", force, sample_todo())
105
+ _seed_doc(workspace_dir / "decisions.md", force, "# Decisions\n\n")
106
+ _seed_doc(workspace_dir / "spec.md", force, sample_spec())
126
107
 
127
108
  # Seed an always-available briefing doc for interactive Codex sessions.
128
109
  ensure_about_car_file_for_repo(
129
110
  repo_root,
130
111
  doc_paths={
131
- "todo": ca_dir / "TODO.md",
132
- "progress": ca_dir / "PROGRESS.md",
133
- "opinions": ca_dir / "OPINIONS.md",
134
- "spec": ca_dir / "SPEC.md",
135
- "summary": ca_dir / "SUMMARY.md",
112
+ "active_context": workspace_dir / "active_context.md",
113
+ "decisions": workspace_dir / "decisions.md",
114
+ "spec": workspace_dir / "spec.md",
136
115
  },
137
116
  force=force,
138
117
  )
codex_autorunner/cli.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
5
  import os
6
6
  import shlex
7
7
  import subprocess
8
+ import uuid
8
9
  from pathlib import Path
9
10
  from typing import NoReturn, Optional
10
11
 
@@ -18,16 +19,17 @@ from .core.config import (
18
19
  CONFIG_FILENAME,
19
20
  ConfigError,
20
21
  HubConfig,
22
+ RepoConfig,
21
23
  _normalize_base_path,
24
+ derive_repo_config,
22
25
  find_nearest_hub_config_path,
23
26
  load_hub_config,
24
27
  )
25
- from .core.engine import Engine, LockError, clear_stale_lock, doctor
28
+ from .core.engine import DoctorReport, Engine, LockError, clear_stale_lock, doctor
26
29
  from .core.git_utils import GitError, run_git
27
30
  from .core.hub import HubSupervisor
28
31
  from .core.logging_utils import log_event, setup_rotating_logger
29
32
  from .core.optional_dependencies import require_optional_dependencies
30
- from .core.snapshot import SnapshotError
31
33
  from .core.state import RunnerState, load_state, now_iso, save_state, state_lock
32
34
  from .core.usage import (
33
35
  UsageError,
@@ -37,20 +39,21 @@ from .core.usage import (
37
39
  summarize_repo_usage,
38
40
  )
39
41
  from .core.utils import RepoNotFoundError, default_editor, find_repo_root
40
- from .integrations.app_server.env import build_app_server_env
41
- from .integrations.app_server.supervisor import WorkspaceAppServerSupervisor
42
42
  from .integrations.telegram.adapter import TelegramAPIError, TelegramBotClient
43
+ from .integrations.telegram.doctor import telegram_doctor_checks
43
44
  from .integrations.telegram.service import (
44
45
  TelegramBotConfig,
45
46
  TelegramBotConfigError,
46
47
  TelegramBotLockError,
47
48
  TelegramBotService,
48
49
  )
50
+ from .integrations.telegram.state import TelegramStateStore
49
51
  from .manifest import load_manifest
50
52
  from .server import create_hub_app
51
- from .spec_ingest import SpecIngestError, SpecIngestService, clear_work_docs
52
53
  from .voice import VoiceConfig
53
54
 
55
+ logger = logging.getLogger("codex_autorunner.cli")
56
+
54
57
  app = typer.Typer(add_completion=False)
55
58
  hub_app = typer.Typer(add_completion=False)
56
59
  telegram_app = typer.Typer(add_completion=False)
@@ -115,14 +118,16 @@ def _resolve_repo_api_path(repo_root: Path, hub: Optional[Path], path: str) -> s
115
118
  manifest_value = hub_cfg.get("manifest")
116
119
  if isinstance(manifest_value, str) and manifest_value.strip():
117
120
  manifest_rel = manifest_value.strip()
118
- except Exception:
121
+ except (OSError, yaml.YAMLError, KeyError, ValueError) as exc:
122
+ logger.debug("Failed to read hub config for manifest: %s", exc)
119
123
  manifest_rel = None
120
124
  manifest_path = hub_root / (manifest_rel or ".codex-autorunner/manifest.yml")
121
125
  if not manifest_path.exists():
122
126
  return path
123
127
  try:
124
128
  manifest = load_manifest(manifest_path, hub_root)
125
- except Exception:
129
+ except (OSError, ValueError, KeyError) as exc:
130
+ logger.debug("Failed to load manifest: %s", exc)
126
131
  return path
127
132
  repo_root = repo_root.resolve()
128
133
  for entry in manifest.repos:
@@ -287,6 +292,7 @@ def init(
287
292
  def status(
288
293
  repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
289
294
  hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
295
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output"),
290
296
  ):
291
297
  """Show autorunner status."""
292
298
  engine = _require_repo_config(repo, hub)
@@ -301,6 +307,51 @@ def status(
301
307
  opencode_record = (
302
308
  state.sessions.get(opencode_session_id) if opencode_session_id else None
303
309
  )
310
+
311
+ if output_json:
312
+ hub_config_path = _resolve_hub_config_path_for_cli(engine.repo_root, hub)
313
+ payload = {
314
+ "repo": str(engine.repo_root),
315
+ "hub": (
316
+ str(hub_config_path.parent.parent.resolve())
317
+ if hub_config_path
318
+ else None
319
+ ),
320
+ "status": state.status,
321
+ "last_run_id": state.last_run_id,
322
+ "last_exit_code": state.last_exit_code,
323
+ "last_run_started_at": state.last_run_started_at,
324
+ "last_run_finished_at": state.last_run_finished_at,
325
+ "runner_pid": state.runner_pid,
326
+ "session_id": session_id,
327
+ "session_record": (
328
+ {
329
+ "repo_path": session_record.repo_path,
330
+ "created_at": session_record.created_at,
331
+ "last_seen_at": session_record.last_seen_at,
332
+ "status": session_record.status,
333
+ "agent": session_record.agent,
334
+ }
335
+ if session_record
336
+ else None
337
+ ),
338
+ "opencode_session_id": opencode_session_id,
339
+ "opencode_record": (
340
+ {
341
+ "repo_path": opencode_record.repo_path,
342
+ "created_at": opencode_record.created_at,
343
+ "last_seen_at": opencode_record.last_seen_at,
344
+ "status": opencode_record.status,
345
+ "agent": opencode_record.agent,
346
+ }
347
+ if opencode_record
348
+ else None
349
+ ),
350
+ "outstanding_todos": len(outstanding),
351
+ }
352
+ typer.echo(json.dumps(payload, indent=2))
353
+ return
354
+
304
355
  typer.echo(f"Repo: {engine.repo_root}")
305
356
  typer.echo(f"Status: {state.status}")
306
357
  typer.echo(f"Last run id: {state.last_run_id}")
@@ -341,7 +392,15 @@ def sessions(
341
392
  source = "server"
342
393
  try:
343
394
  payload = _request_json("GET", url, token_env=config.server_auth_token_env)
344
- except Exception:
395
+ except (
396
+ httpx.HTTPError,
397
+ httpx.ConnectError,
398
+ httpx.TimeoutException,
399
+ OSError,
400
+ ) as exc:
401
+ logger.debug(
402
+ "Failed to fetch sessions from server, falling back to state: %s", exc
403
+ )
345
404
  state = load_state(engine.state_path)
346
405
  payload = {
347
406
  "sessions": [
@@ -407,8 +466,15 @@ def stop_session(
407
466
  stopped_id = response.get("session_id", payload.get("session_id", ""))
408
467
  typer.echo(f"Stopped session {stopped_id}")
409
468
  return
410
- except Exception:
411
- pass
469
+ except (
470
+ httpx.HTTPError,
471
+ httpx.ConnectError,
472
+ httpx.TimeoutException,
473
+ OSError,
474
+ ) as exc:
475
+ logger.debug(
476
+ "Failed to stop session via server, falling back to state: %s", exc
477
+ )
412
478
 
413
479
  with state_lock(engine.state_path):
414
480
  state = load_state(engine.state_path)
@@ -467,7 +533,7 @@ def usage(
467
533
  except RepoNotFoundError:
468
534
  repo_root = None
469
535
 
470
- if repo_root and (repo_root / ".codex-autorunner" / "state.json").exists():
536
+ if repo_root and (repo_root / ".codex-autorunner" / "state.sqlite3").exists():
471
537
  engine = _require_repo_config(repo, hub)
472
538
  else:
473
539
  try:
@@ -568,8 +634,8 @@ def run(
568
634
  if engine:
569
635
  try:
570
636
  engine.release_lock()
571
- except Exception:
572
- pass
637
+ except OSError as exc:
638
+ logger.debug("Failed to release lock in run command: %s", exc)
573
639
 
574
640
 
575
641
  @app.command()
@@ -591,8 +657,8 @@ def once(
591
657
  if engine:
592
658
  try:
593
659
  engine.release_lock()
594
- except Exception:
595
- pass
660
+ except OSError as exc:
661
+ logger.debug("Failed to release lock in once command: %s", exc)
596
662
 
597
663
 
598
664
  @app.command()
@@ -650,8 +716,8 @@ def resume(
650
716
  if engine:
651
717
  try:
652
718
  engine.release_lock()
653
- except Exception:
654
- pass
719
+ except OSError as exc:
720
+ logger.debug("Failed to release lock in resume command: %s", exc)
655
721
 
656
722
 
657
723
  @app.command()
@@ -690,7 +756,7 @@ def log(
690
756
 
691
757
  @app.command()
692
758
  def edit(
693
- target: str = typer.Argument(..., help="todo|progress|opinions|spec"),
759
+ target: str = typer.Argument(..., help="active_context|decisions|spec"),
694
760
  repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
695
761
  hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
696
762
  ):
@@ -698,8 +764,8 @@ def edit(
698
764
  engine = _require_repo_config(repo, hub)
699
765
  config = engine.config
700
766
  key = target.lower()
701
- if key not in ("todo", "progress", "opinions", "spec"):
702
- _raise_exit("Invalid target; choose todo, progress, opinions, or spec")
767
+ if key not in ("active_context", "decisions", "spec"):
768
+ _raise_exit("Invalid target; choose active_context, decisions, or spec")
703
769
  path = config.doc_path(key)
704
770
  editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or default_editor()
705
771
  editor_parts = shlex.split(editor)
@@ -709,84 +775,6 @@ def edit(
709
775
  subprocess.run([*editor_parts, str(path)])
710
776
 
711
777
 
712
- @app.command("ingest-spec")
713
- def ingest_spec_cmd(
714
- repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
715
- hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
716
- spec: Optional[Path] = typer.Option(
717
- None, "--spec", help="Path to SPEC (defaults to configured docs.spec)"
718
- ),
719
- force: bool = typer.Option(
720
- False, "--force", help="Overwrite TODO/PROGRESS/OPINIONS"
721
- ),
722
- ):
723
- """Generate TODO/PROGRESS/OPINIONS from SPEC using Codex."""
724
- try:
725
- engine = _require_repo_config(repo, hub)
726
- config = engine.config
727
- if not config.app_server.command:
728
- raise SpecIngestError("app_server.command must be configured")
729
-
730
- async def _run_ingest() -> dict:
731
- logger = logging.getLogger("codex_autorunner.cli.app_server")
732
-
733
- def _env_builder(
734
- workspace_root: Path, _workspace_id: str, state_dir: Path
735
- ) -> dict[str, str]:
736
- state_dir.mkdir(parents=True, exist_ok=True)
737
- return build_app_server_env(
738
- config.app_server.command,
739
- workspace_root,
740
- state_dir,
741
- logger=logger,
742
- event_prefix="cli",
743
- )
744
-
745
- supervisor = WorkspaceAppServerSupervisor(
746
- config.app_server.command,
747
- state_root=config.app_server.state_root,
748
- env_builder=_env_builder,
749
- logger=logger,
750
- max_handles=config.app_server.max_handles,
751
- idle_ttl_seconds=config.app_server.idle_ttl_seconds,
752
- request_timeout=config.app_server.request_timeout,
753
- )
754
- service = SpecIngestService(engine, app_server_supervisor=supervisor)
755
- try:
756
- await service.execute(force=force, spec_path=spec, message=None)
757
- return service.apply_patch()
758
- finally:
759
- await supervisor.close_all()
760
-
761
- docs = asyncio.run(_run_ingest())
762
- except (ConfigError, SpecIngestError) as exc:
763
- _raise_exit(str(exc), cause=exc)
764
-
765
- typer.echo("Ingested SPEC into TODO/PROGRESS/OPINIONS.")
766
- for key, content in docs.items():
767
- lines = len(content.splitlines())
768
- typer.echo(f"- {key.upper()}: {lines} lines")
769
-
770
-
771
- @app.command("clear-docs")
772
- def clear_docs_cmd(
773
- repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
774
- hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
775
- yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
776
- ):
777
- """Clear TODO/PROGRESS/OPINIONS to empty templates."""
778
- if not yes:
779
- confirm = input("Clear TODO/PROGRESS/OPINIONS? Type CLEAR to confirm: ").strip()
780
- if confirm.upper() != "CLEAR":
781
- _raise_exit("Aborted.")
782
- engine = _require_repo_config(repo, hub)
783
- try:
784
- clear_work_docs(engine)
785
- except ConfigError as exc:
786
- _raise_exit(str(exc), cause=exc)
787
- typer.echo("Cleared TODO/PROGRESS/OPINIONS.")
788
-
789
-
790
778
  @app.command("doctor")
791
779
  def doctor_cmd(
792
780
  repo: Optional[Path] = typer.Option(None, "--repo", help="Repo or hub path"),
@@ -794,7 +782,19 @@ def doctor_cmd(
794
782
  ):
795
783
  """Validate repo or hub setup."""
796
784
  try:
797
- report = doctor(repo or Path.cwd())
785
+ start_path = repo or Path.cwd()
786
+ report = doctor(start_path)
787
+
788
+ hub_config = load_hub_config(start_path)
789
+ repo_config: Optional[RepoConfig] = None
790
+ try:
791
+ repo_root = find_repo_root(start_path)
792
+ repo_config = derive_repo_config(hub_config, repo_root)
793
+ except RepoNotFoundError:
794
+ repo_config = None
795
+
796
+ telegram_checks = telegram_doctor_checks(repo_config or hub_config)
797
+ report = DoctorReport(checks=report.checks + telegram_checks)
798
798
  except ConfigError as exc:
799
799
  _raise_exit(str(exc), cause=exc)
800
800
  if json_output:
@@ -812,56 +812,6 @@ def doctor_cmd(
812
812
  typer.echo("Doctor check passed")
813
813
 
814
814
 
815
- @app.command()
816
- def snapshot(
817
- repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
818
- hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
819
- ):
820
- """Generate or update `.codex-autorunner/SNAPSHOT.md`."""
821
- try:
822
- engine = _require_repo_config(repo, hub)
823
- config = engine.config
824
- if not config.app_server.command:
825
- raise SnapshotError("app_server.command must be configured")
826
-
827
- async def _run_snapshot() -> None:
828
- logger = logging.getLogger("codex_autorunner.cli.app_server")
829
-
830
- def _env_builder(
831
- workspace_root: Path, _workspace_id: str, state_dir: Path
832
- ) -> dict[str, str]:
833
- state_dir.mkdir(parents=True, exist_ok=True)
834
- return build_app_server_env(
835
- config.app_server.command,
836
- workspace_root,
837
- state_dir,
838
- logger=logger,
839
- event_prefix="cli",
840
- )
841
-
842
- supervisor = WorkspaceAppServerSupervisor(
843
- config.app_server.command,
844
- state_root=config.app_server.state_root,
845
- env_builder=_env_builder,
846
- logger=logger,
847
- max_handles=config.app_server.max_handles,
848
- idle_ttl_seconds=config.app_server.idle_ttl_seconds,
849
- request_timeout=config.app_server.request_timeout,
850
- )
851
- from .core.snapshot import SnapshotService
852
-
853
- service = SnapshotService(engine, app_server_supervisor=supervisor)
854
- try:
855
- await service.generate_snapshot()
856
- finally:
857
- await supervisor.close_all()
858
-
859
- asyncio.run(_run_snapshot())
860
- except (ConfigError, SnapshotError) as exc:
861
- _raise_exit(str(exc), cause=exc)
862
- typer.echo("Snapshot written to .codex-autorunner/SNAPSHOT.md")
863
-
864
-
865
815
  @app.command()
866
816
  def serve(
867
817
  path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
@@ -1041,6 +991,8 @@ def telegram_start(
1041
991
  housekeeping_config=config.housekeeping,
1042
992
  update_repo_url=update_repo_url,
1043
993
  update_repo_ref=update_repo_ref,
994
+ update_skip_checks=config.update_skip_checks,
995
+ app_server_auto_restart=config.app_server.auto_restart,
1044
996
  )
1045
997
  await service.run_polling()
1046
998
 
@@ -1086,8 +1038,139 @@ def telegram_health(
1086
1038
  asyncio.run(_run())
1087
1039
  except TelegramAPIError as exc:
1088
1040
  _raise_exit(f"Telegram health check failed: {exc}", cause=exc)
1089
- except Exception as exc:
1090
- _raise_exit(f"Telegram health check failed: {exc}", cause=exc)
1041
+
1042
+
1043
+ @telegram_app.command("state-check")
1044
+ def telegram_state_check(
1045
+ path: Optional[Path] = typer.Option(None, "--path", help="Repo or hub root path"),
1046
+ ):
1047
+ """Open the Telegram state DB and ensure schema migrations apply."""
1048
+ try:
1049
+ config = load_hub_config(path or Path.cwd())
1050
+ except ConfigError as exc:
1051
+ _raise_exit(str(exc), cause=exc)
1052
+ telegram_cfg = TelegramBotConfig.from_raw(
1053
+ config.raw.get("telegram_bot") if isinstance(config.raw, dict) else None,
1054
+ root=config.root,
1055
+ agent_binaries=getattr(config, "agents", None)
1056
+ and {name: agent.binary for name, agent in config.agents.items()},
1057
+ )
1058
+ if not telegram_cfg.enabled:
1059
+ _raise_exit("telegram_bot is disabled; set telegram_bot.enabled: true")
1060
+
1061
+ try:
1062
+ store = TelegramStateStore(
1063
+ telegram_cfg.state_file,
1064
+ default_approval_mode=telegram_cfg.defaults.approval_mode,
1065
+ )
1066
+ # This will open the DB and apply schema/migrations.
1067
+ store._connection_sync() # type: ignore[attr-defined]
1068
+ except Exception as exc: # pragma: no cover - defensive runtime check
1069
+ _raise_exit(f"Telegram state check failed: {exc}", cause=exc)
1070
+
1071
+
1072
+ @app.command()
1073
+ def flow(
1074
+ action: str = typer.Argument(..., help="worker"),
1075
+ repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
1076
+ hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
1077
+ run_id: Optional[str] = typer.Option(
1078
+ None, "--run-id", help="Flow run ID (for worker)"
1079
+ ),
1080
+ ):
1081
+ """Flow runtime commands."""
1082
+ engine = _require_repo_config(repo, hub)
1083
+
1084
+ if action == "worker":
1085
+ if not run_id:
1086
+ _raise_exit("--run-id is required for worker command")
1087
+ try:
1088
+ run_id = str(uuid.UUID(str(run_id)))
1089
+ except ValueError:
1090
+ _raise_exit("Invalid run_id format; must be a UUID")
1091
+
1092
+ from .core.flows import FlowController, FlowStore
1093
+ from .core.flows.models import FlowRunStatus
1094
+ from .flows.ticket_flow import build_ticket_flow_definition
1095
+ from .tickets import AgentPool
1096
+
1097
+ db_path = engine.repo_root / ".codex-autorunner" / "flows.db"
1098
+ artifacts_root = engine.repo_root / ".codex-autorunner" / "flows"
1099
+
1100
+ typer.echo(f"Starting flow worker for run {run_id}")
1101
+
1102
+ async def _run_worker():
1103
+ typer.echo(f"Flow worker started for {run_id}")
1104
+ typer.echo(f"DB path: {db_path}")
1105
+ typer.echo(f"Artifacts root: {artifacts_root}")
1106
+
1107
+ store = FlowStore(db_path)
1108
+ store.initialize()
1109
+
1110
+ record = store.get_flow_run(run_id)
1111
+ if not record:
1112
+ typer.echo(f"Flow run {run_id} not found", err=True)
1113
+ store.close()
1114
+ raise typer.Exit(code=1)
1115
+ store.close()
1116
+
1117
+ agent_pool: AgentPool | None = None
1118
+
1119
+ def _build_definition(flow_type: str):
1120
+ nonlocal agent_pool
1121
+ if flow_type == "pr_flow":
1122
+ _raise_exit(
1123
+ "PR flow is no longer supported. Use ticket_flow instead."
1124
+ )
1125
+ if flow_type == "ticket_flow":
1126
+ agent_pool = AgentPool(engine.config)
1127
+ return build_ticket_flow_definition(agent_pool=agent_pool)
1128
+ _raise_exit(f"Unknown flow type for run {run_id}: {flow_type}")
1129
+ return None
1130
+
1131
+ definition = _build_definition(record.flow_type)
1132
+ definition.validate()
1133
+
1134
+ controller = FlowController(
1135
+ definition=definition,
1136
+ db_path=db_path,
1137
+ artifacts_root=artifacts_root,
1138
+ )
1139
+ controller.initialize()
1140
+
1141
+ record = controller.get_status(run_id)
1142
+ if not record:
1143
+ typer.echo(f"Flow run {run_id} not found", err=True)
1144
+ raise typer.Exit(code=1)
1145
+
1146
+ if record.status.is_terminal() and record.status not in {
1147
+ FlowRunStatus.STOPPED,
1148
+ FlowRunStatus.FAILED,
1149
+ }:
1150
+ typer.echo(
1151
+ f"Flow run {run_id} already completed (status={record.status})"
1152
+ )
1153
+ return
1154
+
1155
+ action = (
1156
+ "Resuming" if record.status != FlowRunStatus.PENDING else "Starting"
1157
+ )
1158
+ typer.echo(f"{action} flow run {run_id} from step: {record.current_step}")
1159
+ try:
1160
+ final_record = await controller.run_flow(run_id)
1161
+ typer.echo(
1162
+ f"Flow run {run_id} finished with status {final_record.status}"
1163
+ )
1164
+ finally:
1165
+ if agent_pool is not None:
1166
+ try:
1167
+ await agent_pool.close()
1168
+ except Exception:
1169
+ typer.echo("Failed to close agent pool cleanly", err=True)
1170
+
1171
+ asyncio.run(_run_worker())
1172
+ else:
1173
+ _raise_exit(f"Unknown action: {action}")
1091
1174
 
1092
1175
 
1093
1176
  if __name__ == "__main__":