codex-autorunner 0.1.2__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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
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,18 +39,17 @@ 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
 
54
55
  logger = logging.getLogger("codex_autorunner.cli")
@@ -755,7 +756,7 @@ def log(
755
756
 
756
757
  @app.command()
757
758
  def edit(
758
- target: str = typer.Argument(..., help="todo|progress|opinions|spec"),
759
+ target: str = typer.Argument(..., help="active_context|decisions|spec"),
759
760
  repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
760
761
  hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
761
762
  ):
@@ -763,8 +764,8 @@ def edit(
763
764
  engine = _require_repo_config(repo, hub)
764
765
  config = engine.config
765
766
  key = target.lower()
766
- if key not in ("todo", "progress", "opinions", "spec"):
767
- _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")
768
769
  path = config.doc_path(key)
769
770
  editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or default_editor()
770
771
  editor_parts = shlex.split(editor)
@@ -774,84 +775,6 @@ def edit(
774
775
  subprocess.run([*editor_parts, str(path)])
775
776
 
776
777
 
777
- @app.command("ingest-spec")
778
- def ingest_spec_cmd(
779
- repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
780
- hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
781
- spec: Optional[Path] = typer.Option(
782
- None, "--spec", help="Path to SPEC (defaults to configured docs.spec)"
783
- ),
784
- force: bool = typer.Option(
785
- False, "--force", help="Overwrite TODO/PROGRESS/OPINIONS"
786
- ),
787
- ):
788
- """Generate TODO/PROGRESS/OPINIONS from SPEC using Codex."""
789
- try:
790
- engine = _require_repo_config(repo, hub)
791
- config = engine.config
792
- if not config.app_server.command:
793
- raise SpecIngestError("app_server.command must be configured")
794
-
795
- async def _run_ingest() -> dict:
796
- logger = logging.getLogger("codex_autorunner.cli.app_server")
797
-
798
- def _env_builder(
799
- workspace_root: Path, _workspace_id: str, state_dir: Path
800
- ) -> dict[str, str]:
801
- state_dir.mkdir(parents=True, exist_ok=True)
802
- return build_app_server_env(
803
- config.app_server.command,
804
- workspace_root,
805
- state_dir,
806
- logger=logger,
807
- event_prefix="cli",
808
- )
809
-
810
- supervisor = WorkspaceAppServerSupervisor(
811
- config.app_server.command,
812
- state_root=config.app_server.state_root,
813
- env_builder=_env_builder,
814
- logger=logger,
815
- max_handles=config.app_server.max_handles,
816
- idle_ttl_seconds=config.app_server.idle_ttl_seconds,
817
- request_timeout=config.app_server.request_timeout,
818
- )
819
- service = SpecIngestService(engine, app_server_supervisor=supervisor)
820
- try:
821
- await service.execute(force=force, spec_path=spec, message=None)
822
- return service.apply_patch()
823
- finally:
824
- await supervisor.close_all()
825
-
826
- docs = asyncio.run(_run_ingest())
827
- except (ConfigError, SpecIngestError) as exc:
828
- _raise_exit(str(exc), cause=exc)
829
-
830
- typer.echo("Ingested SPEC into TODO/PROGRESS/OPINIONS.")
831
- for key, content in docs.items():
832
- lines = len(content.splitlines())
833
- typer.echo(f"- {key.upper()}: {lines} lines")
834
-
835
-
836
- @app.command("clear-docs")
837
- def clear_docs_cmd(
838
- repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
839
- hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
840
- yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
841
- ):
842
- """Clear TODO/PROGRESS/OPINIONS to empty templates."""
843
- if not yes:
844
- confirm = input("Clear TODO/PROGRESS/OPINIONS? Type CLEAR to confirm: ").strip()
845
- if confirm.upper() != "CLEAR":
846
- _raise_exit("Aborted.")
847
- engine = _require_repo_config(repo, hub)
848
- try:
849
- clear_work_docs(engine)
850
- except ConfigError as exc:
851
- _raise_exit(str(exc), cause=exc)
852
- typer.echo("Cleared TODO/PROGRESS/OPINIONS.")
853
-
854
-
855
778
  @app.command("doctor")
856
779
  def doctor_cmd(
857
780
  repo: Optional[Path] = typer.Option(None, "--repo", help="Repo or hub path"),
@@ -859,7 +782,19 @@ def doctor_cmd(
859
782
  ):
860
783
  """Validate repo or hub setup."""
861
784
  try:
862
- 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)
863
798
  except ConfigError as exc:
864
799
  _raise_exit(str(exc), cause=exc)
865
800
  if json_output:
@@ -877,56 +812,6 @@ def doctor_cmd(
877
812
  typer.echo("Doctor check passed")
878
813
 
879
814
 
880
- @app.command()
881
- def snapshot(
882
- repo: Optional[Path] = typer.Option(None, "--repo", help="Repo path"),
883
- hub: Optional[Path] = typer.Option(None, "--hub", help="Hub root path"),
884
- ):
885
- """Generate or update `.codex-autorunner/SNAPSHOT.md`."""
886
- try:
887
- engine = _require_repo_config(repo, hub)
888
- config = engine.config
889
- if not config.app_server.command:
890
- raise SnapshotError("app_server.command must be configured")
891
-
892
- async def _run_snapshot() -> None:
893
- logger = logging.getLogger("codex_autorunner.cli.app_server")
894
-
895
- def _env_builder(
896
- workspace_root: Path, _workspace_id: str, state_dir: Path
897
- ) -> dict[str, str]:
898
- state_dir.mkdir(parents=True, exist_ok=True)
899
- return build_app_server_env(
900
- config.app_server.command,
901
- workspace_root,
902
- state_dir,
903
- logger=logger,
904
- event_prefix="cli",
905
- )
906
-
907
- supervisor = WorkspaceAppServerSupervisor(
908
- config.app_server.command,
909
- state_root=config.app_server.state_root,
910
- env_builder=_env_builder,
911
- logger=logger,
912
- max_handles=config.app_server.max_handles,
913
- idle_ttl_seconds=config.app_server.idle_ttl_seconds,
914
- request_timeout=config.app_server.request_timeout,
915
- )
916
- from .core.snapshot import SnapshotService
917
-
918
- service = SnapshotService(engine, app_server_supervisor=supervisor)
919
- try:
920
- await service.generate_snapshot()
921
- finally:
922
- await supervisor.close_all()
923
-
924
- asyncio.run(_run_snapshot())
925
- except (ConfigError, SnapshotError) as exc:
926
- _raise_exit(str(exc), cause=exc)
927
- typer.echo("Snapshot written to .codex-autorunner/SNAPSHOT.md")
928
-
929
-
930
815
  @app.command()
931
816
  def serve(
932
817
  path: Optional[Path] = typer.Option(None, "--path", "--hub", help="Hub root path"),
@@ -1106,6 +991,8 @@ def telegram_start(
1106
991
  housekeeping_config=config.housekeeping,
1107
992
  update_repo_url=update_repo_url,
1108
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,
1109
996
  )
1110
997
  await service.run_polling()
1111
998
 
@@ -1151,8 +1038,139 @@ def telegram_health(
1151
1038
  asyncio.run(_run())
1152
1039
  except TelegramAPIError as exc:
1153
1040
  _raise_exit(f"Telegram health check failed: {exc}", cause=exc)
1154
- except Exception as exc:
1155
- _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}")
1156
1174
 
1157
1175
 
1158
1176
  if __name__ == "__main__":
@@ -17,6 +17,26 @@ ABOUT_CAR_REL_PATH = Path(".codex-autorunner") / ABOUT_CAR_BASENAME
17
17
  # If this marker is present, codex-autorunner may safely refresh the file content.
18
18
  ABOUT_CAR_GENERATED_MARKER = "<!-- CAR:AUTOGENERATED -->"
19
19
 
20
+ CAR_CONTEXT_KEYWORDS = (
21
+ "car",
22
+ "codex",
23
+ "spec",
24
+ "autorunner",
25
+ "workspace",
26
+ "ticket",
27
+ "tickets",
28
+ "context",
29
+ "decision",
30
+ "decisions",
31
+ "handoff",
32
+ "dispatch",
33
+ "inbox",
34
+ )
35
+
36
+ CAR_CONTEXT_HINT = (
37
+ "Context: read .codex-autorunner/ABOUT_CAR.md for repo-specific rules."
38
+ )
39
+
20
40
 
21
41
  def _display_path(repo_root: Path, path: Path) -> str:
22
42
  try:
@@ -28,11 +48,9 @@ def _display_path(repo_root: Path, path: Path) -> str:
28
48
  def build_about_car_markdown(
29
49
  *,
30
50
  repo_root: Path,
31
- todo_path: Path,
32
- progress_path: Path,
33
- opinions_path: Path,
51
+ active_context_path: Path,
52
+ decisions_path: Path,
34
53
  spec_path: Path,
35
- summary_path: Path,
36
54
  hub_config_path: Optional[Path] = None,
37
55
  repo_override_path: Optional[Path] = None,
38
56
  ) -> str:
@@ -44,11 +62,9 @@ def build_about_car_markdown(
44
62
  repo_override_path = repo_override_path or (repo_root / REPO_OVERRIDE_FILENAME)
45
63
  root_config_path = repo_root / ROOT_CONFIG_FILENAME
46
64
  root_override_path = repo_root / ROOT_OVERRIDE_FILENAME
47
- todo_disp = _display_path(repo_root, todo_path)
48
- progress_disp = _display_path(repo_root, progress_path)
49
- opinions_disp = _display_path(repo_root, opinions_path)
65
+ active_context_disp = _display_path(repo_root, active_context_path)
66
+ decisions_disp = _display_path(repo_root, decisions_path)
50
67
  spec_disp = _display_path(repo_root, spec_path)
51
- summary_disp = _display_path(repo_root, summary_path)
52
68
  hub_config_disp = _display_path(repo_root, hub_config_path)
53
69
  repo_override_disp = _display_path(repo_root, repo_override_path)
54
70
  root_config_disp = _display_path(repo_root, root_config_path)
@@ -58,25 +74,25 @@ def build_about_car_markdown(
58
74
  f"{ABOUT_CAR_GENERATED_MARKER}\n"
59
75
  "# ABOUT_CAR — Codex Autorunner (CAR)\n\n"
60
76
  "You are running inside **Codex Autorunner (CAR)**.\n\n"
61
- "CAR uses a small set of markdown **work docs** as the control surface for long-horizon work. "
62
- "These docs live under the repo-local, gitignored `.codex-autorunner/` directory.\n\n"
63
- "## Work docs (canonical)\n"
64
- "- **TODO** ordered checklist of high-level tasks: "
65
- f"`{todo_disp}`\n"
66
- "- **PROGRESS** — running notes / validation / context: "
67
- f"`{progress_disp}`\n"
68
- "- **OPINIONS** — constraints + style guidelines: "
69
- f"`{opinions_disp}`\n"
70
- "- **SPEC** — source-of-truth requirements and scope: "
71
- f"`{spec_disp}`\n"
72
- "- **SUMMARY** — user-facing report + external/user action items: "
73
- f"`{summary_disp}`\n\n"
77
+ "CAR uses a ticket-first workflow.\n\n"
78
+ "## Required for operation\n"
79
+ "- Tickets live under `.codex-autorunner/tickets/`.\n\n"
80
+ "## Optional workspace docs\n"
81
+ "- **Active context**: "
82
+ f"`{active_context_disp}`\n"
83
+ "- **Decisions**: "
84
+ f"`{decisions_disp}`\n"
85
+ "- **Spec**: "
86
+ f"`{spec_disp}`\n\n"
74
87
  "## Critical rules\n"
75
- f'- When the user says **"add this to the TODOs"**, edit `{todo_disp}`.\n'
76
- "- Do **not** create new copies of TODO/PROGRESS/OPINIONS/SPEC/SUMMARY elsewhere in the repo.\n"
88
+ "- Do **not** create new copies of workspace docs elsewhere in the repo.\n"
77
89
  "- Treat `.codex-autorunner/` as intentional project structure even though it is hidden/gitignored.\n\n"
90
+ "## Agent Flow\n"
91
+ "- **Dispatch**: An update or message from the agent.\n"
92
+ "- **Handoff**: Passing control from agent to user (or vice versa).\n"
93
+ "- **Inbox**: Where the agent receives files/messages.\n\n"
78
94
  "## How CAR works (short)\n"
79
- "- `car run/once` repeatedly runs Codex non-interactively, feeding it the work docs (and the prior run tail).\n"
95
+ "- The web UI provides ticket editing + unified file chat.\n"
80
96
  "- `car serve` starts the hub web UI. The **Terminal** tab launches the configured `codex` binary in a PTY.\n"
81
97
  f"- Hub config lives at `{hub_config_disp}` (generated).\n"
82
98
  f"- Repo overrides (optional) live at `{repo_override_disp}`.\n"
@@ -101,11 +117,9 @@ def ensure_about_car_file_for_repo(
101
117
 
102
118
  content = build_about_car_markdown(
103
119
  repo_root=repo_root,
104
- todo_path=doc_paths["todo"],
105
- progress_path=doc_paths["progress"],
106
- opinions_path=doc_paths["opinions"],
120
+ active_context_path=doc_paths["active_context"],
121
+ decisions_path=doc_paths["decisions"],
107
122
  spec_path=doc_paths["spec"],
108
- summary_path=doc_paths["summary"],
109
123
  )
110
124
  if content and not content.endswith("\n"):
111
125
  content += "\n"
@@ -129,10 +143,8 @@ def ensure_about_car_file(config: Config, *, force: bool = False) -> Path:
129
143
  """Config-aware wrapper that uses configured doc paths."""
130
144
  repo_root = config.root
131
145
  docs = {
132
- "todo": config.doc_path("todo"),
133
- "progress": config.doc_path("progress"),
134
- "opinions": config.doc_path("opinions"),
146
+ "active_context": config.doc_path("active_context"),
147
+ "decisions": config.doc_path("decisions"),
135
148
  "spec": config.doc_path("spec"),
136
- "summary": config.doc_path("summary"),
137
149
  }
138
150
  return ensure_about_car_file_for_repo(repo_root, doc_paths=docs, force=force)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+
6
+ def handle_agent_output(
7
+ log_app_server_output: Callable[[int, list[str]], None],
8
+ write_run_artifact: Callable[[int, str, str], Any],
9
+ merge_run_index_entry: Callable[[int, dict[str, Any]], None],
10
+ run_id: int,
11
+ output: str | list[str],
12
+ ) -> None:
13
+ if isinstance(output, str):
14
+ messages = [output]
15
+ else:
16
+ messages = output
17
+ log_app_server_output(run_id, messages)
18
+ output_text = "\n\n".join(messages).strip() if messages else ""
19
+ if output_text:
20
+ output_path = write_run_artifact(run_id, "output.txt", output_text)
21
+ merge_run_index_entry(run_id, {"artifacts": {"output_path": str(output_path)}})
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, cast
4
4
 
5
+ from .redaction import redact_text
5
6
  from .text_delta_coalescer import TextDeltaCoalescer
6
7
 
7
8
 
@@ -69,7 +70,8 @@ def _extract_error_message(params: Any) -> str:
69
70
 
70
71
 
71
72
  class AppServerEventFormatter:
72
- def __init__(self) -> None:
73
+ def __init__(self, redact_enabled: bool = True) -> None:
74
+ self._redact_enabled = redact_enabled
73
75
  self._thinking_items: set[str] = set()
74
76
  self._reasoning_coalescers: dict[str, TextDeltaCoalescer] = {}
75
77
 
@@ -187,7 +189,8 @@ class AppServerEventFormatter:
187
189
  or params.get("value")
188
190
  )
189
191
  if isinstance(diff, str) and diff:
190
- lines.extend(diff.splitlines())
192
+ diff_text = redact_text(diff) if self._redact_enabled else diff
193
+ lines.extend(diff_text.splitlines())
191
194
  return lines
192
195
 
193
196
  if method == "error":
@@ -199,7 +202,8 @@ class AppServerEventFormatter:
199
202
  if "outputdelta" in method.lower():
200
203
  delta = params.get("delta") or params.get("text") or params.get("output")
201
204
  if isinstance(delta, str) and delta:
202
- lines.extend(delta.splitlines())
205
+ delta_text = redact_text(delta) if self._redact_enabled else delta
206
+ lines.extend(delta_text.splitlines())
203
207
  return lines
204
208
 
205
209
  return lines