ctrlrelay 0.19.1__tar.gz → 0.20.1__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 (213) hide show
  1. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/CHANGELOG.md +85 -1
  2. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/PKG-INFO +1 -1
  3. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/configuration.md +25 -1
  4. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/pyproject.toml +1 -1
  5. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/cli.py +40 -4
  6. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/config.py +9 -2
  7. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/dispatcher.py +4 -0
  8. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/github.py +13 -0
  9. ctrlrelay-0.20.1/src/ctrlrelay/core/orphans.py +433 -0
  10. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/poller.py +87 -1
  11. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/state.py +202 -7
  12. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/worktree.py +319 -1
  13. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/base.py +5 -0
  14. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/dev.py +779 -148
  15. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/secops.py +10 -3
  16. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/task.py +10 -3
  17. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/fleet.py +5 -0
  18. ctrlrelay-0.20.1/src/ctrlrelay/web/health.py +396 -0
  19. ctrlrelay-0.20.1/src/ctrlrelay/web/links.py +74 -0
  20. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/static/index.html +42 -12
  21. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/status.py +16 -1
  22. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_start.py +114 -0
  23. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_integration.py +12 -0
  24. ctrlrelay-0.20.1/tests/test_dev_plan_step.py +447 -0
  25. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_step_switches.py +0 -1
  26. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_work_steps.py +5 -2
  27. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dispatcher.py +2 -0
  28. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_fleet.py +1 -0
  29. ctrlrelay-0.20.1/tests/test_orphans.py +564 -0
  30. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_poller.py +20 -0
  31. ctrlrelay-0.20.1/tests/test_poller_recheck.py +240 -0
  32. ctrlrelay-0.20.1/tests/test_question_survives_restart.py +606 -0
  33. ctrlrelay-0.20.1/tests/test_timeout_save.py +419 -0
  34. ctrlrelay-0.20.1/tests/test_web_health.py +624 -0
  35. ctrlrelay-0.20.1/tests/test_web_peer_links.py +99 -0
  36. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_server.py +66 -3
  37. ctrlrelay-0.19.1/src/ctrlrelay/web/health.py +0 -167
  38. ctrlrelay-0.19.1/src/ctrlrelay/web/links.py +0 -35
  39. ctrlrelay-0.19.1/tests/test_web_health.py +0 -228
  40. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  41. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  42. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/dependabot.yml +0 -0
  44. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/build.yml +0 -0
  45. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/cla.yml +0 -0
  46. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/pages.yml +0 -0
  47. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/publish.yml +0 -0
  48. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/test.yml +0 -0
  49. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.gitignore +0 -0
  50. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/CODE_OF_CONDUCT.md +0 -0
  51. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/CONTRIBUTING.md +0 -0
  52. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/LICENSE +0 -0
  53. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/README.md +0 -0
  54. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/SECURITY.md +0 -0
  55. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/config/orchestrator.yaml.example +0 -0
  56. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/Gemfile +0 -0
  57. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/_config.yml +0 -0
  58. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/adding-a-machine.md +0 -0
  59. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/architecture.md +0 -0
  60. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/bridge.md +0 -0
  61. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/cli.md +0 -0
  62. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/development.md +0 -0
  63. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/feedback-loop.md +0 -0
  64. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/getting-started.md +0 -0
  65. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/index.md +0 -0
  66. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/llms.txt +0 -0
  67. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/operations.md +0 -0
  68. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/personalization.md +0 -0
  69. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/registered-mode.md +0 -0
  70. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  71. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  72. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/__init__.py +0 -0
  73. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/__init__.py +0 -0
  74. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/__main__.py +0 -0
  75. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/factory.py +0 -0
  76. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/handler.py +0 -0
  77. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  78. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/node_tag.py +0 -0
  79. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/protocol.py +0 -0
  80. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/server.py +0 -0
  81. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  82. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/__init__.py +0 -0
  83. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/agent_env.py +0 -0
  84. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/archived.py +0 -0
  85. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/audit.py +0 -0
  86. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/binaries.py +0 -0
  87. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/checkpoint.py +0 -0
  88. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/claims.py +0 -0
  89. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/code_review.py +0 -0
  90. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/dispatch_health.py +0 -0
  91. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/events.py +0 -0
  92. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/github_budget.py +0 -0
  93. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/heartbeat.py +0 -0
  94. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/job_claims.py +0 -0
  95. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/network.py +0 -0
  96. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/node_check.py +0 -0
  97. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/obs.py +0 -0
  98. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/pr_verifier.py +0 -0
  99. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/pr_watcher.py +0 -0
  100. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/publish.py +0 -0
  101. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/question_expiry.py +0 -0
  102. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/redact.py +0 -0
  103. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/scheduler.py +0 -0
  104. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/usage.py +0 -0
  105. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/dashboard/__init__.py +0 -0
  106. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/dashboard/client.py +0 -0
  107. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/gh_protocol.py +0 -0
  108. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/install.py +0 -0
  109. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/__init__.py +0 -0
  110. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/manager.py +0 -0
  111. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/merge.py +0 -0
  112. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/paths.py +0 -0
  113. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/__init__.py +0 -0
  114. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  115. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/setup.py +0 -0
  116. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/__init__.py +0 -0
  117. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  118. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  119. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  120. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  121. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  122. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  123. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  124. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  125. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/__init__.py +0 -0
  126. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/base.py +0 -0
  127. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/file_mock.py +0 -0
  128. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/socket_client.py +0 -0
  129. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/__init__.py +0 -0
  130. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/numbers.py +0 -0
  131. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/server.py +0 -0
  132. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/transcript.py +0 -0
  133. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/waiting.py +0 -0
  134. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/__init__.py +0 -0
  135. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/_poller_fakes.py +0 -0
  136. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/_publish_fakes.py +0 -0
  137. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/conftest.py +0 -0
  138. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/fixtures/gh_search_issues.json +0 -0
  139. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  140. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/fixtures/transcript_sample.jsonl +0 -0
  141. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_backend_wiring.py +0 -0
  142. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_no_keys.py +0 -0
  143. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_routing.py +0 -0
  144. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_sandbox.py +0 -0
  145. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_archived.py +0 -0
  146. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_audit.py +0 -0
  147. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_binary_resolution.py +0 -0
  148. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_bridge_protocol.py +0 -0
  149. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_bridge_server.py +0 -0
  150. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_bridge_start_command.py +0 -0
  151. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_chat_machines.py +0 -0
  152. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_checkpoint.py +0 -0
  153. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_claim_reconciliation.py +0 -0
  154. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_ci_wait.py +0 -0
  155. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_dev.py +0 -0
  156. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_repos.py +0 -0
  157. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_secops.py +0 -0
  158. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_version.py +0 -0
  159. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_code_review.py +0 -0
  160. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_config.py +0 -0
  161. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dashboard_client.py +0 -0
  162. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_pipeline.py +0 -0
  163. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dispatch_health.py +0 -0
  164. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dispatch_outcome_hook.py +0 -0
  165. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_docs_site.py +0 -0
  166. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_event_emission.py +0 -0
  167. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_events.py +0 -0
  168. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_gh_protocol.py +0 -0
  169. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_github.py +0 -0
  170. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_github_budget.py +0 -0
  171. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_install.py +0 -0
  172. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_issue_outcome_announcement.py +0 -0
  173. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_job_claims.py +0 -0
  174. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_mattermost_handler.py +0 -0
  175. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_network.py +0 -0
  176. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_node_check.py +0 -0
  177. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_node_claims.py +0 -0
  178. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_notify_transport.py +0 -0
  179. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_obs.py +0 -0
  180. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_opencode_adapter.py +0 -0
  181. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_personalization.py +0 -0
  182. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_personalization_merge.py +0 -0
  183. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pipeline_base.py +0 -0
  184. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_poller_search.py +0 -0
  185. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_post_merge.py +0 -0
  186. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pr_verifier.py +0 -0
  187. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pr_watcher.py +0 -0
  188. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pre_dispatch_retry.py +0 -0
  189. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_publish_failure_retry.py +0 -0
  190. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_question_backoff.py +0 -0
  191. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_question_expiry.py +0 -0
  192. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_redact.py +0 -0
  193. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_relay_alert_off_lock.py +0 -0
  194. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_relay_git_263.py +0 -0
  195. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_resume_prompt_contract.py +0 -0
  196. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_review_copy.py +0 -0
  197. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_scheduler.py +0 -0
  198. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_secops_integration.py +0 -0
  199. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_secops_pipeline.py +0 -0
  200. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_setup.py +0 -0
  201. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_state.py +0 -0
  202. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_task_pipeline.py +0 -0
  203. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_telegram_handler.py +0 -0
  204. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_transport.py +0 -0
  205. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_transport_resolution.py +0 -0
  206. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_unreviewed_pr_announcement.py +0 -0
  207. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_usage.py +0 -0
  208. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_links.py +0 -0
  209. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_numbers.py +0 -0
  210. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_status.py +0 -0
  211. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_transcript.py +0 -0
  212. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_waiting.py +0 -0
  213. {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_worktree.py +0 -0
@@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.20.1] - 2026-11-04
11
+
12
+ ### Fixed
13
+
14
+ - The status page's server panel reads memory and uptime on macOS too. It
15
+ read those two from `/proc`, which macOS does not have, so on a Mac those
16
+ rows were empty and the page printed the read error. On macOS it now
17
+ uses `sysctl` and `vm_stat`. macOS has no `MemAvailable`, so "available"
18
+ there is free plus inactive pages: an estimate, and not the same
19
+ measurement as Linux's (#321).
20
+
21
+ - A run waiting for the operator's answer is no longer lost when the poller
22
+ stops during the wait. The question is now saved, and the session marked
23
+ `blocked`, before the wait starts rather than after it ends (if that
24
+ write fails it is logged and the wait goes ahead as before). If the
25
+ poller stops, a later answer in chat reaches the saved question and
26
+ resumes the same agent session; when the poller starts again it drops the
27
+ repo lock such a run held, if the process that ran it is gone, so the
28
+ repo is not wedged until then. An answer that arrives during the wait
29
+ still resumes the run at once and closes the saved question; if a reply
30
+ already answered the saved question first, that first answer stands and
31
+ the run is resumed once, with it. A planning session's question resumes
32
+ the plan, not the code. The claim on the issue is not marked held while the
33
+ question waits, so after a stop it goes stale as before (#324).
34
+
35
+ - An issue is read again from GitHub just before its run starts. A poll
36
+ returns a batch that is handled one issue at a time, so with one slot the
37
+ last issue could start an hour after the poll saw it, after its trigger
38
+ label was removed or the issue was closed. Now an issue that is closed,
39
+ has gained an `exclude_labels` label, or no longer meets any trigger is
40
+ skipped and left for the next poll to judge; so is one that cannot be read.
41
+ The run is routed on the labels read then, not the poll's. Who added a
42
+ trigger is not asked again: a trigger the operator removed and someone
43
+ else put back in that window still counts (#326).
44
+
45
+ ## [0.20.0] - 2026-11-04
46
+
47
+ ### Added
48
+
49
+ - `repos[].steps.plan` (`off`, `on`, `required`; unset is `off`) runs a
50
+ planning session before the dev pipeline's code session. It changes no
51
+ files and writes a plan with numbered acceptance criteria, saved under
52
+ `<paths.contexts>/<owner>-<repo>/plans/issue-<n>.md` (outside the worktree),
53
+ posted on the issue as the criteria list, and added to the code session's
54
+ prompt. A vague issue gets one question instead of a pull request. A plan
55
+ without criteria is a failed plan: `on` reports it and codes without one,
56
+ `required` fails the run (#220).
57
+ - On the status page, a peer machine's name links to that machine's own
58
+ page: in the machine strip, in "Who has what" and on a "Needs you" card.
59
+ The address is the `fleet.peers` entry from this machine's config, cut to
60
+ `<scheme>://<host>[:port]/`; nothing a peer answers changes it, and an
61
+ entry with userinfo or a non-http scheme gives plain text (#319).
62
+
63
+ ### Fixed
64
+
65
+ - A run whose relay process died mid-run (the machine ran out of memory, or
66
+ the service was stopped) no longer stays `running` for ever. Sessions now
67
+ record the process running them (`owner_pid`, `owner_run`) and their claim
68
+ id. When the poller starts, it finds `running` sessions whose process is
69
+ gone, and in whose worktree no process is still working, and marks them
70
+ failed, dropping any repo lock they held. A dev or task issue goes back to
71
+ the next poll unless an open pull request was found for its branch. Where
72
+ the run had claimed the issue, the relay posts a comment that names the
73
+ worktree and releases the claim. The relay deletes nothing in the
74
+ worktree. It detaches the worktree's HEAD, when that can be done safely,
75
+ so the next run can check the issue's branch out. Rows written by an older
76
+ version name no process and are left alone (#312).
77
+ - A dev run whose agent session hits its time limit no longer throws away
78
+ the agent's work. Before the worktree is removed, the files on disk are
79
+ committed on top of the commit it sits on, and that commit, or the
80
+ unpushed commits alone, are pushed to a new `wip/issue-<n>-<run id>`
81
+ branch. The run's own branch is not pushed, no pull request is opened, and
82
+ the local run branch is kept, since it can hold commits the worktree's
83
+ HEAD does not reach. The failure comment on the issue names the wip branch
84
+ and, when it is known, how long the run took. If the work cannot be saved,
85
+ the worktree is kept on disk and the comment names it. A run with nothing
86
+ new loses only its worktree. Files the worktree's `.gitignore` ignores are
87
+ not saved; files that cannot be read are not saved either, and then the
88
+ worktree is kept and the comment says so. The files are read by a git with
89
+ a private git dir, index, config and attributes override, so filters,
90
+ hooks and attributes named by the agent do not take effect (#311).
91
+
10
92
  ## [0.19.1] - 2026-11-03
11
93
 
12
94
  ### Fixed
@@ -2266,7 +2348,9 @@ pipeline).
2266
2348
  per-phase implementation plans (Phase 0 through Phase 4).
2267
2349
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2268
2350
 
2269
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.1...HEAD
2351
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.1...HEAD
2352
+ [0.20.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.0...v0.20.1
2353
+ [0.20.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.1...v0.20.0
2270
2354
  [0.19.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...v0.19.1
2271
2355
  [0.19.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.18.0...v0.19.0
2272
2356
  [0.18.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...v0.18.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.19.1
3
+ Version: 0.20.1
4
4
  Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
5
  Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
6
  Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
@@ -865,7 +865,8 @@ repos:
865
865
 
866
866
  ### repos[].steps
867
867
 
868
- The dev pipeline does its work in a fixed order: **code** (the agent run and
868
+ The dev pipeline does its work in a fixed order: **plan** (an optional
869
+ planning session, off unless asked for), **code** (the agent run and
869
870
  any questions it asks), **publish** (the relay pushes the branch and opens the
870
871
  pull request), **verify** (wait for CI and mergeability, asking the agent to
871
872
  fix what fails, up to `max_fix_attempts` rounds) and **review** (the
@@ -893,6 +894,7 @@ repos:
893
894
 
894
895
  | Step | Accepts | Unset means |
895
896
  |---|---|---|
897
+ | `plan` | `off`, `on`, `required` | `off`: no planning session. |
896
898
  | `code` | `required` | A failed or crashed agent run stops the run. Same as `required`. |
897
899
  | `publish` | `required` | A refused or failed push stops the run (the branch is kept). Same as `required`. |
898
900
  | `verify` | `off`, `required` | CI still red after the fix rounds stops the run. CI that is still pending when the wait times out hands the PR over. |
@@ -905,6 +907,28 @@ PR as ready while its CI is red, with no line in the announcement saying so.
905
907
  Unknown step names (a typo such as `verfy`) are refused as well. A bare `on`
906
908
  or `off` works unquoted.
907
909
 
910
+ **`plan: on`** or **`plan: required`** runs one planning session in the
911
+ run's worktree before the code session. It is told to read the issue and the
912
+ repo and change no files, and returns a plan with four headings: `## Change`,
913
+ `## Files`, `## Risks` and `## Acceptance criteria`, a numbered list of items
914
+ that can each be checked by a command or by reading the diff. The relay then:
915
+
916
+ - saves the plan to `<paths.contexts>/<owner>-<repo>/plans/issue-<n>.md`,
917
+ outside the worktree, so it is never committed;
918
+ - posts the criteria list as one comment on the issue;
919
+ - adds the plan to the code session's prompt under `## Plan`, telling the
920
+ agent the criteria are what "done" means.
921
+
922
+ If the issue is too vague to plan, the planning session asks one question,
923
+ through the same chat loop the code session uses. No code session runs and no
924
+ PR is opened until it is answered; an answer that arrives after the run
925
+ stopped resumes the planning session, then codes with the plan.
926
+
927
+ A plan with no numbered acceptance criteria is a failed plan, as is a
928
+ planning session that fails or crashes. With `on` the failure is posted on the
929
+ issue and the code session runs without a plan; with `required` the run fails
930
+ and no code session runs.
931
+
908
932
  **`verify: required`** also fails the run when the CI wait times out, before
909
933
  or after a fix round. (Checks that cannot be read at all fail the run in every
910
934
  mode that waits on CI, not only under `required`.)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrlrelay"
3
- version = "0.19.1"
3
+ version = "0.20.1"
4
4
  description = "Local-first orchestrator for headless coding agents across multiple GitHub repos"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -3801,7 +3801,8 @@ def poller_start(
3801
3801
  )
3802
3802
  try:
3803
3803
  state_db.mark_pending_resume_resumed(
3804
- session_id
3804
+ session_id, answered_at=row["answered_at"],
3805
+ answer=row["answer"],
3805
3806
  )
3806
3807
  except Exception:
3807
3808
  pass
@@ -3844,7 +3845,8 @@ def poller_start(
3844
3845
  )
3845
3846
  try:
3846
3847
  state_db.mark_pending_resume_resumed(
3847
- session_id
3848
+ session_id, answered_at=row["answered_at"],
3849
+ answer=row["answer"],
3848
3850
  )
3849
3851
  except Exception:
3850
3852
  pass
@@ -3862,7 +3864,10 @@ def poller_start(
3862
3864
  # same broken row. Operator can inspect via the
3863
3865
  # sessions table.
3864
3866
  try:
3865
- state_db.mark_pending_resume_resumed(session_id)
3867
+ state_db.mark_pending_resume_resumed(
3868
+ session_id, answered_at=row["answered_at"],
3869
+ answer=row["answer"],
3870
+ )
3866
3871
  except Exception:
3867
3872
  pass
3868
3873
  continue
@@ -3926,7 +3931,10 @@ def poller_start(
3926
3931
  continue
3927
3932
 
3928
3933
  try:
3929
- state_db.mark_pending_resume_resumed(session_id)
3934
+ state_db.mark_pending_resume_resumed(
3935
+ session_id, answered_at=row["answered_at"],
3936
+ answer=row["answer"],
3937
+ )
3930
3938
  except Exception:
3931
3939
  pass
3932
3940
 
@@ -4203,6 +4211,34 @@ def poller_start(
4203
4211
  skip_initial_persist=True,
4204
4212
  )
4205
4213
 
4214
+ # Runs whose process died with them (#312): settle them before
4215
+ # the first poll, so the issues they held are taken again by
4216
+ # that poll rather than skipped as already seen.
4217
+ from ctrlrelay.core.orphans import reap_orphaned_sessions
4218
+ reaped = await reap_orphaned_sessions(
4219
+ state_db, poller=poller, github=github, worktree=worktree,
4220
+ )
4221
+ for r in reaped:
4222
+ where = (
4223
+ f"#{r['issue_number']} in {r['repo']}"
4224
+ if r["issue_number"] is not None else r["repo"]
4225
+ )
4226
+ kept = (
4227
+ f" Its worktree is kept at {r['worktree_path']}."
4228
+ if r["worktree_path"]
4229
+ and Path(r["worktree_path"]).exists() else ""
4230
+ )
4231
+ console.print(
4232
+ f"[yellow]Run {r['session_id']} for {where} stopped "
4233
+ f"when the relay did; marked failed.{kept}[/yellow]"
4234
+ )
4235
+ # A run that stopped while waiting for an answer (#324) is
4236
+ # `blocked` with its question saved, and is resumed when the
4237
+ # answer comes; the repo lock it held would only wedge the
4238
+ # repo until then.
4239
+ from ctrlrelay.core.orphans import release_dead_blocked_locks
4240
+ release_dead_blocked_locks(state_db)
4241
+
4206
4242
  try:
4207
4243
  await github_budget.refresh()
4208
4244
  await run_poll_loop(
@@ -507,6 +507,9 @@ _REPO_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
507
507
  # reads this table as well, so a caller that bypasses the config cannot
508
508
  # get a mode accepted and ignored.
509
509
  DEV_STEP_MODES: dict[str, tuple[str, ...]] = {
510
+ # ``plan`` (#220) runs before ``code``. Unset is ``off``: a repo that
511
+ # does not ask for a plan runs exactly as it did before the step.
512
+ "plan": ("off", "on", "required"),
510
513
  "code": ("required",),
511
514
  "publish": ("required",),
512
515
  "verify": ("off", "required"),
@@ -514,6 +517,7 @@ DEV_STEP_MODES: dict[str, tuple[str, ...]] = {
514
517
  }
515
518
 
516
519
  _STEP_REFUSALS: dict[str, str] = {
520
+ "plan": "",
517
521
  "code": (
518
522
  "a dev run without the agent run has nothing to publish"
519
523
  ),
@@ -567,7 +571,7 @@ class DevStepsConfig(BaseModel):
567
571
  ``verify`` stops it when CI is still red after the fix rounds, but
568
572
  hands the pull request over when the CI wait times out; ``review``
569
573
  reports a review that did not post and hands the pull request over
570
- anyway (``on``).
574
+ anyway (``on``); ``plan`` does not run (``off``).
571
575
 
572
576
  Unknown keys are refused, so a typo such as ``verfy: required``
573
577
  cannot load as "unset".
@@ -575,12 +579,15 @@ class DevStepsConfig(BaseModel):
575
579
 
576
580
  model_config = {"extra": "forbid"}
577
581
 
582
+ plan: str | None = None
578
583
  code: str | None = None
579
584
  publish: str | None = None
580
585
  verify: str | None = None
581
586
  review: str | None = None
582
587
 
583
- @field_validator("code", "publish", "verify", "review", mode="before")
588
+ @field_validator(
589
+ "plan", "code", "publish", "verify", "review", mode="before"
590
+ )
584
591
  @classmethod
585
592
  def _known_mode(cls, v: Any, info: Any) -> str | None:
586
593
  return check_dev_step_mode(info.field_name, v)
@@ -133,6 +133,9 @@ class SessionResult:
133
133
  stdout: str = ""
134
134
  stderr: str = ""
135
135
  agent_session_id: str | None = None
136
+ # The relay killed the agent at its time limit (#311). Set only by the
137
+ # relay's own timeout path, never parsed from anything the agent wrote.
138
+ timed_out: bool = False
136
139
 
137
140
  @property
138
141
  def success(self) -> bool:
@@ -352,6 +355,7 @@ class CliAgentAdapter:
352
355
  exit_code=-1,
353
356
  state=None,
354
357
  stderr="Session timed out",
358
+ timed_out=True,
355
359
  )
356
360
  except asyncio.CancelledError as e:
357
361
  # Scheduler/shutdown cancel: kill the child BEFORE re-raising
@@ -561,6 +561,19 @@ class GitHubCLI:
561
561
  )
562
562
  return json.loads(output)
563
563
 
564
+ async def get_issue_gate(self, repo: str, issue_number: int) -> dict[str, Any]:
565
+ """Return ``{"state", "labels", "assignees"}`` for one issue, read
566
+ live over REST: the fields that decide whether the relay may still
567
+ start it (#326). ``labels`` and ``assignees`` are lists of names.
568
+ """
569
+ output = await self._run_gh(
570
+ "api", f"repos/{repo}/issues/{issue_number}",
571
+ "--jq",
572
+ "{state: .state, labels: [.labels[].name], "
573
+ "assignees: [.assignees[].login]}",
574
+ )
575
+ return json.loads(output)
576
+
564
577
  async def create_pr(
565
578
  self,
566
579
  repo: str,