ctrlrelay 0.19.0__tar.gz → 0.20.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 (214) hide show
  1. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/CHANGELOG.md +59 -1
  2. ctrlrelay-0.20.0/PKG-INFO +164 -0
  3. ctrlrelay-0.20.0/README.md +123 -0
  4. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/_config.yml +7 -2
  5. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/adding-a-machine.md +9 -6
  6. ctrlrelay-0.20.0/docs/architecture.md +231 -0
  7. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/bridge.md +115 -87
  8. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/cli.md +32 -21
  9. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/configuration.md +45 -16
  10. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/development.md +18 -15
  11. ctrlrelay-0.20.0/docs/feedback-loop.md +209 -0
  12. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/getting-started.md +32 -21
  13. ctrlrelay-0.20.0/docs/index.md +89 -0
  14. ctrlrelay-0.20.0/docs/llms.txt +36 -0
  15. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/operations.md +17 -10
  16. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/pyproject.toml +1 -1
  17. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/cli.py +22 -0
  18. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/config.py +9 -2
  19. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/dispatcher.py +4 -0
  20. ctrlrelay-0.20.0/src/ctrlrelay/core/orphans.py +388 -0
  21. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/state.py +65 -1
  22. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/worktree.py +319 -1
  23. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/merge.py +37 -2
  24. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/base.py +5 -0
  25. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/dev.py +653 -136
  26. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/secops.py +10 -3
  27. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/task.py +10 -3
  28. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/fleet.py +5 -0
  29. ctrlrelay-0.20.0/src/ctrlrelay/web/links.py +74 -0
  30. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/static/index.html +42 -12
  31. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_start.py +54 -0
  32. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_integration.py +4 -0
  33. ctrlrelay-0.20.0/tests/test_dev_plan_step.py +395 -0
  34. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_step_switches.py +0 -1
  35. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_work_steps.py +5 -2
  36. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dispatcher.py +2 -0
  37. ctrlrelay-0.20.0/tests/test_orphans.py +564 -0
  38. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_personalization_merge.py +39 -21
  39. ctrlrelay-0.20.0/tests/test_timeout_save.py +419 -0
  40. ctrlrelay-0.20.0/tests/test_web_peer_links.py +99 -0
  41. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_server.py +66 -3
  42. ctrlrelay-0.19.0/PKG-INFO +0 -263
  43. ctrlrelay-0.19.0/README.md +0 -222
  44. ctrlrelay-0.19.0/docs/architecture.md +0 -222
  45. ctrlrelay-0.19.0/docs/feedback-loop.md +0 -197
  46. ctrlrelay-0.19.0/docs/index.md +0 -62
  47. ctrlrelay-0.19.0/src/ctrlrelay/web/links.py +0 -35
  48. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  49. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  50. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  51. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/dependabot.yml +0 -0
  52. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/build.yml +0 -0
  53. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/cla.yml +0 -0
  54. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/pages.yml +0 -0
  55. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/publish.yml +0 -0
  56. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/test.yml +0 -0
  57. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.gitignore +0 -0
  58. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/CODE_OF_CONDUCT.md +0 -0
  59. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/CONTRIBUTING.md +0 -0
  60. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/LICENSE +0 -0
  61. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/SECURITY.md +0 -0
  62. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/config/orchestrator.yaml.example +0 -0
  63. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/Gemfile +0 -0
  64. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/personalization.md +0 -0
  65. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/registered-mode.md +0 -0
  66. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  67. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  68. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/__init__.py +0 -0
  69. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  70. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/__main__.py +0 -0
  71. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/factory.py +0 -0
  72. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/handler.py +0 -0
  73. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  74. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/node_tag.py +0 -0
  75. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  76. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/server.py +0 -0
  77. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  78. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/__init__.py +0 -0
  79. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/agent_env.py +0 -0
  80. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/archived.py +0 -0
  81. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/audit.py +0 -0
  82. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/binaries.py +0 -0
  83. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/checkpoint.py +0 -0
  84. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/claims.py +0 -0
  85. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/code_review.py +0 -0
  86. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
  87. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/events.py +0 -0
  88. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/github.py +0 -0
  89. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/github_budget.py +0 -0
  90. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/heartbeat.py +0 -0
  91. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/job_claims.py +0 -0
  92. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/network.py +0 -0
  93. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/node_check.py +0 -0
  94. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/obs.py +0 -0
  95. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/poller.py +0 -0
  96. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  97. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  98. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/publish.py +0 -0
  99. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/question_expiry.py +0 -0
  100. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/redact.py +0 -0
  101. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/scheduler.py +0 -0
  102. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/usage.py +0 -0
  103. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  104. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/dashboard/client.py +0 -0
  105. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/gh_protocol.py +0 -0
  106. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/install.py +0 -0
  107. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/__init__.py +0 -0
  108. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/manager.py +0 -0
  109. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/paths.py +0 -0
  110. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  111. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  112. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/setup.py +0 -0
  113. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/__init__.py +0 -0
  114. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  115. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  116. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  117. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  118. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  119. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  120. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  121. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  122. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/__init__.py +0 -0
  123. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/base.py +0 -0
  124. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/file_mock.py +0 -0
  125. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/socket_client.py +0 -0
  126. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/__init__.py +0 -0
  127. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/health.py +0 -0
  128. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/numbers.py +0 -0
  129. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/server.py +0 -0
  130. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/status.py +0 -0
  131. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/transcript.py +0 -0
  132. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/waiting.py +0 -0
  133. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/__init__.py +0 -0
  134. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/_poller_fakes.py +0 -0
  135. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/_publish_fakes.py +0 -0
  136. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/conftest.py +0 -0
  137. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/fixtures/gh_search_issues.json +0 -0
  138. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  139. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/fixtures/transcript_sample.jsonl +0 -0
  140. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_backend_wiring.py +0 -0
  141. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_no_keys.py +0 -0
  142. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_routing.py +0 -0
  143. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_sandbox.py +0 -0
  144. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_archived.py +0 -0
  145. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_audit.py +0 -0
  146. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_binary_resolution.py +0 -0
  147. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_bridge_protocol.py +0 -0
  148. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_bridge_server.py +0 -0
  149. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_bridge_start_command.py +0 -0
  150. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_chat_machines.py +0 -0
  151. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_checkpoint.py +0 -0
  152. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_claim_reconciliation.py +0 -0
  153. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_ci_wait.py +0 -0
  154. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_dev.py +0 -0
  155. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_repos.py +0 -0
  156. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_secops.py +0 -0
  157. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_version.py +0 -0
  158. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_code_review.py +0 -0
  159. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_config.py +0 -0
  160. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dashboard_client.py +0 -0
  161. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_pipeline.py +0 -0
  162. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dispatch_health.py +0 -0
  163. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dispatch_outcome_hook.py +0 -0
  164. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_docs_site.py +0 -0
  165. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_event_emission.py +0 -0
  166. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_events.py +0 -0
  167. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_fleet.py +0 -0
  168. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_gh_protocol.py +0 -0
  169. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_github.py +0 -0
  170. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_github_budget.py +0 -0
  171. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_install.py +0 -0
  172. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_issue_outcome_announcement.py +0 -0
  173. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_job_claims.py +0 -0
  174. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_mattermost_handler.py +0 -0
  175. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_network.py +0 -0
  176. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_node_check.py +0 -0
  177. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_node_claims.py +0 -0
  178. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_notify_transport.py +0 -0
  179. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_obs.py +0 -0
  180. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_opencode_adapter.py +0 -0
  181. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_personalization.py +0 -0
  182. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pipeline_base.py +0 -0
  183. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_poller.py +0 -0
  184. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_poller_search.py +0 -0
  185. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_post_merge.py +0 -0
  186. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pr_verifier.py +0 -0
  187. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pr_watcher.py +0 -0
  188. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pre_dispatch_retry.py +0 -0
  189. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_publish_failure_retry.py +0 -0
  190. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_question_backoff.py +0 -0
  191. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_question_expiry.py +0 -0
  192. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_redact.py +0 -0
  193. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_relay_alert_off_lock.py +0 -0
  194. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_relay_git_263.py +0 -0
  195. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_resume_prompt_contract.py +0 -0
  196. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_review_copy.py +0 -0
  197. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_scheduler.py +0 -0
  198. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_secops_integration.py +0 -0
  199. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_secops_pipeline.py +0 -0
  200. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_setup.py +0 -0
  201. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_state.py +0 -0
  202. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_task_pipeline.py +0 -0
  203. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_telegram_handler.py +0 -0
  204. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_transport.py +0 -0
  205. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_transport_resolution.py +0 -0
  206. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_unreviewed_pr_announcement.py +0 -0
  207. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_usage.py +0 -0
  208. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_health.py +0 -0
  209. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_links.py +0 -0
  210. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_numbers.py +0 -0
  211. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_status.py +0 -0
  212. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_transcript.py +0 -0
  213. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_waiting.py +0 -0
  214. {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_worktree.py +0 -0
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.20.0] - 2026-11-04
11
+
12
+ ### Added
13
+
14
+ - `repos[].steps.plan` (`off`, `on`, `required`; unset is `off`) runs a
15
+ planning session before the dev pipeline's code session. It changes no
16
+ files and writes a plan with numbered acceptance criteria, saved under
17
+ `<paths.contexts>/<owner>-<repo>/plans/issue-<n>.md` (outside the worktree),
18
+ posted on the issue as the criteria list, and added to the code session's
19
+ prompt. A vague issue gets one question instead of a pull request. A plan
20
+ without criteria is a failed plan: `on` reports it and codes without one,
21
+ `required` fails the run (#220).
22
+ - On the status page, a peer machine's name links to that machine's own
23
+ page: in the machine strip, in "Who has what" and on a "Needs you" card.
24
+ The address is the `fleet.peers` entry from this machine's config, cut to
25
+ `<scheme>://<host>[:port]/`; nothing a peer answers changes it, and an
26
+ entry with userinfo or a non-http scheme gives plain text (#319).
27
+
28
+ ### Fixed
29
+
30
+ - A run whose relay process died mid-run (the machine ran out of memory, or
31
+ the service was stopped) no longer stays `running` for ever. Sessions now
32
+ record the process running them (`owner_pid`, `owner_run`) and their claim
33
+ id. When the poller starts, it finds `running` sessions whose process is
34
+ gone, and in whose worktree no process is still working, and marks them
35
+ failed, dropping any repo lock they held. A dev or task issue goes back to
36
+ the next poll unless an open pull request was found for its branch. Where
37
+ the run had claimed the issue, the relay posts a comment that names the
38
+ worktree and releases the claim. The relay deletes nothing in the
39
+ worktree. It detaches the worktree's HEAD, when that can be done safely,
40
+ so the next run can check the issue's branch out. Rows written by an older
41
+ version name no process and are left alone (#312).
42
+ - A dev run whose agent session hits its time limit no longer throws away
43
+ the agent's work. Before the worktree is removed, the files on disk are
44
+ committed on top of the commit it sits on, and that commit, or the
45
+ unpushed commits alone, are pushed to a new `wip/issue-<n>-<run id>`
46
+ branch. The run's own branch is not pushed, no pull request is opened, and
47
+ the local run branch is kept, since it can hold commits the worktree's
48
+ HEAD does not reach. The failure comment on the issue names the wip branch
49
+ and, when it is known, how long the run took. If the work cannot be saved,
50
+ the worktree is kept on disk and the comment names it. A run with nothing
51
+ new loses only its worktree. Files the worktree's `.gitignore` ignores are
52
+ not saved; files that cannot be read are not saved either, and then the
53
+ worktree is kept and the comment says so. The files are read by a git with
54
+ a private git dir, index, config and attributes override, so filters,
55
+ hooks and attributes named by the agent do not take effect (#311).
56
+
57
+ ## [0.19.1] - 2026-11-03
58
+
59
+ ### Fixed
60
+
61
+ - A synced or live `settings.json` nested more than 64 levels deep is now
62
+ refused right after it is read. On Python 3.14 the parser accepts about
63
+ 65,000 levels, and writing that back out used all the memory on the
64
+ machine instead of failing (#313).
65
+
10
66
  ## [0.19.0] - 2026-11-03
11
67
 
12
68
  ### Added
@@ -2257,7 +2313,9 @@ pipeline).
2257
2313
  per-phase implementation plans (Phase 0 through Phase 4).
2258
2314
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2259
2315
 
2260
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...HEAD
2316
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.0...HEAD
2317
+ [0.20.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.1...v0.20.0
2318
+ [0.19.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...v0.19.1
2261
2319
  [0.19.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.18.0...v0.19.0
2262
2320
  [0.18.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...v0.18.0
2263
2321
  [0.17.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.0...v0.17.1
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.5
2
+ Name: ctrlrelay
3
+ Version: 0.20.0
4
+ Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
+ Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
+ Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
7
+ Project-URL: Repository, https://github.com/AInvirion/ctrlrelay
8
+ Project-URL: Issues, https://github.com/AInvirion/ctrlrelay/issues
9
+ Project-URL: Changelog, https://github.com/AInvirion/ctrlrelay/blob/main/CHANGELOG.md
10
+ Author-email: Oscar Valenzuela <oscar.valenzuela.b@gmail.com>
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ Keywords: automation,claude,github,orchestrator
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Classifier: Topic :: Software Development :: Version Control :: Git
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: apscheduler>=3.10.0
28
+ Requires-Dist: httpx>=0.27.0
29
+ Requires-Dist: pydantic>=2.0.0
30
+ Requires-Dist: python-telegram-bot>=21.0
31
+ Requires-Dist: pyyaml>=6.0.0
32
+ Requires-Dist: rich>=13.0.0
33
+ Requires-Dist: typer>=0.12.0
34
+ Requires-Dist: websockets>=12.0
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
37
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
38
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
39
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # ctrlrelay
43
+
44
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
45
+ [![PyPI](https://img.shields.io/pypi/v/ctrlrelay.svg)](https://pypi.org/project/ctrlrelay/)
46
+ [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/ctrlrelay/)
47
+ [![Tests and lint](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml/badge.svg)](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml)
48
+
49
+ **Assign an issue. Get a reviewed pull request. Answer questions from your phone.**
50
+
51
+ ctrlrelay turns the coding agent you already use into a teammate that works
52
+ your GitHub backlog in the background. It picks up issues, writes the code in
53
+ a clean branch, opens a pull request, waits for CI, gets the change reviewed,
54
+ and pings you on **Telegram** or **Mattermost** only when it truly needs you.
55
+
56
+ It runs on your own machine. Your code, your credentials, your agent
57
+ subscription. No hosted service in the middle.
58
+
59
+ **[Documentation](https://ainvirion.github.io/ctrlrelay/)** ·
60
+ **[Get started](https://ainvirion.github.io/ctrlrelay/getting-started/)** ·
61
+ **[Why ctrlrelay](https://ainvirion.github.io/ctrlrelay/#why-ctrlrelay)**
62
+
63
+ ## Why ctrlrelay
64
+
65
+ Coding agents are great when you sit and watch them. ctrlrelay is for the
66
+ work you don't want to sit and watch.
67
+
68
+ - **Your backlog is the to-do list.** No new tool to learn. Assign an issue
69
+ (or add a label) and the work starts. The result is a normal pull request
70
+ your team already knows how to review.
71
+ - **Runs where your code already lives.** ctrlrelay is a small program on your
72
+ laptop or server — not a cloud service. Nothing leaves your machine except
73
+ the calls your agent and GitHub already make. No per-seat platform fee on
74
+ top of the agent you already pay for.
75
+ - **It asks instead of guessing.** When the agent is stuck, you get the
76
+ question in chat. You reply from your phone and the *same* session picks up
77
+ where it stopped.
78
+ - **Safe by default.** Every issue gets its own isolated copy of the repo.
79
+ One job per repo at a time. Optional Linux sandbox that hides your secrets
80
+ from the agent. Nothing merges without your rules allowing it.
81
+ - **Done means done.** It waits for CI, fixes failing checks, and posts an
82
+ independent review on the pull request — so what reaches you is ready to
83
+ read, not a first draft.
84
+ - **Pick your agent per repo.** Claude Code today, or OpenCode — which can
85
+ run on a local model — for the repos where that fits.
86
+ - **Grows with you.** Start on one laptop. Add more machines later; they share
87
+ the work without stepping on each other.
88
+ - **Open source.** Apache 2.0. Read every line of what runs on your machine.
89
+
90
+ ## What it does
91
+
92
+ - **Issues → pull requests.** Watches every repo you choose and works each new
93
+ issue end to end.
94
+ - **Questions in your chat.** Telegram (a private bot) or Mattermost (a channel
95
+ on your own server).
96
+ - **Automatic review.** Every agent pull request gets a second-opinion review
97
+ comment before you look at it.
98
+ - **Daily security sweep.** Reviews Dependabot alerts and pull requests across
99
+ your repos every morning.
100
+ - **Status page.** See what each agent is doing, what is waiting, and what
101
+ needs you — at a glance.
102
+ - **Many machines, one account.** Spread work across machines, with one chat
103
+ and one status page.
104
+ - **Settings follow you.** Optional sync of your agent setup across machines
105
+ through a private GitHub repo.
106
+
107
+ ## How it works
108
+
109
+ ```mermaid
110
+ flowchart LR
111
+ GH[("GitHub<br/>issues")] -->|new issue| CR["ctrlrelay<br/>on your machine"]
112
+ CR -->|works in a clean branch| AG["Coding agent<br/>Claude Code · OpenCode"]
113
+ AG -->|stuck?| CH["Your chat<br/>Telegram · Mattermost"]
114
+ CH -->|your answer| AG
115
+ AG -->|opens| PR["Pull request"]
116
+ PR -->|CI + review| YOU(["You merge"])
117
+ ```
118
+
119
+ 1. **You assign an issue** to yourself, or add a label you pick.
120
+ 2. **ctrlrelay starts the agent** in a fresh, isolated copy of the repo.
121
+ 3. **If the agent has a question**, it lands in your chat. Your reply goes
122
+ straight back to the same session.
123
+ 4. **A pull request opens.** ctrlrelay waits for CI, fixes what fails, and
124
+ posts a review.
125
+ 5. **You merge** — or ask for changes, like with any teammate.
126
+
127
+ ## Quick start
128
+
129
+ ```bash
130
+ pipx install ctrlrelay
131
+ ctrlrelay setup # finds your repos and writes your config
132
+ ctrlrelay poller start # starts working assigned issues
133
+ ```
134
+
135
+ You need Python 3.12+, the GitHub CLI, and a coding agent CLI. The
136
+ **[getting started guide](https://ainvirion.github.io/ctrlrelay/getting-started/)**
137
+ covers prerequisites, chat setup and running it as a background service.
138
+
139
+ ## Documentation
140
+
141
+ Everything lives at **[ainvirion.github.io/ctrlrelay](https://ainvirion.github.io/ctrlrelay/)**:
142
+
143
+ - [Getting started](https://ainvirion.github.io/ctrlrelay/getting-started/) — install and first run
144
+ - [Chat bridge](https://ainvirion.github.io/ctrlrelay/bridge/) — set up Telegram or Mattermost
145
+ - [Configuration](https://ainvirion.github.io/ctrlrelay/configuration/) — every setting
146
+ - [Operations](https://ainvirion.github.io/ctrlrelay/operations/) — background services, status page, several machines
147
+ - [CLI reference](https://ainvirion.github.io/ctrlrelay/cli/) — every command
148
+
149
+ ## Contributing
150
+
151
+ Contributions are welcome. Read the [contributing guide](CONTRIBUTING.md) and
152
+ the [code of conduct](CODE_OF_CONDUCT.md). First-time contributors sign a
153
+ one-time Contributor Assignment Agreement in the pull request.
154
+
155
+ ## Security
156
+
157
+ Found a vulnerability? Please follow the [security policy](SECURITY.md) and open
158
+ a private advisory — not a public issue.
159
+
160
+ ## License
161
+
162
+ Apache License 2.0 — see [LICENSE](LICENSE).
163
+
164
+ Copyright (c) 2026 AInvirion LLC. All Rights Reserved.
@@ -0,0 +1,123 @@
1
+ # ctrlrelay
2
+
3
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+ [![PyPI](https://img.shields.io/pypi/v/ctrlrelay.svg)](https://pypi.org/project/ctrlrelay/)
5
+ [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/ctrlrelay/)
6
+ [![Tests and lint](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml/badge.svg)](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml)
7
+
8
+ **Assign an issue. Get a reviewed pull request. Answer questions from your phone.**
9
+
10
+ ctrlrelay turns the coding agent you already use into a teammate that works
11
+ your GitHub backlog in the background. It picks up issues, writes the code in
12
+ a clean branch, opens a pull request, waits for CI, gets the change reviewed,
13
+ and pings you on **Telegram** or **Mattermost** only when it truly needs you.
14
+
15
+ It runs on your own machine. Your code, your credentials, your agent
16
+ subscription. No hosted service in the middle.
17
+
18
+ **[Documentation](https://ainvirion.github.io/ctrlrelay/)** ·
19
+ **[Get started](https://ainvirion.github.io/ctrlrelay/getting-started/)** ·
20
+ **[Why ctrlrelay](https://ainvirion.github.io/ctrlrelay/#why-ctrlrelay)**
21
+
22
+ ## Why ctrlrelay
23
+
24
+ Coding agents are great when you sit and watch them. ctrlrelay is for the
25
+ work you don't want to sit and watch.
26
+
27
+ - **Your backlog is the to-do list.** No new tool to learn. Assign an issue
28
+ (or add a label) and the work starts. The result is a normal pull request
29
+ your team already knows how to review.
30
+ - **Runs where your code already lives.** ctrlrelay is a small program on your
31
+ laptop or server — not a cloud service. Nothing leaves your machine except
32
+ the calls your agent and GitHub already make. No per-seat platform fee on
33
+ top of the agent you already pay for.
34
+ - **It asks instead of guessing.** When the agent is stuck, you get the
35
+ question in chat. You reply from your phone and the *same* session picks up
36
+ where it stopped.
37
+ - **Safe by default.** Every issue gets its own isolated copy of the repo.
38
+ One job per repo at a time. Optional Linux sandbox that hides your secrets
39
+ from the agent. Nothing merges without your rules allowing it.
40
+ - **Done means done.** It waits for CI, fixes failing checks, and posts an
41
+ independent review on the pull request — so what reaches you is ready to
42
+ read, not a first draft.
43
+ - **Pick your agent per repo.** Claude Code today, or OpenCode — which can
44
+ run on a local model — for the repos where that fits.
45
+ - **Grows with you.** Start on one laptop. Add more machines later; they share
46
+ the work without stepping on each other.
47
+ - **Open source.** Apache 2.0. Read every line of what runs on your machine.
48
+
49
+ ## What it does
50
+
51
+ - **Issues → pull requests.** Watches every repo you choose and works each new
52
+ issue end to end.
53
+ - **Questions in your chat.** Telegram (a private bot) or Mattermost (a channel
54
+ on your own server).
55
+ - **Automatic review.** Every agent pull request gets a second-opinion review
56
+ comment before you look at it.
57
+ - **Daily security sweep.** Reviews Dependabot alerts and pull requests across
58
+ your repos every morning.
59
+ - **Status page.** See what each agent is doing, what is waiting, and what
60
+ needs you — at a glance.
61
+ - **Many machines, one account.** Spread work across machines, with one chat
62
+ and one status page.
63
+ - **Settings follow you.** Optional sync of your agent setup across machines
64
+ through a private GitHub repo.
65
+
66
+ ## How it works
67
+
68
+ ```mermaid
69
+ flowchart LR
70
+ GH[("GitHub<br/>issues")] -->|new issue| CR["ctrlrelay<br/>on your machine"]
71
+ CR -->|works in a clean branch| AG["Coding agent<br/>Claude Code · OpenCode"]
72
+ AG -->|stuck?| CH["Your chat<br/>Telegram · Mattermost"]
73
+ CH -->|your answer| AG
74
+ AG -->|opens| PR["Pull request"]
75
+ PR -->|CI + review| YOU(["You merge"])
76
+ ```
77
+
78
+ 1. **You assign an issue** to yourself, or add a label you pick.
79
+ 2. **ctrlrelay starts the agent** in a fresh, isolated copy of the repo.
80
+ 3. **If the agent has a question**, it lands in your chat. Your reply goes
81
+ straight back to the same session.
82
+ 4. **A pull request opens.** ctrlrelay waits for CI, fixes what fails, and
83
+ posts a review.
84
+ 5. **You merge** — or ask for changes, like with any teammate.
85
+
86
+ ## Quick start
87
+
88
+ ```bash
89
+ pipx install ctrlrelay
90
+ ctrlrelay setup # finds your repos and writes your config
91
+ ctrlrelay poller start # starts working assigned issues
92
+ ```
93
+
94
+ You need Python 3.12+, the GitHub CLI, and a coding agent CLI. The
95
+ **[getting started guide](https://ainvirion.github.io/ctrlrelay/getting-started/)**
96
+ covers prerequisites, chat setup and running it as a background service.
97
+
98
+ ## Documentation
99
+
100
+ Everything lives at **[ainvirion.github.io/ctrlrelay](https://ainvirion.github.io/ctrlrelay/)**:
101
+
102
+ - [Getting started](https://ainvirion.github.io/ctrlrelay/getting-started/) — install and first run
103
+ - [Chat bridge](https://ainvirion.github.io/ctrlrelay/bridge/) — set up Telegram or Mattermost
104
+ - [Configuration](https://ainvirion.github.io/ctrlrelay/configuration/) — every setting
105
+ - [Operations](https://ainvirion.github.io/ctrlrelay/operations/) — background services, status page, several machines
106
+ - [CLI reference](https://ainvirion.github.io/ctrlrelay/cli/) — every command
107
+
108
+ ## Contributing
109
+
110
+ Contributions are welcome. Read the [contributing guide](CONTRIBUTING.md) and
111
+ the [code of conduct](CODE_OF_CONDUCT.md). First-time contributors sign a
112
+ one-time Contributor Assignment Agreement in the pull request.
113
+
114
+ ## Security
115
+
116
+ Found a vulnerability? Please follow the [security policy](SECURITY.md) and open
117
+ a private advisory — not a public issue.
118
+
119
+ ## License
120
+
121
+ Apache License 2.0 — see [LICENSE](LICENSE).
122
+
123
+ Copyright (c) 2026 AInvirion LLC. All Rights Reserved.
@@ -1,7 +1,8 @@
1
1
  title: ctrlrelay
2
2
  description: >-
3
- Local-first orchestrator for headless coding agents across multiple GitHub
4
- repos — operator guides and architecture reference.
3
+ Assign a GitHub issue, get a reviewed pull request. ctrlrelay runs your
4
+ coding agent in the background and asks you on Telegram or Mattermost
5
+ when it needs you.
5
6
  url: "https://ainvirion.github.io"
6
7
  baseurl: "/ctrlrelay"
7
8
 
@@ -26,6 +27,10 @@ search:
26
27
 
27
28
  heading_anchors: true
28
29
 
30
+ # Render ```mermaid fences as diagrams
31
+ mermaid:
32
+ version: "10.9.1"
33
+
29
34
  # Aux links appear in the top right of the layout
30
35
  aux_links:
31
36
  "GitHub":
@@ -39,10 +39,12 @@ A token copied between machines is revoked for all of them at once.
39
39
 
40
40
  ## 3. Its own agent login
41
41
 
42
- Log the agent CLI in on this machine. Each machine can use its own
43
- subscription; it pauses on its own when its usage nears the limit
44
- (`usage.pause_at`). Two machines on one subscription share one set of usage
45
- numbers and pause together, which is correct.
42
+ Log the agent CLI in on this machine — whichever your repos use (`claude`,
43
+ `opencode` or both, per `agent.type` and `repos[].agent_type`). For Claude Code, each
44
+ machine can use its own subscription; it pauses on its own when its usage
45
+ nears the limit (`usage.pause_at`). Two machines on one subscription share one
46
+ set of usage numbers and pause together, which is correct. OpenCode has no
47
+ usage reading of its own.
46
48
 
47
49
  ## 4. A config with its own `node_id`
48
50
 
@@ -103,8 +105,9 @@ ctrlrelay node check
103
105
  ✓ peer http://node-a:8787: reachable, node-a
104
106
  ```
105
107
 
106
- `--no-agent` skips the agent check, which makes one tiny call on the
107
- subscription. A peer that answers with this machine's own name means two
108
+ `--no-agent` skips the agent check, which makes one tiny `claude` call on the
109
+ subscription. The row is skipped when no repo routes to `claude`, and it never
110
+ checks `opencode`: run that CLI once by hand. A peer that answers with this machine's own name means two
108
111
  machines share a `node_id` (or the URL points back here).
109
112
 
110
113
  ## 8. Start it
@@ -0,0 +1,231 @@
1
+ ---
2
+ title: Architecture
3
+ layout: default
4
+ nav_order: 9
5
+ description: "Layer diagram, dispatcher / agent interaction, state-DB shape, and worktree lifecycle for contributors."
6
+ permalink: /architecture/
7
+ ---
8
+
9
+ # Architecture
10
+
11
+ This page is for people working _on_ ctrlrelay, not just _with_ it. For an
12
+ operator-level view of the BLOCKED protocol see
13
+ [Feedback loop]({{ '/feedback-loop/' | relative_url }}).
14
+
15
+ ## Layered overview
16
+
17
+ ```mermaid
18
+ flowchart TD
19
+ CLI["CLI / daemons<br/>cli.py (Typer app)<br/>bridge/__main__.py (bridge daemon)"]
20
+ PIPE["Pipelines — pipelines/<br/>dev · secops · task · post_merge · base"]
21
+ BRIDGE["Bridge — bridge/<br/>server · protocol · factory · node_tag<br/>telegram_handler · mattermost_handler"]
22
+ CORE["Core — core/<br/>Dispatcher (claude / opencode) · WorktreeManager<br/>GitHub CLI wrapper · Checkpoint · StateDB (SQLite)<br/>Poller · PRWatcher · Config (pydantic)"]
23
+ TRANS["Transports — transports/<br/>SocketTransport (to the bridge)<br/>FileMockTransport (tests)"]
24
+ DASH["Dashboard (optional, push-only) — dashboard/<br/>DashboardClient"]
25
+ CHAT["Chat: Telegram or Mattermost"]
26
+ CLI --> PIPE
27
+ CLI --> BRIDGE
28
+ PIPE --> CORE
29
+ PIPE --> TRANS
30
+ PIPE --> DASH
31
+ TRANS -- "Unix socket" --> BRIDGE
32
+ BRIDGE --> CHAT
33
+ ```
34
+
35
+ The CLI is a thin shell over async functions in `pipelines/` and `core/`. The
36
+ bridge is a separate daemon that pipelines reach over a Unix socket — it is
37
+ deliberately decoupled so it can be restarted without affecting in-flight
38
+ pipeline work.
39
+
40
+ ## Dispatcher ↔ agent interaction
41
+
42
+ [`src/ctrlrelay/core/dispatcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/dispatcher.py)
43
+ is the only place ctrlrelay calls out to a coding agent. It has two adapters
44
+ with the same async `spawn_session` shape, so pipelines stay agent-agnostic:
45
+ `ClaudeDispatcher` (Claude Code) and `OpenCodeDispatcher` (OpenCode).
46
+ `make_repo_agent_dispatcher` picks one per repo: `agent.type` is the default,
47
+ `repos[].agent_type` overrides it for a single repo.
48
+
49
+ - **Spawn shape:** Claude Code runs
50
+ `claude -p <prompt> --output-format stream-json --verbose --dangerously-skip-permissions [--resume <id>]`;
51
+ OpenCode runs `opencode run ... --format json --dangerously-skip-permissions [--session <id>]`.
52
+ - **Working directory:** the session's worktree (so file edits land on the
53
+ right branch).
54
+ - **Environment additions:** `CTRLRELAY_SESSION_ID` and `CTRLRELAY_STATE_FILE`
55
+ are the only contract between dispatcher and child. Anything else the agent
56
+ needs (issue body, PR rules, etc.) goes into the prompt.
57
+ - **Credentials (#219):** every spawn says whether it gets GitHub access
58
+ (`spawn_session(..., github_access=...)`, no default). dev and task spawn
59
+ without it: GitHub tokens, `gh`'s login, git credential helpers, SSH and
60
+ the desktop keyring are cut off (`core/agent_env.py`), and ctrlrelay
61
+ pushes the branch, opens the PR or posts the report itself afterwards
62
+ (`core/publish.py`, `WorktreeManager.push_run_branch`). secops still
63
+ spawns with access. See
64
+ [What the agent can reach](configuration.md#what-the-agent-can-reach-219).
65
+ - **Result:** `SessionResult(session_id, exit_code, state, stdout, stderr,
66
+ agent_session_id)`. `session_id` is our composite orchestrator id;
67
+ `agent_session_id` is the agent's own session id, parsed from its JSON
68
+ stdout and the value its resume flag actually expects. `state` is the parsed
69
+ `CheckpointState` if a state file was written, else `None`. The wrapper
70
+ exposes `.success`, `.blocked`, `.failed` properties for pattern-matching
71
+ pipelines.
72
+ - **Resume:** `resume_session_id` translates to `--resume <id>` (Claude
73
+ Code) or `--session <id>` (OpenCode). Pipelines look up the agent's own id
74
+ (persisted as `sessions.agent_session_id` by the prior spawn) and pass it
75
+ through — newer `claude` CLI builds reject non-UUID values. If state_db
76
+ has no id (session predates capture or stdout wasn't JSON), the spawn falls back to a fresh session rather than
77
+ hard-failing.
78
+
79
+ The dispatcher does NOT proxy or wrap the agent session — it shells out the
80
+ same way you'd shell out to `gh` or `git`. This is deliberate.
81
+
82
+ ## State DB shape
83
+
84
+ Schema in
85
+ [`src/ctrlrelay/core/state.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/state.py).
86
+ SQLite, single file at `paths.state_db`.
87
+
88
+ | Table | Columns | Notes |
89
+ |---|---|---|
90
+ | `sessions` | `id`, `pipeline`, `repo`, `issue_number`, `worktree_path`, `status`, `blocked_question`, `started_at`, `ended_at`, `claude_exit_code`, `summary`, `agent_session_id`, `output_tail` | Indexed on `repo` and `status`. One row per pipeline run. `agent_session_id` stores the agent's own session id for resume; `claude_exit_code` holds the worker's exit code whichever backend ran; `output_tail` is a scrubbed, capped tail of its output (#204). |
91
+ | `repo_locks` | `repo` (PK), `session_id`, `acquired_at` | Acquired with `acquire_lock`, released in the pipeline's `finally`. |
92
+ | `github_cursor` | `repo`, `last_checked_at`, `last_seen_issue_update` | Bounds GitHub API calls in the poller. |
93
+ | `pending_resumes` | `session_id` (PK), `pipeline`, `repo`, `question`, `created_at`, `answer`, `answered_at`, `resumed_at`, `expired_at`, `expired_reason`, `held_until`, `posted`, `last_answered_at` | One row per BLOCKED session awaiting an answer, whichever chat it was asked on. An answer arriving after the in-session wait lands here and the poller's sweeper drives the resume. |
94
+ | `pr_watches` | `repo`, `pr_number` (PK pair), `session_id`, `issue_number`, `pr_url`, `started_at`, `cleanup_phase` | In-flight post-merge watchers, rehydrated on poller start. |
95
+ | `automation_decisions` | `id`, `repo`, `operation`, `policy`, `item_id`, `decision`, `decided_by`, `decided_at`, `context` | Audit trail for `ask`-policy decisions. |
96
+ | `dispatch_health` | single row: `consecutive_failures`, `alerted_at`, `alert_claim_token`, `alert_delivered_at`, `last_failure_at`, `last_success_at` | Whether the relay can still run a worker at all (#204). |
97
+ | `relay_pushes` | `id`, `repo`, `branch`, `session_id`, `sha`, `pushed_at` | Every branch head ctrlrelay pushed for an agent (#219); the push lease is taken from here. |
98
+ | `events` | `id`, `ts`, `kind`, `repo`, `issue_number`, `session_id`, `detail` | Feed for the status page, written best-effort. |
99
+ | `telegram_pending` | `request_id`, `session_id`, `question`, `asked_at`, `answered_at`, `answer` | Legacy: still created by the schema, but no code reads or writes it. Outstanding questions live in `pending_resumes`. |
100
+
101
+ Methods on `StateDB`: `acquire_lock`, `release_lock`, `get_lock_holder`,
102
+ `list_locks`, `set_agent_session_id`, `get_agent_session_id`, plus raw
103
+ `execute` / `commit`. The pipeline code uses raw SQL for the `sessions`
104
+ table on purpose — kept thin and grep-able.
105
+
106
+ ## Worktree lifecycle
107
+
108
+ `WorktreeManager` (in
109
+ [`src/ctrlrelay/core/worktree.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/worktree.py))
110
+ is a thin wrapper over `git worktree`. Conventions:
111
+
112
+ - **Bare repo path:** `<paths.bare_repos>/<owner>-<repo>.git`. Cloned on first
113
+ use, fetched on each subsequent use.
114
+ - **Worktree path:** `<paths.worktrees>/<owner>-<repo>-<session_id>`.
115
+ - **Branch:** for the dev pipeline, derived from `dev_branch_template` with
116
+ `{n}` → issue number. Default `fix/issue-{n}`.
117
+
118
+ Lifecycle in `run_dev_issue`:
119
+
120
+ ```
121
+ ensure_bare_repo(repo)
122
+ branch_preexisted = await branch_exists_locally(repo, branch)
123
+ worktree_path = create_worktree_with_new_branch(repo, session_id, branch)
124
+ ... agent session runs ...
125
+ on DONE: remove_worktree (keep branch — open PR refs it)
126
+ on BLOCKED: keep both (operator may resume)
127
+ on FAILED: remove_worktree
128
+ if not branch_preexisted and not on remote: delete_branch
129
+ ```
130
+
131
+ The "preexisted" guard prevents a retry from clobbering a branch that already
132
+ holds an open PR from a previous successful session.
133
+
134
+ ## PR verifier and post-merge
135
+
136
+ After a `DONE`-with-`pr_url` checkpoint, the dev pipeline runs
137
+ [`PRVerifier`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/pr_verifier.py)
138
+ to confirm the PR is mergeable and CI is green. If not, it builds a fix prompt
139
+ (merge conflicts → "rebase and resolve", failing checks → "investigate and
140
+ fix"), resumes the same agent session with that prompt, and re-verifies. Up
141
+ to `DEFAULT_MAX_FIX_ATTEMPTS` (3) attempts.
142
+
143
+ `PRWatcher` (in
144
+ [`src/ctrlrelay/core/pr_watcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/pr_watcher.py))
145
+ polls a PR for merge status; the post-merge handler in
146
+ [`src/ctrlrelay/pipelines/post_merge.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/pipelines/post_merge.py)
147
+ closes the originating issue and notifies the operator once the PR lands.
148
+ The poller starts one `pr_watch_task` per PR the dev pipeline hands off, and
149
+ persists it in `pr_watches` so a poller restart resumes the watch.
150
+
151
+ ## Bridge architecture
152
+
153
+ The bridge daemon (`ctrlrelay/bridge/server.py`) listens on a Unix socket
154
+ (mode `0o600` — owner-only). It speaks newline-delimited JSON, defined in
155
+ `bridge/protocol.py`:
156
+
157
+ The bridge builds exactly one chat handler from `transport.type`
158
+ (`bridge/factory.py`): `TelegramHandler` or `MattermostHandler`. The server
159
+ knows only the handler interface, so the flow is the same for both:
160
+
161
+ ```mermaid
162
+ sequenceDiagram
163
+ participant Cl as Client (pipeline)
164
+ participant S as Bridge server
165
+ participant H as Chat handler
166
+ participant Ch as Telegram or Mattermost
167
+ Cl->>S: ask(q)
168
+ S->>H: ask(q, options)
169
+ H->>Ch: post (sendMessage / POST /api/v4/posts)
170
+ Note over S,H: server records post id → session
171
+ Ch-->>H: reply (getUpdates long-poll / WebSocket posted event)
172
+ H-->>S: on_reply(text, reply_to_post_id)
173
+ S-->>Cl: answer(text)
174
+ ```
175
+
176
+ The handler reports the post the reply points at — Telegram's
177
+ `reply_to_message_id`, or Mattermost's thread `root_id`. Matching reply →
178
+ pending question:
179
+
180
+ 1. The reply points at a live pending question — route to that question.
181
+ This is the only route that is right when several are outstanding.
182
+ 2. The reply points at a question that is no longer live (its ask timed
183
+ out, or the bridge restarted) — hand it to the orphan router, which
184
+ attaches the answer to that session's `pending_resumes` row.
185
+ 3. A fresh message with exactly one question outstanding — route there.
186
+ 4. A fresh message with several outstanding — refuse, and tell the operator
187
+ to reply to the question they mean. There is no FIFO fallback.
188
+
189
+ When several machines share one Mattermost bot and channel
190
+ (`transport.mattermost.shared: true`), each machine prefixes its posts with
191
+ `[<node>]` and ignores replies to other machines' posts (`bridge/node_tag.py`).
192
+
193
+ Multiple in-flight asks are supported. The bridge retains the original
194
+ socket for each ask so the answer routes back to the correct caller.
195
+
196
+ Bridge restart safety: pending in-memory state is lost on bridge restart.
197
+ Pipelines waiting on `transport.ask` stop waiting and persist the session as
198
+ `BLOCKED` with a `pending_resumes` row; a later answer still drives the
199
+ resume through the sweeper. Restart the bridge **before** restarting the
200
+ poller to minimise this window.
201
+
202
+ ## Why this shape
203
+
204
+ A few decisions worth flagging:
205
+
206
+ - **Local-first, file-based protocol.** Everything between ctrlrelay and the
207
+ agent is a file under the worktree. No long-lived shared memory, no socket
208
+ contract with the agent. Survives orchestrator restart trivially.
209
+ - **Subprocess-not-library.** `claude` or `opencode` is invoked as a
210
+ subprocess. ctrlrelay has no Python coupling to either agent's internals — it exchanges a prompt
211
+ in and a JSON file out. Versioning is decoupled.
212
+ - **Per-repo serial, parallel across repos.** Repo locks in SQLite enforce
213
+ this. A failed lock acquisition fails the session immediately rather than
214
+ queueing.
215
+ - **No background event loop.** Each CLI command spins up its own
216
+ `asyncio.run(...)`. The poller and bridge each own one event loop while they
217
+ run. Simpler to reason about than a long-lived shared loop.
218
+ - **Bridge as separate process.** Restartable independent of pipelines. The
219
+ cost is the small in-memory pending-asks state that does not survive a
220
+ bridge restart.
221
+
222
+ ## Where to read next
223
+
224
+ - [`src/ctrlrelay/core/config.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/config.py)
225
+ — pydantic schema, the source of truth for the YAML.
226
+ - [`src/ctrlrelay/pipelines/dev.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/pipelines/dev.py)
227
+ — full dev-pipeline orchestration including BLOCKED loop and PR-fix loop.
228
+ - [`src/ctrlrelay/core/dispatcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/dispatcher.py)
229
+ — the only place we call `claude` or `opencode`.
230
+ - [`tests/`](https://github.com/AInvirion/ctrlrelay/tree/main/tests) — covers
231
+ every public surface; treat tests as executable specifications.