codexloop 0.2.0__tar.gz → 0.3.0__tar.gz

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 (258) hide show
  1. {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/ci.yml +24 -1
  2. {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/release-please.yml +5 -1
  3. codexloop-0.3.0/.release-please-manifest.json +3 -0
  4. {codexloop-0.2.0 → codexloop-0.3.0}/CHANGELOG.md +30 -0
  5. {codexloop-0.2.0 → codexloop-0.3.0}/PKG-INFO +1 -1
  6. {codexloop-0.2.0 → codexloop-0.3.0}/pyproject.toml +14 -1
  7. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/__init__.py +1 -1
  8. codexloop-0.3.0/src/codexloop/application/interfaces/__init__.py +75 -0
  9. codexloop-0.3.0/src/codexloop/application/interfaces/agent.py +46 -0
  10. codexloop-0.3.0/src/codexloop/application/interfaces/api.py +10 -0
  11. codexloop-0.3.0/src/codexloop/application/interfaces/control.py +21 -0
  12. {codexloop-0.2.0/src/codexloop/application/usecases → codexloop-0.3.0/src/codexloop/application/interfaces}/doctor.py +7 -9
  13. codexloop-0.3.0/src/codexloop/application/interfaces/observability.py +42 -0
  14. codexloop-0.3.0/src/codexloop/application/interfaces/permissions.py +14 -0
  15. codexloop-0.3.0/src/codexloop/application/interfaces/storage.py +31 -0
  16. codexloop-0.3.0/src/codexloop/application/interfaces/system.py +16 -0
  17. codexloop-0.3.0/src/codexloop/application/ports.py +61 -0
  18. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/runner.py +115 -1
  19. codexloop-0.3.0/src/codexloop/application/usecases/doctor.py +18 -0
  20. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/run_control.py +1 -5
  21. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/bootstrap.py +15 -2
  22. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/app.py +24 -1
  23. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/run.py +10 -1
  24. codexloop-0.3.0/src/codexloop/domain/forecast.py +271 -0
  25. codexloop-0.3.0/src/codexloop/domain/handoff_marker.py +107 -0
  26. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/loop.py +31 -2
  27. codexloop-0.3.0/src/codexloop/domain/verbosity.py +80 -0
  28. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/logging.py +21 -0
  29. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/rundir.py +27 -2
  30. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/fakes.py +3 -0
  31. codexloop-0.3.0/tests/application/test_interfaces_convention.py +100 -0
  32. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_runner.py +66 -0
  33. {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_app.py +12 -0
  34. codexloop-0.3.0/tests/cli/test_verbosity_flags.py +48 -0
  35. codexloop-0.3.0/tests/domain/test_forecast.py +209 -0
  36. codexloop-0.3.0/tests/domain/test_handoff_marker.py +83 -0
  37. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_loop.py +73 -1
  38. codexloop-0.3.0/tests/domain/test_verbosity.py +60 -0
  39. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_providers_gateway_extra.py +2 -1
  40. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_appserver_gateway.py +9 -0
  41. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_coverage_boost.py +5 -1
  42. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_doctor_env.py +36 -0
  43. codexloop-0.3.0/tests/infrastructure/test_git_savepoints_units.py +69 -0
  44. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_logging_redact.py +41 -0
  45. codexloop-0.3.0/tests/infrastructure/test_rollout_scan.py +91 -0
  46. codexloop-0.3.0/tests/infrastructure/test_rundir_run_id.py +48 -0
  47. codexloop-0.3.0/uv.lock +2277 -0
  48. codexloop-0.2.0/.release-please-manifest.json +0 -3
  49. codexloop-0.2.0/src/codexloop/application/ports.py +0 -158
  50. {codexloop-0.2.0 → codexloop-0.3.0}/.agents/skills/codexloop-run/SKILL.md +0 -0
  51. {codexloop-0.2.0 → codexloop-0.3.0}/.claude/skills/codexloop-run/SKILL.md +0 -0
  52. {codexloop-0.2.0 → codexloop-0.3.0}/.cursor/rules/codexloop-run.mdc +0 -0
  53. {codexloop-0.2.0 → codexloop-0.3.0}/.editorconfig +0 -0
  54. {codexloop-0.2.0 → codexloop-0.3.0}/.github/CODEOWNERS +0 -0
  55. {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/docs.yml +0 -0
  56. {codexloop-0.2.0 → codexloop-0.3.0}/.github/workflows/publish-testpypi.yml +0 -0
  57. {codexloop-0.2.0 → codexloop-0.3.0}/.gitignore +0 -0
  58. {codexloop-0.2.0 → codexloop-0.3.0}/.pre-commit-config.yaml +0 -0
  59. {codexloop-0.2.0 → codexloop-0.3.0}/AGENTS.md +0 -0
  60. {codexloop-0.2.0 → codexloop-0.3.0}/CLAUDE.md +0 -0
  61. {codexloop-0.2.0 → codexloop-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  62. {codexloop-0.2.0 → codexloop-0.3.0}/CONTRIBUTING.md +0 -0
  63. {codexloop-0.2.0 → codexloop-0.3.0}/LICENSE +0 -0
  64. {codexloop-0.2.0 → codexloop-0.3.0}/README.md +0 -0
  65. {codexloop-0.2.0 → codexloop-0.3.0}/SECURITY.md +0 -0
  66. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0001-onion-import-linter.md +0 -0
  67. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0002-subprocess-codex-exec.md +0 -0
  68. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0003-credits-exhausted.md +0 -0
  69. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0004-adaptive-probe-loop.md +0 -0
  70. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0005-layered-capacity-probe.md +0 -0
  71. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0006-generated-openai-rest.md +0 -0
  72. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0007-default-approval-sandbox.md +0 -0
  73. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0008-policy-via-c-flags.md +0 -0
  74. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0009-optional-app-server.md +0 -0
  75. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0010-rollout-best-effort.md +0 -0
  76. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0011-never-consume-resets.md +0 -0
  77. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/0012-minimum-codex-version.md +0 -0
  78. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/adr/index.md +0 -0
  79. {codexloop-0.2.0 → codexloop-0.3.0}/docs/architecture/index.md +0 -0
  80. {codexloop-0.2.0 → codexloop-0.3.0}/docs/contributing.md +0 -0
  81. {codexloop-0.2.0 → codexloop-0.3.0}/docs/getting-started.md +0 -0
  82. {codexloop-0.2.0 → codexloop-0.3.0}/docs/guides/rest-api-surface.md +0 -0
  83. {codexloop-0.2.0 → codexloop-0.3.0}/docs/index.md +0 -0
  84. {codexloop-0.2.0 → codexloop-0.3.0}/docs/plans/_shared-transplant-outline.md +0 -0
  85. {codexloop-0.2.0 → codexloop-0.3.0}/docs/plans/architecture-and-roadmap.md +0 -0
  86. {codexloop-0.2.0 → codexloop-0.3.0}/docs/plans/research-notes.md +0 -0
  87. {codexloop-0.2.0 → codexloop-0.3.0}/docs/publishing.md +0 -0
  88. {codexloop-0.2.0 → codexloop-0.3.0}/docs/reference/cli.md +0 -0
  89. {codexloop-0.2.0 → codexloop-0.3.0}/docs/superpowers/plans/2026-08-13-codexloop-implementation.md +0 -0
  90. {codexloop-0.2.0 → codexloop-0.3.0}/mkdocs.yml +0 -0
  91. {codexloop-0.2.0 → codexloop-0.3.0}/release-please-config.json +0 -0
  92. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/__init__.py +0 -0
  93. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/dto.py +0 -0
  94. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/__init__.py +0 -0
  95. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/list_threads.py +0 -0
  96. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/preflight.py +0 -0
  97. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/resume_thread.py +0 -0
  98. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/application/usecases/run_plan.py +0 -0
  99. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/__init__.py +0 -0
  100. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/asyncio.py +0 -0
  101. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/__init__.py +0 -0
  102. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/approval_cmd.py +0 -0
  103. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/capacity.py +0 -0
  104. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/cwd_cmd.py +0 -0
  105. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/doctor.py +0 -0
  106. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/effort_cmd.py +0 -0
  107. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/logs.py +0 -0
  108. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/model_cmd.py +0 -0
  109. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/prompt.py +0 -0
  110. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/reset.py +0 -0
  111. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/resume.py +0 -0
  112. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/runs.py +0 -0
  113. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/sandbox_cmd.py +0 -0
  114. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/savepoints.py +0 -0
  115. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/snapshot.py +0 -0
  116. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/status.py +0 -0
  117. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/stop.py +0 -0
  118. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/threads.py +0 -0
  119. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/unwind.py +0 -0
  120. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/commands/watch.py +0 -0
  121. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/cli/render.py +0 -0
  122. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/__init__.py +0 -0
  123. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/approval.py +0 -0
  124. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/backoff.py +0 -0
  125. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/budget.py +0 -0
  126. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/capacity.py +0 -0
  127. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/classify.py +0 -0
  128. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/completion.py +0 -0
  129. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/control.py +0 -0
  130. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/error_codes.py +0 -0
  131. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/errors.py +0 -0
  132. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/model_profile.py +0 -0
  133. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/plan.py +0 -0
  134. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/savepoint.py +0 -0
  135. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/savepoint_message.py +0 -0
  136. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/session.py +0 -0
  137. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/signals.py +0 -0
  138. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/domain/waiting.py +0 -0
  139. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/__init__.py +0 -0
  140. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/__init__.py +0 -0
  141. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/argv.py +0 -0
  142. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/events.py +0 -0
  143. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/gateway.py +0 -0
  144. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/probe.py +0 -0
  145. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/process.py +0 -0
  146. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/schema.py +0 -0
  147. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/scripted.py +0 -0
  148. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/agent/translate.py +0 -0
  149. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/__init__.py +0 -0
  150. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/api_baseline.json +0 -0
  151. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/binder.py +0 -0
  152. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/gateway.py +0 -0
  153. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/introspect.py +0 -0
  154. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/json_io.py +0 -0
  155. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/params.py +0 -0
  156. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/providers.py +0 -0
  157. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/api/registry.py +0 -0
  158. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/__init__.py +0 -0
  159. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/client.py +0 -0
  160. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/gateway.py +0 -0
  161. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/appserver/ratelimits.py +0 -0
  162. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/audit.py +0 -0
  163. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/capacity_probe.py +0 -0
  164. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/clock.py +0 -0
  165. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/config.py +0 -0
  166. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/control.py +0 -0
  167. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/doctor_env.py +0 -0
  168. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/events.py +0 -0
  169. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/git_savepoints.py +0 -0
  170. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/lock.py +0 -0
  171. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/notify.py +0 -0
  172. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/progress.py +0 -0
  173. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/redact.py +0 -0
  174. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/rollout.py +0 -0
  175. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/snapshot.py +0 -0
  176. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/state.py +0 -0
  177. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/state_bus.py +0 -0
  178. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/infrastructure/stream_ui.py +0 -0
  179. {codexloop-0.2.0 → codexloop-0.3.0}/src/codexloop/py.typed +0 -0
  180. {codexloop-0.2.0 → codexloop-0.3.0}/tests/__init__.py +0 -0
  181. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/__init__.py +0 -0
  182. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_doctor.py +0 -0
  183. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_ports.py +0 -0
  184. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_usecases.py +0 -0
  185. {codexloop-0.2.0 → codexloop-0.3.0}/tests/application/test_waiting_runner.py +0 -0
  186. {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/__init__.py +0 -0
  187. {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_api.py +0 -0
  188. {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_asyncio.py +0 -0
  189. {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_e2e_shim.py +0 -0
  190. {codexloop-0.2.0 → codexloop-0.3.0}/tests/cli/test_ops_commands.py +0 -0
  191. {codexloop-0.2.0 → codexloop-0.3.0}/tests/conftest.py +0 -0
  192. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/__init__.py +0 -0
  193. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/strategies.py +0 -0
  194. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_approval.py +0 -0
  195. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_backoff.py +0 -0
  196. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_budget.py +0 -0
  197. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_capacity.py +0 -0
  198. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_classify.py +0 -0
  199. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_completion.py +0 -0
  200. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_control.py +0 -0
  201. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_error_codes.py +0 -0
  202. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_model_profile.py +0 -0
  203. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_plan.py +0 -0
  204. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_savepoint.py +0 -0
  205. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_session.py +0 -0
  206. {codexloop-0.2.0 → codexloop-0.3.0}/tests/domain/test_waiting.py +0 -0
  207. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/README.md +0 -0
  208. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/clean_completion.jsonl +0 -0
  209. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/done_with_marker.jsonl +0 -0
  210. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/huge_line.jsonl +0 -0
  211. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/malformed_line.jsonl +0 -0
  212. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/tool_heavy.jsonl +0 -0
  213. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/truncated_stream.jsonl +0 -0
  214. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/turn_failed_429_quota.jsonl +0 -0
  215. {codexloop-0.2.0 → codexloop-0.3.0}/tests/fixtures/jsonl/turn_failed_429_window.jsonl +0 -0
  216. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/__init__.py +0 -0
  217. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/__init__.py +0 -0
  218. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_binder.py +0 -0
  219. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_drift.py +0 -0
  220. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_gateway.py +0 -0
  221. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_introspect.py +0 -0
  222. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_json_io.py +0 -0
  223. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_params.py +0 -0
  224. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_params_extra.py +0 -0
  225. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/api/test_providers.py +0 -0
  226. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_appserver.py +0 -0
  227. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_argv.py +0 -0
  228. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_capacity_probe.py +0 -0
  229. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_config.py +0 -0
  230. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_control_and_savepoints.py +0 -0
  231. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_events.py +0 -0
  232. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_gateway.py +0 -0
  233. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_lock.py +0 -0
  234. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_probe.py +0 -0
  235. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_process.py +0 -0
  236. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_ratelimits_extra.py +0 -0
  237. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_rollout.py +0 -0
  238. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_rundir_state.py +0 -0
  239. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_scripted.py +0 -0
  240. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_snapshot.py +0 -0
  241. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_state_bus.py +0 -0
  242. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_stream_ui.py +0 -0
  243. {codexloop-0.2.0 → codexloop-0.3.0}/tests/infrastructure/test_translate.py +0 -0
  244. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/done.json +0 -0
  245. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/quota_then_topup.json +0 -0
  246. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/unknown_429.json +0 -0
  247. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/wait_long.json +0 -0
  248. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/fixtures/agent_scripts/window_then_done.json +0 -0
  249. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/system/conftest.py +0 -0
  250. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/system/test_matrix_inprocess.py +0 -0
  251. {codexloop-0.2.0 → codexloop-0.3.0}/tests/live/system/test_subprocess_smoke.py +0 -0
  252. {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/__init__.py +0 -0
  253. {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/fake_appserver.py +0 -0
  254. {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/fake_codex.py +0 -0
  255. {codexloop-0.2.0 → codexloop-0.3.0}/tests/shim/test_fake_codex.py +0 -0
  256. {codexloop-0.2.0 → codexloop-0.3.0}/tests/test_bootstrap.py +0 -0
  257. {codexloop-0.2.0 → codexloop-0.3.0}/tests/test_no_vendor_leak.py +0 -0
  258. {codexloop-0.2.0 → codexloop-0.3.0}/tests/test_packaging.py +0 -0
@@ -35,7 +35,17 @@ jobs:
35
35
  - run: lint-imports
36
36
  - run: bandit -q -r src/codexloop
37
37
  - run: pip-audit
38
- - run: pytest --cov-report=term-missing
38
+ # Per layer, not in aggregate: a blanket floor lets a well-tested domain
39
+ # hide an untested adapter. pytest-cov unions --cov scopes within one
40
+ # invocation, so each floor needs its own run.
41
+ - name: Coverage floor - domain (100%)
42
+ run: pytest -q --cov=codexloop.domain --cov-fail-under=100
43
+ - name: Coverage floor - application (100%)
44
+ run: pytest -q --cov=codexloop.application --cov-fail-under=100
45
+ - name: Coverage floor - infrastructure (100%)
46
+ run: pytest -q --cov=codexloop.infrastructure --cov-fail-under=100
47
+ - name: Coverage floor - cli (100%)
48
+ run: pytest -q --cov=codexloop.cli --cov-fail-under=100
39
49
 
40
50
  docs:
41
51
  name: MkDocs builds strictly
@@ -49,6 +59,19 @@ jobs:
49
59
  - run: pip install -e ".[docs]"
50
60
  - run: mkdocs build --strict
51
61
 
62
+ uv-lock:
63
+ name: uv.lock is in sync with pyproject.toml
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v7.0.1
67
+ - uses: actions/setup-python@v7.0.0
68
+ with:
69
+ python-version: "3.12"
70
+ - uses: astral-sh/setup-uv@v3
71
+ with:
72
+ enable-cache: true
73
+ - run: uv lock --check
74
+
52
75
  build:
53
76
  name: Package builds and is installable
54
77
  runs-on: ubuntu-latest
@@ -9,6 +9,10 @@ on:
9
9
  description: "Build and publish ref to PyPI (Trusted Publishing). Use after tagging or to retry a release."
10
10
  type: boolean
11
11
  default: false
12
+ publish_tag:
13
+ description: "Git tag to build/publish when publish_to_pypi=true"
14
+ type: string
15
+ default: "v0.2.0"
12
16
 
13
17
  permissions:
14
18
  contents: write
@@ -65,7 +69,7 @@ jobs:
65
69
  steps:
66
70
  - uses: actions/checkout@v4
67
71
  with:
68
- ref: v0.1.0
72
+ ref: ${{ inputs.publish_tag }}
69
73
  - uses: actions/setup-python@v5
70
74
  with:
71
75
  python-version: "3.12"
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.0"
3
+ }
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/adammatthewsteinberger/codexloop/compare/v0.2.0...v0.3.0) (2026-08-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * add a -v/-q verbosity ladder and third-party log control ([b58dfc0](https://github.com/adammatthewsteinberger/codexloop/commit/b58dfc017ce9af34af07cb390ea9a8efd67435d3))
9
+ * add capacity forecasting (measurement only, disabled by default) ([c2a362b](https://github.com/adammatthewsteinberger/codexloop/commit/c2a362bcbd111e7f87e5fb2534eb1687e50dc42d))
10
+ * add the wind-down decision to the run loop state machine ([033b5b2](https://github.com/adammatthewsteinberger/codexloop/commit/033b5b2ecece721832f31cd08e103738c193c812))
11
+ * let the caller name a run with --run-id ([e22c2d0](https://github.com/adammatthewsteinberger/codexloop/commit/e22c2d05c03db5be84ecbbd544da418130c1023c))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **tests:** remove residual SHA-collision flake in savepoints test ([#20](https://github.com/adammatthewsteinberger/codexloop/issues/20)) ([b375f71](https://github.com/adammatthewsteinberger/codexloop/commit/b375f71ba5ebda62f178bdc39bb619418932dedf))
17
+
18
+ ## [0.3.0](https://github.com/adammatthewsteinberger/codexloop/compare/v0.2.0...v0.3.0) (2026-08-16)
19
+
20
+
21
+ ### Features
22
+
23
+ * add a -v/-q verbosity ladder and third-party log control ([b58dfc0](https://github.com/adammatthewsteinberger/codexloop/commit/b58dfc017ce9af34af07cb390ea9a8efd67435d3))
24
+ * add capacity forecasting (measurement only, disabled by default) ([c2a362b](https://github.com/adammatthewsteinberger/codexloop/commit/c2a362bcbd111e7f87e5fb2534eb1687e50dc42d))
25
+ * add the wind-down decision to the run loop state machine ([033b5b2](https://github.com/adammatthewsteinberger/codexloop/commit/033b5b2ecece721832f31cd08e103738c193c812))
26
+ * let the caller name a run with --run-id ([e22c2d0](https://github.com/adammatthewsteinberger/codexloop/commit/e22c2d05c03db5be84ecbbd544da418130c1023c))
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * **tests:** remove residual SHA-collision flake in savepoints test ([#20](https://github.com/adammatthewsteinberger/codexloop/issues/20)) ([b375f71](https://github.com/adammatthewsteinberger/codexloop/commit/b375f71ba5ebda62f178bdc39bb619418932dedf))
32
+
3
33
  ## [0.2.0](https://github.com/adammatthewsteinberger/codexloop/compare/v0.1.1...v0.2.0) (2026-08-14)
4
34
 
5
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codexloop
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Onion-architected, autonomous OpenAI Codex session runner — never blocks on a human, distinguishes rate limits from exhausted credits, and resumes safely across usage windows.
5
5
  Project-URL: Homepage, https://github.com/adammatthewsteinberger/codexloop
6
6
  Project-URL: Repository, https://github.com/adammatthewsteinberger/codexloop
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codexloop"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Onion-architected, autonomous OpenAI Codex session runner — never blocks on a human, distinguishes rate limits from exhausted credits, and resumes safely across usage windows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -173,3 +173,16 @@ forbidden_modules = [
173
173
  "docker",
174
174
  "sqlalchemy",
175
175
  ]
176
+
177
+
178
+ [[tool.importlinter.contracts]]
179
+ name = "Interfaces declare seams, never consume them"
180
+ type = "forbidden"
181
+ source_modules = ["codexloop.application.interfaces"]
182
+ forbidden_modules = [
183
+ "codexloop.application.runner",
184
+ "codexloop.application.usecases",
185
+ "codexloop.infrastructure",
186
+ "codexloop.cli",
187
+ "codexloop.bootstrap",
188
+ ]
@@ -1,3 +1,3 @@
1
1
  """codexloop — onion-architected, autonomous OpenAI Codex session runner."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,75 @@
1
+ """Application interfaces -- every seam implemented by infrastructure/ and
2
+ never imported from it.
3
+
4
+ One module per collaborator family so a reader finds a seam by what it does
5
+ rather than by scrolling one long file. `application/ports.py` re-exports
6
+ this package unchanged, so existing imports keep working.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from codexloop.application.interfaces.agent import (
12
+ AgentGateway,
13
+ CapacityProbe,
14
+ RunResources,
15
+ ThreadCatalog,
16
+ )
17
+ from codexloop.application.interfaces.api import (
18
+ ApiGateway,
19
+ )
20
+ from codexloop.application.interfaces.control import (
21
+ ControlInbox,
22
+ RunControl,
23
+ )
24
+ from codexloop.application.interfaces.doctor import (
25
+ DoctorCheck,
26
+ DoctorEnvironment,
27
+ DoctorReport,
28
+ )
29
+ from codexloop.application.interfaces.observability import (
30
+ AuditLog,
31
+ Logger,
32
+ Notifier,
33
+ ProgressReporter,
34
+ RunEventSink,
35
+ StateBus,
36
+ )
37
+ from codexloop.application.interfaces.permissions import (
38
+ PermissionMode,
39
+ )
40
+ from codexloop.application.interfaces.storage import (
41
+ RunSnapshotSink,
42
+ RunStateStore,
43
+ SavePointStore,
44
+ SessionLock,
45
+ )
46
+ from codexloop.application.interfaces.system import (
47
+ Clock,
48
+ Sleeper,
49
+ )
50
+
51
+ __all__ = [
52
+ "AgentGateway",
53
+ "ApiGateway",
54
+ "AuditLog",
55
+ "CapacityProbe",
56
+ "Clock",
57
+ "ControlInbox",
58
+ "DoctorCheck",
59
+ "DoctorEnvironment",
60
+ "DoctorReport",
61
+ "Logger",
62
+ "Notifier",
63
+ "PermissionMode",
64
+ "ProgressReporter",
65
+ "RunControl",
66
+ "RunEventSink",
67
+ "RunResources",
68
+ "RunSnapshotSink",
69
+ "RunStateStore",
70
+ "SavePointStore",
71
+ "SessionLock",
72
+ "Sleeper",
73
+ "StateBus",
74
+ "ThreadCatalog",
75
+ ]
@@ -0,0 +1,46 @@
1
+ """The vendor session seam: sending turns, probing capacity, enumerating
2
+ sessions, and the run-scoped resources attached to one."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Protocol, runtime_checkable
8
+
9
+ from codexloop.application.dto import ProbeResult, TurnOutcome
10
+ from codexloop.application.interfaces.permissions import PermissionMode
11
+ from codexloop.domain.model_profile import ModelEffortProfile
12
+ from codexloop.domain.session import ThreadRef
13
+
14
+
15
+ @runtime_checkable
16
+ class AgentGateway(Protocol):
17
+ """Vendor-agnostic agent session (exec and app-server adapters implement this)."""
18
+
19
+ async def send_turn(self, prompt: str) -> TurnOutcome: ...
20
+ async def close(self) -> None: ...
21
+ async def set_profile(self, profile: ModelEffortProfile) -> None: ...
22
+ async def set_permission_mode(self, mode: PermissionMode) -> None: ...
23
+ async def set_cwd(self, path: str) -> None: ...
24
+ async def set_session_resources(self, resources: Mapping[str, object]) -> None: ...
25
+ def resolve_tool_approval(self, request_id: str, *, allow: bool, reason: str = "") -> bool: ...
26
+
27
+
28
+ @runtime_checkable
29
+ class CapacityProbe(Protocol):
30
+ async def probe(self) -> ProbeResult: ...
31
+
32
+
33
+ @runtime_checkable
34
+ class ThreadCatalog(Protocol):
35
+ """Our run registry keyed by ``thread_id``, not vendor session discovery."""
36
+
37
+ def list_threads(self) -> Sequence[ThreadRef]: ...
38
+ def get(self, thread_id: str) -> ThreadRef | None: ...
39
+ def record(self, ref: ThreadRef) -> None: ...
40
+
41
+
42
+ @runtime_checkable
43
+ class RunResources(Protocol):
44
+ """Placeholder for run-scoped attachments applied mid-run."""
45
+
46
+ def as_payload(self) -> Mapping[str, object]: ...
@@ -0,0 +1,10 @@
1
+ """The generated REST surface seam."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+
8
+ @runtime_checkable
9
+ class ApiGateway(Protocol):
10
+ def invoke(self, method_path: str, **kwargs: object) -> object: ...
@@ -0,0 +1,21 @@
1
+ """The out-of-band control plane: commands an operator drops for a live run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+ from typing import Protocol, runtime_checkable
8
+
9
+ from codexloop.domain.control import ControlCommand
10
+
11
+
12
+ @runtime_checkable
13
+ class RunControl(Protocol):
14
+ def poll(self) -> Sequence[ControlCommand]: ...
15
+
16
+
17
+ @runtime_checkable
18
+ class ControlInbox(Protocol):
19
+ """Where an out-of-band command is dropped for a live run to pick up."""
20
+
21
+ def enqueue(self, command: ControlCommand) -> Path: ...
@@ -1,11 +1,15 @@
1
- """Use case: pre-flight doctor checks before a long unattended run."""
1
+ """What `doctor` needs from the outside world, and the vocabulary it answers in.
2
+
3
+ The result types live here alongside the Protocol: they are part of the seam's
4
+ vocabulary, not of the use case that consumes it.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  from collections.abc import Mapping
6
10
  from dataclasses import dataclass, field
7
11
  from pathlib import Path
8
- from typing import Protocol
12
+ from typing import Protocol, runtime_checkable
9
13
 
10
14
 
11
15
  @dataclass(frozen=True, slots=True)
@@ -26,12 +30,6 @@ class DoctorReport:
26
30
  return all(check.passed for check in self.checks)
27
31
 
28
32
 
33
+ @runtime_checkable
29
34
  class DoctorEnvironment(Protocol):
30
35
  def diagnose(self, *, cwd: Path) -> DoctorReport: ...
31
-
32
-
33
- def run_doctor(env: DoctorEnvironment, *, cwd: Path) -> DoctorReport:
34
- return env.diagnose(cwd=cwd)
35
-
36
-
37
- __all__ = ["DoctorCheck", "DoctorEnvironment", "DoctorReport", "run_doctor"]
@@ -0,0 +1,42 @@
1
+ """Everything the run emits outward: logs, audit records, progress, events,
2
+ state publications, usage reads, and operator notifications."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable, Mapping
7
+ from typing import Protocol, runtime_checkable
8
+
9
+
10
+ @runtime_checkable
11
+ class ProgressReporter(Protocol):
12
+ def report(self, event: str, **detail: object) -> None: ...
13
+
14
+
15
+ @runtime_checkable
16
+ class AuditLog(Protocol):
17
+ def append(self, event_type: str, payload: Mapping[str, object]) -> None: ...
18
+
19
+
20
+ @runtime_checkable
21
+ class Notifier(Protocol):
22
+ def notify(self, title: str, body: str) -> None: ...
23
+
24
+
25
+ @runtime_checkable
26
+ class Logger(Protocol):
27
+ def bind(self, **kwargs: object) -> Logger: ...
28
+ def debug(self, event: str, **kwargs: object) -> None: ...
29
+ def info(self, event: str, **kwargs: object) -> None: ...
30
+ def warning(self, event: str, **kwargs: object) -> None: ...
31
+ def error(self, event: str, **kwargs: object) -> None: ...
32
+
33
+
34
+ @runtime_checkable
35
+ class RunEventSink(Protocol):
36
+ def emit(self, event: Mapping[str, object]) -> None: ...
37
+
38
+
39
+ @runtime_checkable
40
+ class StateBus(Protocol):
41
+ def publish(self, event_type: str, payload: Mapping[str, object]) -> None: ...
42
+ def subscribe(self, callback: Callable[[str, Mapping[str, object]], None]) -> None: ...
@@ -0,0 +1,14 @@
1
+ """Permission vocabulary shared across the seam, so vendor CLI flag strings
2
+ never leak up into the application layer."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import StrEnum
7
+
8
+
9
+ class PermissionMode(StrEnum):
10
+ """Autonomy posture at the application boundary — not Codex CLI flag strings."""
11
+
12
+ AUTONOMOUS = "autonomous"
13
+ READ_ONLY = "read_only"
14
+ FULL_ACCESS = "full_access"
@@ -0,0 +1,31 @@
1
+ """Durable run state: the state store, the run lock, git save points, and the
2
+ snapshot sink."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Protocol, runtime_checkable
8
+
9
+
10
+ @runtime_checkable
11
+ class RunStateStore(Protocol):
12
+ def load(self, run_id: str) -> dict[str, object] | None: ...
13
+ def save(self, run_id: str, state: Mapping[str, object]) -> None: ...
14
+
15
+
16
+ @runtime_checkable
17
+ class SessionLock(Protocol):
18
+ def acquire(self, thread_id: str) -> bool: ...
19
+ def release(self, thread_id: str) -> None: ...
20
+
21
+
22
+ @runtime_checkable
23
+ class SavePointStore(Protocol):
24
+ def create(self, run_id: str, label: str) -> str: ...
25
+ def list(self, run_id: str) -> Sequence[str]: ...
26
+ def unwind(self, run_id: str, to: str) -> None: ...
27
+
28
+
29
+ @runtime_checkable
30
+ class RunSnapshotSink(Protocol):
31
+ def write(self, snapshot: Mapping[str, object]) -> None: ...
@@ -0,0 +1,16 @@
1
+ """Time and sleeping -- the two ambient effects the run loop needs faked."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Protocol, runtime_checkable
7
+
8
+
9
+ @runtime_checkable
10
+ class Clock(Protocol):
11
+ def now(self) -> datetime: ...
12
+
13
+
14
+ @runtime_checkable
15
+ class Sleeper(Protocol):
16
+ async def sleep_until(self, when: datetime) -> None: ...
@@ -0,0 +1,61 @@
1
+ """Backwards-compatible re-export of `application.interfaces`.
2
+
3
+ The Protocols moved into `application/interfaces/` so every seam lives in
4
+ one discoverable place, one module per collaborator family. This shim keeps
5
+ the old `from codexloop.application.ports import X` path working; new code
6
+ should import from `codexloop.application.interfaces`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from codexloop.application.interfaces import (
12
+ AgentGateway,
13
+ ApiGateway,
14
+ AuditLog,
15
+ CapacityProbe,
16
+ Clock,
17
+ ControlInbox,
18
+ DoctorCheck,
19
+ DoctorEnvironment,
20
+ DoctorReport,
21
+ Logger,
22
+ Notifier,
23
+ PermissionMode,
24
+ ProgressReporter,
25
+ RunControl,
26
+ RunEventSink,
27
+ RunResources,
28
+ RunSnapshotSink,
29
+ RunStateStore,
30
+ SavePointStore,
31
+ SessionLock,
32
+ Sleeper,
33
+ StateBus,
34
+ ThreadCatalog,
35
+ )
36
+
37
+ __all__ = [
38
+ "AgentGateway",
39
+ "ApiGateway",
40
+ "AuditLog",
41
+ "CapacityProbe",
42
+ "Clock",
43
+ "ControlInbox",
44
+ "DoctorCheck",
45
+ "DoctorEnvironment",
46
+ "DoctorReport",
47
+ "Logger",
48
+ "Notifier",
49
+ "PermissionMode",
50
+ "ProgressReporter",
51
+ "RunControl",
52
+ "RunEventSink",
53
+ "RunResources",
54
+ "RunSnapshotSink",
55
+ "RunStateStore",
56
+ "SavePointStore",
57
+ "SessionLock",
58
+ "Sleeper",
59
+ "StateBus",
60
+ "ThreadCatalog",
61
+ ]
@@ -12,6 +12,7 @@ from codexloop.application.ports import (
12
12
  AgentGateway,
13
13
  CapacityProbe,
14
14
  Clock,
15
+ Logger,
15
16
  Notifier,
16
17
  PermissionMode,
17
18
  ProgressReporter,
@@ -45,6 +46,14 @@ from codexloop.domain.control import (
45
46
  Stop,
46
47
  )
47
48
  from codexloop.domain.error_codes import ErrorClass, classify_code
49
+ from codexloop.domain.forecast import (
50
+ BurnRate,
51
+ WindDown,
52
+ WindDownPolicy,
53
+ forecast,
54
+ should_wind_down,
55
+ )
56
+ from codexloop.domain.handoff_marker import HandoffMarker
48
57
  from codexloop.domain.loop import (
49
58
  BackoffUntil,
50
59
  Drain,
@@ -55,6 +64,7 @@ from codexloop.domain.loop import (
55
64
  RunState,
56
65
  SendTurn,
57
66
  WaitUntil,
67
+ WindDownAndFinish,
58
68
  )
59
69
  from codexloop.domain.model_profile import Effort, ModelEffortProfile
60
70
  from codexloop.domain.plan import WorkPlan
@@ -66,6 +76,26 @@ _FAR_FUTURE = timedelta(days=3650)
66
76
  _ZERO = timedelta(0)
67
77
 
68
78
 
79
+ class _NullLogger:
80
+ """Diagnostics are optional; a run must not require them to be wired."""
81
+
82
+ def bind(self, **kwargs: object) -> _NullLogger:
83
+ del kwargs
84
+ return self
85
+
86
+ def debug(self, event: str, **kwargs: object) -> None:
87
+ del event, kwargs
88
+
89
+ def info(self, event: str, **kwargs: object) -> None:
90
+ del event, kwargs
91
+
92
+ def warning(self, event: str, **kwargs: object) -> None:
93
+ del event, kwargs
94
+
95
+ def error(self, event: str, **kwargs: object) -> None:
96
+ del event, kwargs
97
+
98
+
69
99
  def _discard_artifact(name: str, content: str) -> None:
70
100
  del name, content
71
101
 
@@ -90,6 +120,9 @@ class RunnerContext:
90
120
  )
91
121
  wait_policy: AdaptiveWaitPolicy | None = None
92
122
  max_wait: timedelta | None = None
123
+ logger: Logger | None = None
124
+ handoff_marker_writer: Callable[[HandoffMarker], None] | None = None
125
+ wind_down_policy: WindDownPolicy = field(default_factory=WindDownPolicy)
93
126
  run_id: str = "anonymous"
94
127
  cwd: str = "."
95
128
  model: str = "unknown"
@@ -110,6 +143,9 @@ class AutonomousRunner:
110
143
  self._notifier = ctx.notifier
111
144
  self._reporter = ctx.reporter
112
145
  self._budget = ctx.budget
146
+ self._log = ctx.logger or _NullLogger()
147
+ self._handoff_marker_writer = ctx.handoff_marker_writer
148
+ self._wind_down_policy = ctx.wind_down_policy
113
149
  self._wait_policy = ctx.wait_policy or AdaptiveWaitPolicy(WaitConfig(), rand=lambda: 0.0)
114
150
  self._max_wait = ctx.max_wait
115
151
  self._run_id = ctx.run_id
@@ -188,7 +224,11 @@ class AutonomousRunner:
188
224
  self._record_thread(thread_id, started)
189
225
  wait_attempt = 0
190
226
  self._quota_notified = False
191
- outcome = LoopOutcome(capacity=capacity, completion=completion)
227
+ outcome = LoopOutcome(
228
+ capacity=capacity,
229
+ completion=completion,
230
+ wind_down=self._project_wind_down(capacity),
231
+ )
192
232
  case Probe():
193
233
  probed = await self._probe.probe()
194
234
  outcome = LoopOutcome(capacity=probed.outcome)
@@ -207,6 +247,22 @@ class AutonomousRunner:
207
247
  first_turn_done=not first_turn,
208
248
  plan_text=plan_text,
209
249
  )
250
+ case WindDownAndFinish() as wound_down:
251
+ self._write_stop_summary(thread_id, remaining)
252
+ self._persist(
253
+ key=thread_id,
254
+ remaining=remaining,
255
+ first_turn_done=not first_turn,
256
+ plan_text=plan_text,
257
+ reason=f"wind-down: {wound_down.reason}",
258
+ )
259
+ self._write_handoff_marker(wound_down, thread_id, remaining)
260
+ return RunResult(
261
+ success=False,
262
+ reason=f"wind-down: {wound_down.reason}",
263
+ turns=self._ledger.turns,
264
+ thread_id=thread_id,
265
+ )
210
266
  case Finish() as finish:
211
267
  self._persist(
212
268
  key=thread_id,
@@ -401,6 +457,64 @@ class AutonomousRunner:
401
457
  )
402
458
  self._write_artifact("stop-summary.md", body)
403
459
 
460
+ def _project_wind_down(self, capacity: CapacityState) -> WindDown | None:
461
+ """Forecast remaining capacity, but only while the vendor says we are
462
+ not already blocked.
463
+
464
+ Returning None for every non-Available state is what keeps the plan
465
+ windows from ever influencing whether a turn is *sent*: once a real
466
+ rejection lands, the state machine's capacity routing owns it.
467
+ """
468
+ if not isinstance(capacity, Available):
469
+ return None
470
+ now = self._clock.now()
471
+ turns = self._ledger.turns
472
+ projection = forecast(
473
+ capacity,
474
+ turns_spent=turns,
475
+ max_turns=self._budget.max_turns,
476
+ dollars_spent=self._ledger.dollars,
477
+ max_dollars=self._budget.max_dollars,
478
+ observed=BurnRate(
479
+ turns=turns,
480
+ elapsed_seconds=self._ledger.elapsed.total_seconds(),
481
+ dollars=self._ledger.dollars,
482
+ ),
483
+ capacity_as_of=now,
484
+ now=now,
485
+ policy=self._wind_down_policy,
486
+ )
487
+ self._log.debug(
488
+ "capacity.forecast",
489
+ headroom=projection.binding.fraction,
490
+ source=projection.binding.source,
491
+ turns_until_exhaustion=projection.turns_until_exhaustion,
492
+ )
493
+ return should_wind_down(projection, self._wind_down_policy, turns_spent=turns)
494
+
495
+ def _write_handoff_marker(
496
+ self, decision: WindDownAndFinish, thread_id: str | None, remaining: list[str]
497
+ ) -> None:
498
+ """Written last, after the summary and state it names, so that a marker
499
+ on disk means everything it points at is on disk."""
500
+ if self._handoff_marker_writer is None:
501
+ return
502
+ binding = decision.forecast.binding
503
+ self._handoff_marker_writer(
504
+ HandoffMarker(
505
+ run_id=self._run_id,
506
+ reason=decision.reason,
507
+ produced_at=self._clock.now(),
508
+ headroom=binding.fraction,
509
+ headroom_source=binding.source,
510
+ resets_at=binding.resets_at,
511
+ session_id=thread_id,
512
+ turns_spent=self._ledger.turns,
513
+ dollars_spent=self._ledger.dollars,
514
+ remaining_work=tuple(remaining),
515
+ )
516
+ )
517
+
404
518
 
405
519
  def _permission_mode(sandbox: SandboxMode) -> PermissionMode:
406
520
  match sandbox: