ctrlrelay 0.19.0__tar.gz → 0.19.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 (208) hide show
  1. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/CHANGELOG.md +11 -1
  2. ctrlrelay-0.19.1/PKG-INFO +164 -0
  3. ctrlrelay-0.19.1/README.md +123 -0
  4. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/_config.yml +7 -2
  5. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/adding-a-machine.md +9 -6
  6. ctrlrelay-0.19.1/docs/architecture.md +231 -0
  7. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/bridge.md +115 -87
  8. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/cli.md +32 -21
  9. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/configuration.md +20 -15
  10. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/development.md +18 -15
  11. ctrlrelay-0.19.1/docs/feedback-loop.md +209 -0
  12. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/getting-started.md +32 -21
  13. ctrlrelay-0.19.1/docs/index.md +89 -0
  14. ctrlrelay-0.19.1/docs/llms.txt +36 -0
  15. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/operations.md +17 -10
  16. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/pyproject.toml +1 -1
  17. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/personalization/merge.py +37 -2
  18. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_personalization_merge.py +39 -21
  19. ctrlrelay-0.19.0/PKG-INFO +0 -263
  20. ctrlrelay-0.19.0/README.md +0 -222
  21. ctrlrelay-0.19.0/docs/architecture.md +0 -222
  22. ctrlrelay-0.19.0/docs/feedback-loop.md +0 -197
  23. ctrlrelay-0.19.0/docs/index.md +0 -62
  24. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/dependabot.yml +0 -0
  28. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/workflows/build.yml +0 -0
  29. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/workflows/cla.yml +0 -0
  30. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/workflows/pages.yml +0 -0
  31. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/workflows/publish.yml +0 -0
  32. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.github/workflows/test.yml +0 -0
  33. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/.gitignore +0 -0
  34. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/CODE_OF_CONDUCT.md +0 -0
  35. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/CONTRIBUTING.md +0 -0
  36. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/LICENSE +0 -0
  37. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/SECURITY.md +0 -0
  38. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/config/orchestrator.yaml.example +0 -0
  39. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/Gemfile +0 -0
  40. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/personalization.md +0 -0
  41. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/registered-mode.md +0 -0
  42. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  43. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  44. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/__init__.py +0 -0
  45. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/__init__.py +0 -0
  46. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/__main__.py +0 -0
  47. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/factory.py +0 -0
  48. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/handler.py +0 -0
  49. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  50. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/node_tag.py +0 -0
  51. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/protocol.py +0 -0
  52. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/server.py +0 -0
  53. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  54. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/cli.py +0 -0
  55. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/__init__.py +0 -0
  56. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/agent_env.py +0 -0
  57. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/archived.py +0 -0
  58. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/audit.py +0 -0
  59. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/binaries.py +0 -0
  60. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/checkpoint.py +0 -0
  61. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/claims.py +0 -0
  62. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/code_review.py +0 -0
  63. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/config.py +0 -0
  64. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/dispatch_health.py +0 -0
  65. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/dispatcher.py +0 -0
  66. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/events.py +0 -0
  67. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/github.py +0 -0
  68. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/github_budget.py +0 -0
  69. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/heartbeat.py +0 -0
  70. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/job_claims.py +0 -0
  71. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/network.py +0 -0
  72. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/node_check.py +0 -0
  73. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/obs.py +0 -0
  74. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/poller.py +0 -0
  75. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/pr_verifier.py +0 -0
  76. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/pr_watcher.py +0 -0
  77. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/publish.py +0 -0
  78. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/question_expiry.py +0 -0
  79. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/redact.py +0 -0
  80. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/scheduler.py +0 -0
  81. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/state.py +0 -0
  82. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/usage.py +0 -0
  83. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/core/worktree.py +0 -0
  84. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/dashboard/__init__.py +0 -0
  85. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/dashboard/client.py +0 -0
  86. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/gh_protocol.py +0 -0
  87. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/install.py +0 -0
  88. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/personalization/__init__.py +0 -0
  89. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/personalization/manager.py +0 -0
  90. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/personalization/paths.py +0 -0
  91. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/pipelines/__init__.py +0 -0
  92. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/pipelines/base.py +0 -0
  93. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/pipelines/dev.py +0 -0
  94. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  95. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/pipelines/secops.py +0 -0
  96. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/pipelines/task.py +0 -0
  97. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/setup.py +0 -0
  98. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/__init__.py +0 -0
  99. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  100. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  101. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  102. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  103. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  104. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  105. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  106. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  107. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/transports/__init__.py +0 -0
  108. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/transports/base.py +0 -0
  109. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/transports/file_mock.py +0 -0
  110. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/transports/socket_client.py +0 -0
  111. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/__init__.py +0 -0
  112. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/fleet.py +0 -0
  113. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/health.py +0 -0
  114. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/links.py +0 -0
  115. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/numbers.py +0 -0
  116. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/server.py +0 -0
  117. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/static/index.html +0 -0
  118. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/status.py +0 -0
  119. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/transcript.py +0 -0
  120. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/src/ctrlrelay/web/waiting.py +0 -0
  121. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/__init__.py +0 -0
  122. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/_poller_fakes.py +0 -0
  123. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/_publish_fakes.py +0 -0
  124. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/conftest.py +0 -0
  125. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/fixtures/gh_search_issues.json +0 -0
  126. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  127. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/fixtures/transcript_sample.jsonl +0 -0
  128. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_agent_backend_wiring.py +0 -0
  129. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_agent_no_keys.py +0 -0
  130. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_agent_routing.py +0 -0
  131. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_agent_sandbox.py +0 -0
  132. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_archived.py +0 -0
  133. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_audit.py +0 -0
  134. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_binary_resolution.py +0 -0
  135. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_bridge_protocol.py +0 -0
  136. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_bridge_server.py +0 -0
  137. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_bridge_start_command.py +0 -0
  138. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_chat_machines.py +0 -0
  139. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_checkpoint.py +0 -0
  140. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_claim_reconciliation.py +0 -0
  141. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_cli_ci_wait.py +0 -0
  142. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_cli_dev.py +0 -0
  143. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_cli_repos.py +0 -0
  144. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_cli_secops.py +0 -0
  145. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_cli_start.py +0 -0
  146. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_cli_version.py +0 -0
  147. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_code_review.py +0 -0
  148. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_config.py +0 -0
  149. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dashboard_client.py +0 -0
  150. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dev_integration.py +0 -0
  151. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dev_pipeline.py +0 -0
  152. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dev_step_switches.py +0 -0
  153. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dev_work_steps.py +0 -0
  154. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dispatch_health.py +0 -0
  155. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dispatch_outcome_hook.py +0 -0
  156. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_dispatcher.py +0 -0
  157. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_docs_site.py +0 -0
  158. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_event_emission.py +0 -0
  159. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_events.py +0 -0
  160. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_fleet.py +0 -0
  161. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_gh_protocol.py +0 -0
  162. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_github.py +0 -0
  163. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_github_budget.py +0 -0
  164. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_install.py +0 -0
  165. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_issue_outcome_announcement.py +0 -0
  166. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_job_claims.py +0 -0
  167. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_mattermost_handler.py +0 -0
  168. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_network.py +0 -0
  169. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_node_check.py +0 -0
  170. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_node_claims.py +0 -0
  171. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_notify_transport.py +0 -0
  172. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_obs.py +0 -0
  173. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_opencode_adapter.py +0 -0
  174. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_personalization.py +0 -0
  175. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_pipeline_base.py +0 -0
  176. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_poller.py +0 -0
  177. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_poller_search.py +0 -0
  178. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_post_merge.py +0 -0
  179. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_pr_verifier.py +0 -0
  180. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_pr_watcher.py +0 -0
  181. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_pre_dispatch_retry.py +0 -0
  182. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_publish_failure_retry.py +0 -0
  183. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_question_backoff.py +0 -0
  184. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_question_expiry.py +0 -0
  185. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_redact.py +0 -0
  186. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_relay_alert_off_lock.py +0 -0
  187. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_relay_git_263.py +0 -0
  188. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_resume_prompt_contract.py +0 -0
  189. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_review_copy.py +0 -0
  190. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_scheduler.py +0 -0
  191. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_secops_integration.py +0 -0
  192. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_secops_pipeline.py +0 -0
  193. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_setup.py +0 -0
  194. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_state.py +0 -0
  195. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_task_pipeline.py +0 -0
  196. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_telegram_handler.py +0 -0
  197. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_transport.py +0 -0
  198. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_transport_resolution.py +0 -0
  199. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_unreviewed_pr_announcement.py +0 -0
  200. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_usage.py +0 -0
  201. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_health.py +0 -0
  202. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_links.py +0 -0
  203. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_numbers.py +0 -0
  204. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_server.py +0 -0
  205. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_status.py +0 -0
  206. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_transcript.py +0 -0
  207. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_web_waiting.py +0 -0
  208. {ctrlrelay-0.19.0 → ctrlrelay-0.19.1}/tests/test_worktree.py +0 -0
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.19.1] - 2026-11-03
11
+
12
+ ### Fixed
13
+
14
+ - A synced or live `settings.json` nested more than 64 levels deep is now
15
+ refused right after it is read. On Python 3.14 the parser accepts about
16
+ 65,000 levels, and writing that back out used all the memory on the
17
+ machine instead of failing (#313).
18
+
10
19
  ## [0.19.0] - 2026-11-03
11
20
 
12
21
  ### Added
@@ -2257,7 +2266,8 @@ pipeline).
2257
2266
  per-phase implementation plans (Phase 0 through Phase 4).
2258
2267
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2259
2268
 
2260
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...HEAD
2269
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.1...HEAD
2270
+ [0.19.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...v0.19.1
2261
2271
  [0.19.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.18.0...v0.19.0
2262
2272
  [0.18.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...v0.18.0
2263
2273
  [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.19.1
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.