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
@@ -3,117 +3,31 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Callable, Optional
5
5
 
6
- from .config import (
7
- AppServerAutorunnerPromptConfig,
8
- AppServerDocChatPromptConfig,
9
- AppServerSpecIngestPromptConfig,
10
- Config,
11
- )
6
+ from .config import AppServerAutorunnerPromptConfig, Config
12
7
 
13
8
  TRUNCATION_MARKER = "...[truncated]"
14
9
 
15
10
 
16
- DOC_CHAT_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant helping maintain the work docs for this repository.
17
-
18
- Instructions:
19
- - Use the base doc content below. Drafts (if present) are the authoritative base.
20
- - You may inspect the repo and update the work docs listed when needed.
21
- - If you update docs, edit the files directly. If no changes are needed, do not edit files.
22
- - Respond with a short summary of what you did or found.
23
-
24
- Work docs (paths):
25
- - TODO: {todo_path}
26
- - PROGRESS: {progress_path}
27
- - OPINIONS: {opinions_path}
28
- - SPEC: {spec_path}
29
- - SUMMARY: {summary_path}
30
-
31
- {user_viewing_block}
32
-
33
- User request:
34
- {message}
35
-
36
- {docs_context_block}
37
- {recent_summary_block}
38
- """
39
-
40
-
41
- SPEC_INGEST_APP_SERVER_TEMPLATE = """You are preparing work docs (TODO/PROGRESS/OPINIONS) from the SPEC.
42
-
43
- SPEC path: {spec_path}
44
- TODO path: {todo_path}
45
- PROGRESS path: {progress_path}
46
- OPINIONS path: {opinions_path}
47
-
48
- Instructions:
49
- - Read the SPEC and existing docs from disk.
50
- - Edit the TODO, PROGRESS, and OPINIONS files directly to reflect the SPEC.
51
- - The TODO must be a Markdown checklist. Every task MUST be a checkbox line:
52
- - Use `- [ ] <task>` for open items and `- [x] <task>` for completed items.
53
- - Do NOT use plain bullets like `- task` or paragraphs for tasks.
54
- - Do NOT output a patch block. Just edit the files.
55
- - Output a short summary prefixed with "Agent: " explaining what you did.
56
-
57
- User request:
58
- {message}
59
-
60
- {spec_excerpt_block}
61
- """
62
-
63
-
64
- SNAPSHOT_APP_SERVER_TEMPLATE = """You are generating a compact Markdown repo snapshot meant to be pasted into another LLM chat.
65
-
66
- Snapshot path: {snapshot_path}
67
-
68
- Instructions:
69
- - Analyze the provided context and the repository.
70
- - Write the snapshot content directly to the snapshot path.
71
- - Keep the file concise and high-signal.
72
-
73
- Required output format (keep headings exactly):
74
- # Repo Snapshot
75
-
76
- ## What this repo is
77
- - 3–6 bullets.
78
-
79
- ## Architecture overview
80
- - Components and responsibilities.
81
- - Data/control flow (high level).
82
- - How things actually work
83
-
84
- ## Key files and modules
85
- - Bullet list of important paths with 1-line notes.
86
-
87
- ## Extension points and sharp edges
88
- - Config/state/concurrency hazards, limits, sharp edges.
89
-
90
- Inputs:
91
- {seed_context}
92
-
93
- {changes_block}
94
- {previous_snapshot_block}
95
- """
96
-
97
-
98
11
  AUTORUNNER_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant operating on a git repository.
99
12
 
100
- Work docs (read from disk as needed):
101
- - TODO: {todo_path}
102
- - PROGRESS: {progress_path}
103
- - OPINIONS: {opinions_path}
104
- - SPEC: {spec_path}
105
- - SUMMARY: {summary_path}
13
+ Workspace docs (optional; read from disk when useful):
14
+ - Active context: {active_context_path}
15
+ - Decisions: {decisions_path}
16
+ - Spec: {spec_path}
17
+
18
+ Tickets:
19
+ - The authoritative work items are ticket files under `.codex-autorunner/tickets/`.
20
+ - Pick the next not-done ticket, implement it, and update the ticket file (`done: true`) when complete.
106
21
 
107
22
  Instructions:
108
- - Work through TODO items from top to bottom.
109
- - Prefer fixing issues over documenting them.
110
- - Keep TODO/PROGRESS/OPINIONS/SPEC/SUMMARY in sync.
111
- - Make actual edits in the repo as needed.
23
+ - This run is non-interactive. Do not ask the user questions. If unsure, make reasonable assumptions and proceed.
24
+ - Prefer small, safe diffs and keep work focused on the current ticket.
25
+ - You may create new tickets only when needed to break down the current work.
112
26
 
113
27
  User request:
114
28
  {message}
115
29
 
116
- {todo_excerpt_block}
30
+ {workspace_spec_block}
117
31
  {prev_run_block}
118
32
  """
119
33
 
@@ -167,144 +81,6 @@ def _shrink_prompt(
167
81
  return prompt
168
82
 
169
83
 
170
- def build_doc_chat_prompt(
171
- config: Config,
172
- *,
173
- message: str,
174
- recent_summary: Optional[str],
175
- docs: dict[str, dict[str, str]],
176
- context_doc: Optional[str] = None,
177
- ) -> str:
178
- prompt_cfg: AppServerDocChatPromptConfig = config.app_server.prompts.doc_chat
179
- doc_paths = {
180
- "todo": _display_path(config.root, config.doc_path("todo")),
181
- "progress": _display_path(config.root, config.doc_path("progress")),
182
- "opinions": _display_path(config.root, config.doc_path("opinions")),
183
- "spec": _display_path(config.root, config.doc_path("spec")),
184
- "summary": _display_path(config.root, config.doc_path("summary")),
185
- }
186
- message_text = truncate_text(message, prompt_cfg.message_max_chars)
187
- doc_blocks = []
188
- for key, path in doc_paths.items():
189
- payload = docs.get(key, {})
190
- source = payload.get("source") or "disk"
191
- content = truncate_text(
192
- str(payload.get("content") or ""), prompt_cfg.target_excerpt_max_chars
193
- )
194
- if not content.strip():
195
- content = "(empty)"
196
- label = f"{key.upper()} [{path}] ({source.upper()})"
197
- doc_blocks.append(f"{label}\n{content}")
198
- docs_context = "\n\n".join(doc_blocks)
199
- recent_text = truncate_text(
200
- recent_summary or "", prompt_cfg.recent_summary_max_chars
201
- )
202
- user_viewing = ""
203
- if context_doc:
204
- user_viewing = f"The user is currently looking at {context_doc.upper()}."
205
-
206
- sections = {
207
- "message": message_text,
208
- "docs_context": docs_context,
209
- "recent_summary": recent_text,
210
- "user_viewing": user_viewing,
211
- }
212
-
213
- def render() -> str:
214
- return DOC_CHAT_APP_SERVER_TEMPLATE.format(
215
- todo_path=doc_paths["todo"],
216
- progress_path=doc_paths["progress"],
217
- opinions_path=doc_paths["opinions"],
218
- spec_path=doc_paths["spec"],
219
- summary_path=doc_paths["summary"],
220
- message=sections["message"],
221
- user_viewing_block=_optional_block(
222
- "USER_VIEWING", sections["user_viewing"]
223
- ),
224
- docs_context_block=_optional_block("DOC_BASES", sections["docs_context"]),
225
- recent_summary_block=_optional_block(
226
- "RECENT_RUN_SUMMARY", sections["recent_summary"]
227
- ),
228
- )
229
-
230
- return _shrink_prompt(
231
- max_chars=prompt_cfg.max_chars,
232
- render=render,
233
- sections=sections,
234
- order=["recent_summary", "docs_context", "message"],
235
- )
236
-
237
-
238
- def build_spec_ingest_prompt(
239
- config: Config,
240
- *,
241
- message: str,
242
- spec_path: Optional[Path] = None,
243
- ) -> str:
244
- prompt_cfg: AppServerSpecIngestPromptConfig = config.app_server.prompts.spec_ingest
245
- doc_paths = {
246
- "todo": _display_path(config.root, config.doc_path("todo")),
247
- "progress": _display_path(config.root, config.doc_path("progress")),
248
- "opinions": _display_path(config.root, config.doc_path("opinions")),
249
- }
250
- spec_target = spec_path or config.doc_path("spec")
251
- spec_path_str = _display_path(config.root, spec_target)
252
- message_text = truncate_text(message, prompt_cfg.message_max_chars)
253
- spec_excerpt = truncate_text(
254
- spec_target.read_text(encoding="utf-8"),
255
- prompt_cfg.spec_excerpt_max_chars,
256
- )
257
-
258
- sections = {
259
- "message": message_text,
260
- "spec_excerpt": spec_excerpt,
261
- }
262
-
263
- def render() -> str:
264
- return SPEC_INGEST_APP_SERVER_TEMPLATE.format(
265
- spec_path=spec_path_str,
266
- todo_path=doc_paths["todo"],
267
- progress_path=doc_paths["progress"],
268
- opinions_path=doc_paths["opinions"],
269
- message=sections["message"],
270
- spec_excerpt_block=_optional_block(
271
- "SPEC_EXCERPT", sections["spec_excerpt"]
272
- ),
273
- )
274
-
275
- return _shrink_prompt(
276
- max_chars=prompt_cfg.max_chars,
277
- render=render,
278
- sections=sections,
279
- order=["spec_excerpt", "message"],
280
- )
281
-
282
-
283
- def build_app_server_snapshot_prompt(
284
- config: Config,
285
- *,
286
- seed_context: str,
287
- previous_snapshot: Optional[str] = None,
288
- changes: Optional[str] = None,
289
- ) -> str:
290
- snapshot_path = config.doc_path("snapshot")
291
- previous_block = ""
292
- if previous_snapshot:
293
- previous_block = (
294
- f"<PREVIOUS_SNAPSHOT>\n{previous_snapshot.strip()}\n</PREVIOUS_SNAPSHOT>"
295
- )
296
- changes_block = ""
297
- if changes:
298
- changes_block = f"<CHANGES_SINCE_LAST_SNAPSHOT>\n{changes.strip()}\n</CHANGES_SINCE_LAST_SNAPSHOT>"
299
-
300
- return SNAPSHOT_APP_SERVER_TEMPLATE.format(
301
- snapshot_path=snapshot_path,
302
- seed_context=seed_context,
303
- changes_block=changes_block,
304
- previous_snapshot_block=previous_block,
305
- )
306
-
307
-
308
84
  def build_autorunner_prompt(
309
85
  config: Config,
310
86
  *,
@@ -313,35 +89,36 @@ def build_autorunner_prompt(
313
89
  ) -> str:
314
90
  prompt_cfg: AppServerAutorunnerPromptConfig = config.app_server.prompts.autorunner
315
91
  doc_paths = {
316
- "todo": _display_path(config.root, config.doc_path("todo")),
317
- "progress": _display_path(config.root, config.doc_path("progress")),
318
- "opinions": _display_path(config.root, config.doc_path("opinions")),
92
+ "active_context": _display_path(config.root, config.doc_path("active_context")),
93
+ "decisions": _display_path(config.root, config.doc_path("decisions")),
319
94
  "spec": _display_path(config.root, config.doc_path("spec")),
320
- "summary": _display_path(config.root, config.doc_path("summary")),
321
95
  }
96
+
322
97
  message_text = truncate_text(message, prompt_cfg.message_max_chars)
323
- todo_excerpt = truncate_text(
324
- config.doc_path("todo").read_text(encoding="utf-8"),
98
+ spec_excerpt = truncate_text(
99
+ (
100
+ config.doc_path("spec").read_text(encoding="utf-8")
101
+ if config.doc_path("spec").exists()
102
+ else ""
103
+ ),
325
104
  prompt_cfg.todo_excerpt_max_chars,
326
105
  )
327
106
  prev_run_text = truncate_text(prev_run_summary or "", prompt_cfg.prev_run_max_chars)
328
107
 
329
108
  sections = {
330
109
  "message": message_text,
331
- "todo_excerpt": todo_excerpt,
110
+ "workspace_spec": spec_excerpt,
332
111
  "prev_run": prev_run_text,
333
112
  }
334
113
 
335
114
  def render() -> str:
336
115
  return AUTORUNNER_APP_SERVER_TEMPLATE.format(
337
- todo_path=doc_paths["todo"],
338
- progress_path=doc_paths["progress"],
339
- opinions_path=doc_paths["opinions"],
116
+ active_context_path=doc_paths["active_context"],
117
+ decisions_path=doc_paths["decisions"],
340
118
  spec_path=doc_paths["spec"],
341
- summary_path=doc_paths["summary"],
342
119
  message=sections["message"],
343
- todo_excerpt_block=_optional_block(
344
- "TODO_EXCERPT", sections["todo_excerpt"]
120
+ workspace_spec_block=_optional_block(
121
+ "WORKSPACE_SPEC", sections["workspace_spec"]
345
122
  ),
346
123
  prev_run_block=_optional_block("PREV_RUN_SUMMARY", sections["prev_run"]),
347
124
  )
@@ -350,13 +127,11 @@ def build_autorunner_prompt(
350
127
  max_chars=prompt_cfg.max_chars,
351
128
  render=render,
352
129
  sections=sections,
353
- order=["prev_run", "todo_excerpt", "message"],
130
+ order=["prev_run", "workspace_spec", "message"],
354
131
  )
355
132
 
356
133
 
357
134
  APP_SERVER_PROMPT_BUILDERS = {
358
- "doc_chat": build_doc_chat_prompt,
359
- "spec_ingest": build_spec_ingest_prompt,
360
135
  "autorunner": build_autorunner_prompt,
361
136
  }
362
137
 
@@ -364,13 +139,7 @@ APP_SERVER_PROMPT_BUILDERS = {
364
139
  __all__ = [
365
140
  "AUTORUNNER_APP_SERVER_TEMPLATE",
366
141
  "APP_SERVER_PROMPT_BUILDERS",
367
- "DOC_CHAT_APP_SERVER_TEMPLATE",
368
- "SPEC_INGEST_APP_SERVER_TEMPLATE",
369
- "SNAPSHOT_APP_SERVER_TEMPLATE",
370
142
  "TRUNCATION_MARKER",
371
143
  "build_autorunner_prompt",
372
- "build_doc_chat_prompt",
373
- "build_spec_ingest_prompt",
374
- "build_app_server_snapshot_prompt",
375
144
  "truncate_text",
376
145
  ]
@@ -12,19 +12,15 @@ APP_SERVER_THREADS_FILENAME = ".codex-autorunner/app_server_threads.json"
12
12
  APP_SERVER_THREADS_VERSION = 1
13
13
  APP_SERVER_THREADS_CORRUPT_SUFFIX = ".corrupt"
14
14
  APP_SERVER_THREADS_NOTICE_SUFFIX = ".corrupt.json"
15
- DOC_CHAT_KINDS = ("todo", "progress", "opinions", "spec", "summary")
16
- DOC_CHAT_PREFIX = "doc_chat."
17
- DOC_CHAT_KEY = "doc_chat"
18
- DOC_CHAT_OPENCODE_KEY = "doc_chat.opencode"
19
- DOC_CHAT_OPENCODE_PREFIX = "doc_chat.opencode."
20
- DOC_CHAT_KEYS = {DOC_CHAT_KEY} | {f"{DOC_CHAT_PREFIX}{kind}" for kind in DOC_CHAT_KINDS}
21
- DOC_CHAT_KEYS = DOC_CHAT_KEYS | {
22
- DOC_CHAT_OPENCODE_KEY,
23
- *(f"{DOC_CHAT_OPENCODE_PREFIX}{kind}" for kind in DOC_CHAT_KINDS),
24
- }
25
- FEATURE_KEYS = DOC_CHAT_KEYS | {
26
- "spec_ingest",
27
- "spec_ingest.opencode",
15
+ FILE_CHAT_KEY = "file_chat"
16
+ FILE_CHAT_OPENCODE_KEY = "file_chat.opencode"
17
+ FILE_CHAT_PREFIX = "file_chat."
18
+ FILE_CHAT_OPENCODE_PREFIX = "file_chat.opencode."
19
+
20
+ # Static keys that can be reset/managed via the UI.
21
+ FEATURE_KEYS = {
22
+ FILE_CHAT_KEY,
23
+ FILE_CHAT_OPENCODE_KEY,
28
24
  "autorunner",
29
25
  "autorunner.opencode",
30
26
  }
@@ -43,6 +39,10 @@ def normalize_feature_key(raw: str) -> str:
43
39
  key = key.replace("/", ".").replace(":", ".")
44
40
  if key in FEATURE_KEYS:
45
41
  return key
42
+ # Allow per-target file chat threads (e.g. file_chat.ticket.1, file_chat.workspace.spec).
43
+ for prefix in (FILE_CHAT_PREFIX, FILE_CHAT_OPENCODE_PREFIX):
44
+ if key.startswith(prefix) and len(key) > len(prefix):
45
+ return key
46
46
  raise ValueError(f"invalid feature key: {raw}")
47
47
 
48
48
 
@@ -84,20 +84,9 @@ class AppServerThreadRegistry:
84
84
 
85
85
  def feature_map(self) -> dict[str, object]:
86
86
  threads = self.load()
87
- doc_chat_thread = threads.get(DOC_CHAT_KEY)
88
- doc_chat_opencode_thread = threads.get(DOC_CHAT_OPENCODE_KEY)
89
87
  payload: dict[str, object] = {
90
- "doc_chat": {
91
- kind: doc_chat_thread or threads.get(f"{DOC_CHAT_PREFIX}{kind}")
92
- for kind in DOC_CHAT_KINDS
93
- },
94
- "doc_chat_opencode": {
95
- kind: doc_chat_opencode_thread
96
- or threads.get(f"{DOC_CHAT_OPENCODE_PREFIX}{kind}")
97
- for kind in DOC_CHAT_KINDS
98
- },
99
- "spec_ingest": threads.get("spec_ingest"),
100
- "spec_ingest_opencode": threads.get("spec_ingest.opencode"),
88
+ "file_chat": threads.get(FILE_CHAT_KEY),
89
+ "file_chat_opencode": threads.get(FILE_CHAT_OPENCODE_KEY),
101
90
  "autorunner": threads.get("autorunner"),
102
91
  "autorunner_opencode": threads.get("autorunner.opencode"),
103
92
  }
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from contextlib import asynccontextmanager
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timedelta
8
+ from enum import Enum
9
+ from typing import AsyncIterator, Optional
10
+
11
+ from .exceptions import CircuitOpenError
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CircuitState(str, Enum):
17
+ CLOSED = "closed"
18
+ OPEN = "open"
19
+ HALF_OPEN = "half_open"
20
+
21
+
22
+ @dataclass
23
+ class CircuitBreakerConfig:
24
+ """Configuration for circuit breaker behavior."""
25
+
26
+ failure_threshold: int = 5
27
+ """Number of failures before opening circuit."""
28
+
29
+ timeout_seconds: int = 60
30
+ """Seconds to wait before attempting recovery (half-open state)."""
31
+
32
+ half_open_attempts: int = 1
33
+ """Number of successful calls needed to close circuit from half-open state."""
34
+
35
+
36
+ @dataclass
37
+ class CircuitBreakerState:
38
+ """Internal state tracking for circuit breaker."""
39
+
40
+ failure_count: int = 0
41
+ state: CircuitState = CircuitState.CLOSED
42
+ last_failure_time: Optional[datetime] = None
43
+ success_count: int = 0
44
+
45
+
46
+ class CircuitBreaker:
47
+ """
48
+ Circuit breaker pattern implementation for external service resilience.
49
+
50
+ Opens after N consecutive failures, closes after success or timeout.
51
+ Prevents cascading failures and provides fast-fail for degraded services.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ service_name: str,
57
+ *,
58
+ config: Optional[CircuitBreakerConfig] = None,
59
+ logger: Optional[logging.Logger] = None,
60
+ ) -> None:
61
+ self._service_name = service_name
62
+ self._config = config or CircuitBreakerConfig()
63
+ self._logger = logger or logging.getLogger(__name__)
64
+ self._state = CircuitBreakerState()
65
+ self._lock = asyncio.Lock()
66
+
67
+ @asynccontextmanager
68
+ async def call(self) -> AsyncIterator[None]:
69
+ """
70
+ Context manager that raises CircuitOpenError if circuit is open.
71
+
72
+ Tracks failures/successes to manage circuit state transitions.
73
+
74
+ Raises:
75
+ CircuitOpenError: If circuit is open.
76
+
77
+ Example:
78
+ async with circuit_breaker.call():
79
+ result = await external_service.request()
80
+ """
81
+ is_open = False
82
+ async with self._lock:
83
+ if self._should_open_circuit():
84
+ self._open_circuit()
85
+ is_open = True
86
+ elif self._should_half_open_circuit():
87
+ self._half_open_circuit()
88
+ elif self._state.state == CircuitState.OPEN:
89
+ is_open = True
90
+
91
+ if is_open:
92
+ raise CircuitOpenError(
93
+ self._service_name,
94
+ message=f"Circuit breaker OPEN for {self._service_name}. "
95
+ f"Last failure: {self._state.last_failure_time}",
96
+ )
97
+
98
+ try:
99
+ yield
100
+ except Exception as exc:
101
+ self._logger.debug(
102
+ "Exception caught by circuit breaker for %s: %s",
103
+ self._service_name,
104
+ exc,
105
+ )
106
+ await self._record_failure()
107
+ raise
108
+ else:
109
+ await self._record_success()
110
+
111
+ def _should_open_circuit(self) -> bool:
112
+ """Check if circuit should open based on failure count."""
113
+ return (
114
+ self._state.state == CircuitState.CLOSED
115
+ and self._state.failure_count >= self._config.failure_threshold
116
+ )
117
+
118
+ def _should_half_open_circuit(self) -> bool:
119
+ """Check if circuit should transition to half-open state."""
120
+ if self._state.state != CircuitState.OPEN:
121
+ return False
122
+ if self._state.last_failure_time is None:
123
+ return False
124
+ return datetime.utcnow() >= self._state.last_failure_time + timedelta(
125
+ seconds=self._config.timeout_seconds
126
+ )
127
+
128
+ def _open_circuit(self) -> None:
129
+ """Open circuit and log the transition."""
130
+ self._state.state = CircuitState.OPEN
131
+ self._state.last_failure_time = datetime.utcnow()
132
+ self._logger.warning(
133
+ "Circuit breaker OPEN for %s after %d failures",
134
+ self._service_name,
135
+ self._state.failure_count,
136
+ )
137
+
138
+ def _half_open_circuit(self) -> None:
139
+ """Transition to half-open state and allow one test call."""
140
+ self._state.state = CircuitState.HALF_OPEN
141
+ self._state.success_count = 0
142
+ self._logger.info(
143
+ "Circuit breaker HALF_OPEN for %s (testing recovery)",
144
+ self._service_name,
145
+ )
146
+
147
+ async def _record_failure(self) -> None:
148
+ """Record a failure and update state accordingly."""
149
+ async with self._lock:
150
+ self._state.failure_count += 1
151
+ self._state.last_failure_time = datetime.utcnow()
152
+
153
+ if self._state.state == CircuitState.HALF_OPEN:
154
+ self._state.state = CircuitState.OPEN
155
+ self._logger.warning(
156
+ "Circuit breaker OPEN for %s (failure in half-open state)",
157
+ self._service_name,
158
+ )
159
+ elif self._state.state == CircuitState.CLOSED:
160
+ self._logger.debug(
161
+ "Circuit breaker recorded failure for %s (count: %d/%d)",
162
+ self._service_name,
163
+ self._state.failure_count,
164
+ self._config.failure_threshold,
165
+ )
166
+
167
+ async def _record_success(self) -> None:
168
+ """Record a success and update state accordingly."""
169
+ async with self._lock:
170
+ if self._state.state == CircuitState.CLOSED:
171
+ self._state.failure_count = 0
172
+ elif self._state.state == CircuitState.HALF_OPEN:
173
+ self._state.success_count += 1
174
+ if self._state.success_count >= self._config.half_open_attempts:
175
+ self._state.state = CircuitState.CLOSED
176
+ self._state.failure_count = 0
177
+ self._state.success_count = 0
178
+ self._logger.info(
179
+ "Circuit breaker CLOSED for %s (recovery successful)",
180
+ self._service_name,
181
+ )
182
+ else:
183
+ pass
@@ -1,3 +1,9 @@
1
+ # DEPRECATED: This module implements a Codex CLI subprocess runner.
2
+ # The primary execution path now uses the Codex app-server via OpenCode runtime.
3
+ # This file is kept for potential future CLI-as-backend support but is currently
4
+ # not referenced by the main engine. See src/codex_autorunner/core/engine.py for
5
+ # the current execution path (_run_codex_app_server_async).
6
+
1
7
  import asyncio
2
8
  import subprocess
3
9
  from pathlib import Path