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
@@ -3,119 +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
- - This run is non-interactive. Do not ask the user questions. If unsure, make reasonable assumptions and proceed.
20
- - Use the base doc content below. Drafts (if present) are the authoritative base.
21
- - You may inspect the repo and update the work docs listed when needed.
22
- - If you update docs, edit the files directly. If no changes are needed, do not edit files.
23
- - Respond with a short summary of what you did or found.
24
-
25
- Work docs (paths):
26
- - TODO: {todo_path}
27
- - PROGRESS: {progress_path}
28
- - OPINIONS: {opinions_path}
29
- - SPEC: {spec_path}
30
- - SUMMARY: {summary_path}
31
-
32
- {user_viewing_block}
33
-
34
- User request:
35
- {message}
36
-
37
- {docs_context_block}
38
- {recent_summary_block}
39
- """
40
-
41
-
42
- SPEC_INGEST_APP_SERVER_TEMPLATE = """You are preparing work docs (TODO/PROGRESS/OPINIONS) from the SPEC.
43
-
44
- SPEC path: {spec_path}
45
- TODO path: {todo_path}
46
- PROGRESS path: {progress_path}
47
- OPINIONS path: {opinions_path}
48
-
49
- Instructions:
50
- - Read the SPEC and existing docs from disk.
51
- - Edit the TODO, PROGRESS, and OPINIONS files directly to reflect the SPEC.
52
- - The TODO must be a Markdown checklist. Every task MUST be a checkbox line:
53
- - Use `- [ ] <task>` for open items and `- [x] <task>` for completed items.
54
- - Do NOT use plain bullets like `- task` or paragraphs for tasks.
55
- - Do NOT output a patch block. Just edit the files.
56
- - Output a short summary prefixed with "Agent: " explaining what you did.
57
-
58
- User request:
59
- {message}
60
-
61
- {spec_excerpt_block}
62
- """
63
-
64
-
65
- SNAPSHOT_APP_SERVER_TEMPLATE = """You are generating a compact Markdown repo snapshot meant to be pasted into another LLM chat.
66
-
67
- Snapshot path: {snapshot_path}
68
-
69
- Instructions:
70
- - Analyze the provided context and the repository.
71
- - Write the snapshot content directly to the snapshot path.
72
- - Keep the file concise and high-signal.
73
-
74
- Required output format (keep headings exactly):
75
- # Repo Snapshot
76
-
77
- ## What this repo is
78
- - 3–6 bullets.
79
-
80
- ## Architecture overview
81
- - Components and responsibilities.
82
- - Data/control flow (high level).
83
- - How things actually work
84
-
85
- ## Key files and modules
86
- - Bullet list of important paths with 1-line notes.
87
-
88
- ## Extension points and sharp edges
89
- - Config/state/concurrency hazards, limits, sharp edges.
90
-
91
- Inputs:
92
- {seed_context}
93
-
94
- {changes_block}
95
- {previous_snapshot_block}
96
- """
97
-
98
-
99
11
  AUTORUNNER_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant operating on a git repository.
100
12
 
101
- Work docs (read from disk as needed):
102
- - TODO: {todo_path}
103
- - PROGRESS: {progress_path}
104
- - OPINIONS: {opinions_path}
105
- - SPEC: {spec_path}
106
- - 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.
107
21
 
108
22
  Instructions:
109
23
  - This run is non-interactive. Do not ask the user questions. If unsure, make reasonable assumptions and proceed.
110
- - Work through TODO items from top to bottom.
111
- - Prefer fixing issues over documenting them.
112
- - Keep TODO/PROGRESS/OPINIONS/SPEC/SUMMARY in sync.
113
- - Make actual edits in the repo as needed.
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.
114
26
 
115
27
  User request:
116
28
  {message}
117
29
 
118
- {todo_excerpt_block}
30
+ {workspace_spec_block}
119
31
  {prev_run_block}
120
32
  """
121
33
 
@@ -169,144 +81,6 @@ def _shrink_prompt(
169
81
  return prompt
170
82
 
171
83
 
172
- def build_doc_chat_prompt(
173
- config: Config,
174
- *,
175
- message: str,
176
- recent_summary: Optional[str],
177
- docs: dict[str, dict[str, str]],
178
- context_doc: Optional[str] = None,
179
- ) -> str:
180
- prompt_cfg: AppServerDocChatPromptConfig = config.app_server.prompts.doc_chat
181
- doc_paths = {
182
- "todo": _display_path(config.root, config.doc_path("todo")),
183
- "progress": _display_path(config.root, config.doc_path("progress")),
184
- "opinions": _display_path(config.root, config.doc_path("opinions")),
185
- "spec": _display_path(config.root, config.doc_path("spec")),
186
- "summary": _display_path(config.root, config.doc_path("summary")),
187
- }
188
- message_text = truncate_text(message, prompt_cfg.message_max_chars)
189
- doc_blocks = []
190
- for key, path in doc_paths.items():
191
- payload = docs.get(key, {})
192
- source = payload.get("source") or "disk"
193
- content = truncate_text(
194
- str(payload.get("content") or ""), prompt_cfg.target_excerpt_max_chars
195
- )
196
- if not content.strip():
197
- content = "(empty)"
198
- label = f"{key.upper()} [{path}] ({source.upper()})"
199
- doc_blocks.append(f"{label}\n{content}")
200
- docs_context = "\n\n".join(doc_blocks)
201
- recent_text = truncate_text(
202
- recent_summary or "", prompt_cfg.recent_summary_max_chars
203
- )
204
- user_viewing = ""
205
- if context_doc:
206
- user_viewing = f"The user is currently looking at {context_doc.upper()}."
207
-
208
- sections = {
209
- "message": message_text,
210
- "docs_context": docs_context,
211
- "recent_summary": recent_text,
212
- "user_viewing": user_viewing,
213
- }
214
-
215
- def render() -> str:
216
- return DOC_CHAT_APP_SERVER_TEMPLATE.format(
217
- todo_path=doc_paths["todo"],
218
- progress_path=doc_paths["progress"],
219
- opinions_path=doc_paths["opinions"],
220
- spec_path=doc_paths["spec"],
221
- summary_path=doc_paths["summary"],
222
- message=sections["message"],
223
- user_viewing_block=_optional_block(
224
- "USER_VIEWING", sections["user_viewing"]
225
- ),
226
- docs_context_block=_optional_block("DOC_BASES", sections["docs_context"]),
227
- recent_summary_block=_optional_block(
228
- "RECENT_RUN_SUMMARY", sections["recent_summary"]
229
- ),
230
- )
231
-
232
- return _shrink_prompt(
233
- max_chars=prompt_cfg.max_chars,
234
- render=render,
235
- sections=sections,
236
- order=["recent_summary", "docs_context", "message"],
237
- )
238
-
239
-
240
- def build_spec_ingest_prompt(
241
- config: Config,
242
- *,
243
- message: str,
244
- spec_path: Optional[Path] = None,
245
- ) -> str:
246
- prompt_cfg: AppServerSpecIngestPromptConfig = config.app_server.prompts.spec_ingest
247
- doc_paths = {
248
- "todo": _display_path(config.root, config.doc_path("todo")),
249
- "progress": _display_path(config.root, config.doc_path("progress")),
250
- "opinions": _display_path(config.root, config.doc_path("opinions")),
251
- }
252
- spec_target = spec_path or config.doc_path("spec")
253
- spec_path_str = _display_path(config.root, spec_target)
254
- message_text = truncate_text(message, prompt_cfg.message_max_chars)
255
- spec_excerpt = truncate_text(
256
- spec_target.read_text(encoding="utf-8"),
257
- prompt_cfg.spec_excerpt_max_chars,
258
- )
259
-
260
- sections = {
261
- "message": message_text,
262
- "spec_excerpt": spec_excerpt,
263
- }
264
-
265
- def render() -> str:
266
- return SPEC_INGEST_APP_SERVER_TEMPLATE.format(
267
- spec_path=spec_path_str,
268
- todo_path=doc_paths["todo"],
269
- progress_path=doc_paths["progress"],
270
- opinions_path=doc_paths["opinions"],
271
- message=sections["message"],
272
- spec_excerpt_block=_optional_block(
273
- "SPEC_EXCERPT", sections["spec_excerpt"]
274
- ),
275
- )
276
-
277
- return _shrink_prompt(
278
- max_chars=prompt_cfg.max_chars,
279
- render=render,
280
- sections=sections,
281
- order=["spec_excerpt", "message"],
282
- )
283
-
284
-
285
- def build_app_server_snapshot_prompt(
286
- config: Config,
287
- *,
288
- seed_context: str,
289
- previous_snapshot: Optional[str] = None,
290
- changes: Optional[str] = None,
291
- ) -> str:
292
- snapshot_path = config.doc_path("snapshot")
293
- previous_block = ""
294
- if previous_snapshot:
295
- previous_block = (
296
- f"<PREVIOUS_SNAPSHOT>\n{previous_snapshot.strip()}\n</PREVIOUS_SNAPSHOT>"
297
- )
298
- changes_block = ""
299
- if changes:
300
- changes_block = f"<CHANGES_SINCE_LAST_SNAPSHOT>\n{changes.strip()}\n</CHANGES_SINCE_LAST_SNAPSHOT>"
301
-
302
- return SNAPSHOT_APP_SERVER_TEMPLATE.format(
303
- snapshot_path=snapshot_path,
304
- seed_context=seed_context,
305
- changes_block=changes_block,
306
- previous_snapshot_block=previous_block,
307
- )
308
-
309
-
310
84
  def build_autorunner_prompt(
311
85
  config: Config,
312
86
  *,
@@ -315,35 +89,36 @@ def build_autorunner_prompt(
315
89
  ) -> str:
316
90
  prompt_cfg: AppServerAutorunnerPromptConfig = config.app_server.prompts.autorunner
317
91
  doc_paths = {
318
- "todo": _display_path(config.root, config.doc_path("todo")),
319
- "progress": _display_path(config.root, config.doc_path("progress")),
320
- "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")),
321
94
  "spec": _display_path(config.root, config.doc_path("spec")),
322
- "summary": _display_path(config.root, config.doc_path("summary")),
323
95
  }
96
+
324
97
  message_text = truncate_text(message, prompt_cfg.message_max_chars)
325
- todo_excerpt = truncate_text(
326
- 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
+ ),
327
104
  prompt_cfg.todo_excerpt_max_chars,
328
105
  )
329
106
  prev_run_text = truncate_text(prev_run_summary or "", prompt_cfg.prev_run_max_chars)
330
107
 
331
108
  sections = {
332
109
  "message": message_text,
333
- "todo_excerpt": todo_excerpt,
110
+ "workspace_spec": spec_excerpt,
334
111
  "prev_run": prev_run_text,
335
112
  }
336
113
 
337
114
  def render() -> str:
338
115
  return AUTORUNNER_APP_SERVER_TEMPLATE.format(
339
- todo_path=doc_paths["todo"],
340
- progress_path=doc_paths["progress"],
341
- opinions_path=doc_paths["opinions"],
116
+ active_context_path=doc_paths["active_context"],
117
+ decisions_path=doc_paths["decisions"],
342
118
  spec_path=doc_paths["spec"],
343
- summary_path=doc_paths["summary"],
344
119
  message=sections["message"],
345
- todo_excerpt_block=_optional_block(
346
- "TODO_EXCERPT", sections["todo_excerpt"]
120
+ workspace_spec_block=_optional_block(
121
+ "WORKSPACE_SPEC", sections["workspace_spec"]
347
122
  ),
348
123
  prev_run_block=_optional_block("PREV_RUN_SUMMARY", sections["prev_run"]),
349
124
  )
@@ -352,13 +127,11 @@ def build_autorunner_prompt(
352
127
  max_chars=prompt_cfg.max_chars,
353
128
  render=render,
354
129
  sections=sections,
355
- order=["prev_run", "todo_excerpt", "message"],
130
+ order=["prev_run", "workspace_spec", "message"],
356
131
  )
357
132
 
358
133
 
359
134
  APP_SERVER_PROMPT_BUILDERS = {
360
- "doc_chat": build_doc_chat_prompt,
361
- "spec_ingest": build_spec_ingest_prompt,
362
135
  "autorunner": build_autorunner_prompt,
363
136
  }
364
137
 
@@ -366,13 +139,7 @@ APP_SERVER_PROMPT_BUILDERS = {
366
139
  __all__ = [
367
140
  "AUTORUNNER_APP_SERVER_TEMPLATE",
368
141
  "APP_SERVER_PROMPT_BUILDERS",
369
- "DOC_CHAT_APP_SERVER_TEMPLATE",
370
- "SPEC_INGEST_APP_SERVER_TEMPLATE",
371
- "SNAPSHOT_APP_SERVER_TEMPLATE",
372
142
  "TRUNCATION_MARKER",
373
143
  "build_autorunner_prompt",
374
- "build_doc_chat_prompt",
375
- "build_spec_ingest_prompt",
376
- "build_app_server_snapshot_prompt",
377
144
  "truncate_text",
378
145
  ]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import logging
4
5
  from datetime import datetime, timezone
5
6
  from pathlib import Path
6
7
  from typing import Optional
@@ -12,19 +13,17 @@ APP_SERVER_THREADS_FILENAME = ".codex-autorunner/app_server_threads.json"
12
13
  APP_SERVER_THREADS_VERSION = 1
13
14
  APP_SERVER_THREADS_CORRUPT_SUFFIX = ".corrupt"
14
15
  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",
16
+ FILE_CHAT_KEY = "file_chat"
17
+ FILE_CHAT_OPENCODE_KEY = "file_chat.opencode"
18
+ FILE_CHAT_PREFIX = "file_chat."
19
+ FILE_CHAT_OPENCODE_PREFIX = "file_chat.opencode."
20
+
21
+ LOGGER = logging.getLogger("codex_autorunner.app_server")
22
+
23
+ # Static keys that can be reset/managed via the UI.
24
+ FEATURE_KEYS = {
25
+ FILE_CHAT_KEY,
26
+ FILE_CHAT_OPENCODE_KEY,
28
27
  "autorunner",
29
28
  "autorunner.opencode",
30
29
  }
@@ -43,6 +42,10 @@ def normalize_feature_key(raw: str) -> str:
43
42
  key = key.replace("/", ".").replace(":", ".")
44
43
  if key in FEATURE_KEYS:
45
44
  return key
45
+ # Allow per-target file chat threads (e.g. file_chat.ticket.1, file_chat.workspace.spec).
46
+ for prefix in (FILE_CHAT_PREFIX, FILE_CHAT_OPENCODE_PREFIX):
47
+ if key.startswith(prefix) and len(key) > len(prefix):
48
+ return key
46
49
  raise ValueError(f"invalid feature key: {raw}")
47
50
 
48
51
 
@@ -84,20 +87,9 @@ class AppServerThreadRegistry:
84
87
 
85
88
  def feature_map(self) -> dict[str, object]:
86
89
  threads = self.load()
87
- doc_chat_thread = threads.get(DOC_CHAT_KEY)
88
- doc_chat_opencode_thread = threads.get(DOC_CHAT_OPENCODE_KEY)
89
90
  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"),
91
+ "file_chat": threads.get(FILE_CHAT_KEY),
92
+ "file_chat_opencode": threads.get(FILE_CHAT_OPENCODE_KEY),
101
93
  "autorunner": threads.get("autorunner"),
102
94
  "autorunner_opencode": threads.get("autorunner.opencode"),
103
95
  }
@@ -188,8 +180,14 @@ class AppServerThreadRegistry:
188
180
  try:
189
181
  atomic_write(self._notice_path(), json.dumps(notice, indent=2) + "\n")
190
182
  except Exception:
191
- pass
183
+ LOGGER.warning(
184
+ "Failed to write app server thread corruption notice.",
185
+ exc_info=True,
186
+ )
192
187
  try:
193
188
  self._save_unlocked({})
194
189
  except Exception:
195
- pass
190
+ LOGGER.warning(
191
+ "Failed to reset app server thread registry after corruption.",
192
+ exc_info=True,
193
+ )
@@ -0,0 +1,165 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Any, Optional, Sequence
4
+
5
+ from .logging_utils import log_event
6
+ from .utils import resolve_executable, subprocess_env
7
+
8
+
9
+ def app_server_env(
10
+ command: Sequence[str],
11
+ cwd: Path,
12
+ *,
13
+ base_env: Optional[dict[str, str]] = None,
14
+ ) -> dict[str, str]:
15
+ extra_paths: list[str] = []
16
+ if command:
17
+ binary = command[0]
18
+ resolved = resolve_executable(binary, env=base_env)
19
+ candidate: Optional[Path] = Path(resolved) if resolved else None
20
+ if candidate is None:
21
+ candidate = Path(binary).expanduser()
22
+ if not candidate.is_absolute():
23
+ candidate = (cwd / candidate).resolve()
24
+ if candidate.exists():
25
+ extra_paths.append(str(candidate.parent))
26
+ return subprocess_env(extra_paths=extra_paths, base_env=base_env)
27
+
28
+
29
+ def seed_codex_home(
30
+ codex_home: Path,
31
+ *,
32
+ logger: Any = None,
33
+ event_prefix: str = "app_server",
34
+ ) -> None:
35
+ logger = logger or __import__("logging").getLogger(__name__)
36
+ auth_path = codex_home / "auth.json"
37
+ source_root = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
38
+ if source_root.resolve() == codex_home.resolve():
39
+ return
40
+ source_auth = source_root / "auth.json"
41
+ if auth_path.exists():
42
+ if auth_path.is_symlink() and auth_path.resolve() == source_auth.resolve():
43
+ return
44
+ log_event(
45
+ logger,
46
+ __import__("logging").INFO,
47
+ f"{event_prefix}.codex_home.seed.skipped",
48
+ reason="auth_exists",
49
+ source=str(source_root),
50
+ target=str(codex_home),
51
+ )
52
+ return
53
+ if not source_root.exists():
54
+ log_event(
55
+ logger,
56
+ __import__("logging").WARNING,
57
+ f"{event_prefix}.codex_home.seed.skipped",
58
+ reason="source_missing",
59
+ source=str(source_root),
60
+ target=str(codex_home),
61
+ )
62
+ return
63
+ if not source_auth.exists():
64
+ log_event(
65
+ logger,
66
+ __import__("logging").WARNING,
67
+ f"{event_prefix}.codex_home.seed.skipped",
68
+ reason="auth_missing",
69
+ source=str(source_root),
70
+ target=str(codex_home),
71
+ )
72
+ return
73
+ try:
74
+ auth_path.symlink_to(source_auth)
75
+ log_event(
76
+ logger,
77
+ __import__("logging").INFO,
78
+ f"{event_prefix}.codex_home.seeded",
79
+ source=str(source_root),
80
+ target=str(codex_home),
81
+ )
82
+ except OSError as exc:
83
+ log_event(
84
+ logger,
85
+ __import__("logging").WARNING,
86
+ f"{event_prefix}.codex_home.seed.failed",
87
+ exc=exc,
88
+ source=str(source_root),
89
+ target=str(codex_home),
90
+ )
91
+
92
+
93
+ def build_app_server_env(
94
+ command: Sequence[str],
95
+ workspace_root: Path,
96
+ state_dir: Path,
97
+ *,
98
+ logger: Any = None,
99
+ event_prefix: str = "app_server",
100
+ base_env: Optional[dict[str, str]] = None,
101
+ ) -> dict[str, str]:
102
+ env = app_server_env(command, workspace_root, base_env=base_env)
103
+ codex_home = state_dir / "codex_home"
104
+ codex_home.mkdir(parents=True, exist_ok=True)
105
+ seed_codex_home(codex_home, logger=logger, event_prefix=event_prefix)
106
+ env["CODEX_HOME"] = str(codex_home)
107
+ return env
108
+
109
+
110
+ def _extract_turn_id(payload: Any) -> Optional[str]:
111
+ if not isinstance(payload, dict):
112
+ return None
113
+ for key in ("turnId", "turn_id", "id"):
114
+ value = payload.get(key)
115
+ if isinstance(value, str):
116
+ return value
117
+ turn = payload.get("turn")
118
+ if isinstance(turn, dict):
119
+ for key in ("id", "turnId", "turn_id"):
120
+ value = turn.get(key)
121
+ if isinstance(value, str):
122
+ return value
123
+ return None
124
+
125
+
126
+ def _extract_thread_id_from_container(payload: Any) -> Optional[str]:
127
+ if not isinstance(payload, dict):
128
+ return None
129
+ for key in ("threadId", "thread_id"):
130
+ value = payload.get(key)
131
+ if isinstance(value, str):
132
+ return value
133
+ thread = payload.get("thread")
134
+ if isinstance(thread, dict):
135
+ for key in ("id", "threadId", "thread_id"):
136
+ value = thread.get(key)
137
+ if isinstance(value, str):
138
+ return value
139
+ return None
140
+
141
+
142
+ def _extract_thread_id_for_turn(payload: Any) -> Optional[str]:
143
+ if not isinstance(payload, dict):
144
+ return None
145
+ for candidate in (payload, payload.get("turn"), payload.get("item")):
146
+ thread_id = _extract_thread_id_from_container(candidate)
147
+ if thread_id:
148
+ return thread_id
149
+ return None
150
+
151
+
152
+ def _extract_thread_id(payload: Any) -> Optional[str]:
153
+ if not isinstance(payload, dict):
154
+ return None
155
+ for key in ("threadId", "thread_id", "id"):
156
+ value = payload.get(key)
157
+ if isinstance(value, str):
158
+ return value
159
+ thread = payload.get("thread")
160
+ if isinstance(thread, dict):
161
+ for key in ("id", "threadId", "thread_id"):
162
+ value = thread.get(key)
163
+ if isinstance(value, str):
164
+ return value
165
+ return None