ctrlrelay 0.17.0__tar.gz → 0.18.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 (199) hide show
  1. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/CHANGELOG.md +124 -6
  2. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/PKG-INFO +1 -1
  3. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/config/orchestrator.yaml.example +50 -4
  4. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/cli.md +1 -1
  5. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/configuration.md +120 -1
  6. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/operations.md +38 -5
  7. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/personalization.md +34 -7
  8. ctrlrelay-0.18.0/docs/registered-mode.md +476 -0
  9. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/pyproject.toml +1 -1
  10. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/cli.py +124 -16
  11. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/__init__.py +8 -0
  12. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/config.py +51 -6
  13. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/dispatcher.py +857 -320
  14. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/node_check.py +16 -2
  15. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/worktree.py +228 -20
  16. ctrlrelay-0.18.0/src/ctrlrelay/install.py +323 -0
  17. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/personalization/manager.py +72 -7
  18. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/pipelines/dev.py +65 -2
  19. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/setup.py +34 -8
  20. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +9 -2
  21. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/launchd/poller.plist.template +9 -2
  22. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/systemd/bridge.service.template +7 -1
  23. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/systemd/poller.service.template +7 -1
  24. ctrlrelay-0.18.0/src/ctrlrelay/web/numbers.py +275 -0
  25. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/static/index.html +103 -0
  26. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/status.py +32 -0
  27. ctrlrelay-0.18.0/tests/test_agent_backend_wiring.py +264 -0
  28. ctrlrelay-0.18.0/tests/test_agent_routing.py +397 -0
  29. ctrlrelay-0.18.0/tests/test_install.py +870 -0
  30. ctrlrelay-0.18.0/tests/test_opencode_adapter.py +1044 -0
  31. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_personalization.py +69 -3
  32. ctrlrelay-0.18.0/tests/test_review_copy.py +406 -0
  33. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_setup.py +115 -0
  34. ctrlrelay-0.18.0/tests/test_web_numbers.py +426 -0
  35. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_web_server.py +1 -1
  36. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_web_status.py +1 -1
  37. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_worktree.py +216 -13
  38. ctrlrelay-0.17.0/src/ctrlrelay/install.py +0 -204
  39. ctrlrelay-0.17.0/tests/test_install.py +0 -365
  40. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  41. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  42. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/dependabot.yml +0 -0
  44. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/workflows/build.yml +0 -0
  45. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/workflows/cla.yml +0 -0
  46. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/workflows/pages.yml +0 -0
  47. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/workflows/publish.yml +0 -0
  48. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.github/workflows/test.yml +0 -0
  49. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/.gitignore +0 -0
  50. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/CODE_OF_CONDUCT.md +0 -0
  51. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/CONTRIBUTING.md +0 -0
  52. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/LICENSE +0 -0
  53. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/README.md +0 -0
  54. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/SECURITY.md +0 -0
  55. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/Gemfile +0 -0
  56. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/_config.yml +0 -0
  57. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/adding-a-machine.md +0 -0
  58. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/architecture.md +0 -0
  59. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/bridge.md +0 -0
  60. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/development.md +0 -0
  61. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/feedback-loop.md +0 -0
  62. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/getting-started.md +0 -0
  63. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/index.md +0 -0
  64. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  65. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  66. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/__init__.py +0 -0
  67. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  68. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/__main__.py +0 -0
  69. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/factory.py +0 -0
  70. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/handler.py +0 -0
  71. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  72. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/node_tag.py +0 -0
  73. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  74. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/server.py +0 -0
  75. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  76. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/agent_env.py +0 -0
  77. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/archived.py +0 -0
  78. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/audit.py +0 -0
  79. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/binaries.py +0 -0
  80. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/checkpoint.py +0 -0
  81. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/claims.py +0 -0
  82. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/code_review.py +0 -0
  83. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
  84. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/events.py +0 -0
  85. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/github.py +0 -0
  86. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/github_budget.py +0 -0
  87. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/heartbeat.py +0 -0
  88. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/job_claims.py +0 -0
  89. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/network.py +0 -0
  90. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/obs.py +0 -0
  91. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/poller.py +0 -0
  92. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  93. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  94. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/publish.py +0 -0
  95. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/question_expiry.py +0 -0
  96. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/redact.py +0 -0
  97. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/scheduler.py +0 -0
  98. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/state.py +0 -0
  99. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/core/usage.py +0 -0
  100. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  101. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/dashboard/client.py +0 -0
  102. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/gh_protocol.py +0 -0
  103. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/personalization/__init__.py +0 -0
  104. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/personalization/paths.py +0 -0
  105. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  106. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/pipelines/base.py +0 -0
  107. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  108. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/pipelines/secops.py +0 -0
  109. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/pipelines/task.py +0 -0
  110. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/__init__.py +0 -0
  111. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  112. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  113. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  114. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  115. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/transports/__init__.py +0 -0
  116. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/transports/base.py +0 -0
  117. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/transports/file_mock.py +0 -0
  118. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/transports/socket_client.py +0 -0
  119. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/__init__.py +0 -0
  120. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/fleet.py +0 -0
  121. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/health.py +0 -0
  122. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/links.py +0 -0
  123. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/server.py +0 -0
  124. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/transcript.py +0 -0
  125. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/src/ctrlrelay/web/waiting.py +0 -0
  126. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/__init__.py +0 -0
  127. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/_poller_fakes.py +0 -0
  128. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/_publish_fakes.py +0 -0
  129. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/conftest.py +0 -0
  130. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/fixtures/gh_search_issues.json +0 -0
  131. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  132. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/fixtures/transcript_sample.jsonl +0 -0
  133. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_agent_no_keys.py +0 -0
  134. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_agent_sandbox.py +0 -0
  135. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_archived.py +0 -0
  136. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_audit.py +0 -0
  137. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_binary_resolution.py +0 -0
  138. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_bridge_protocol.py +0 -0
  139. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_bridge_server.py +0 -0
  140. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_bridge_start_command.py +0 -0
  141. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_chat_machines.py +0 -0
  142. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_checkpoint.py +0 -0
  143. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_claim_reconciliation.py +0 -0
  144. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_cli_ci_wait.py +0 -0
  145. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_cli_dev.py +0 -0
  146. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_cli_repos.py +0 -0
  147. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_cli_secops.py +0 -0
  148. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_cli_start.py +0 -0
  149. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_cli_version.py +0 -0
  150. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_code_review.py +0 -0
  151. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_config.py +0 -0
  152. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_dashboard_client.py +0 -0
  153. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_dev_integration.py +0 -0
  154. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_dev_pipeline.py +0 -0
  155. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_dispatch_health.py +0 -0
  156. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_dispatch_outcome_hook.py +0 -0
  157. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_dispatcher.py +0 -0
  158. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_docs_site.py +0 -0
  159. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_event_emission.py +0 -0
  160. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_events.py +0 -0
  161. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_fleet.py +0 -0
  162. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_gh_protocol.py +0 -0
  163. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_github.py +0 -0
  164. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_github_budget.py +0 -0
  165. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_issue_outcome_announcement.py +0 -0
  166. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_job_claims.py +0 -0
  167. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_mattermost_handler.py +0 -0
  168. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_network.py +0 -0
  169. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_node_check.py +0 -0
  170. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_node_claims.py +0 -0
  171. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_notify_transport.py +0 -0
  172. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_obs.py +0 -0
  173. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_pipeline_base.py +0 -0
  174. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_poller.py +0 -0
  175. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_poller_search.py +0 -0
  176. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_post_merge.py +0 -0
  177. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_pr_verifier.py +0 -0
  178. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_pr_watcher.py +0 -0
  179. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_pre_dispatch_retry.py +0 -0
  180. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_publish_failure_retry.py +0 -0
  181. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_question_backoff.py +0 -0
  182. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_question_expiry.py +0 -0
  183. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_redact.py +0 -0
  184. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_relay_alert_off_lock.py +0 -0
  185. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_resume_prompt_contract.py +0 -0
  186. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_scheduler.py +0 -0
  187. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_secops_integration.py +0 -0
  188. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_secops_pipeline.py +0 -0
  189. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_state.py +0 -0
  190. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_task_pipeline.py +0 -0
  191. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_telegram_handler.py +0 -0
  192. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_transport.py +0 -0
  193. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_transport_resolution.py +0 -0
  194. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_unreviewed_pr_announcement.py +0 -0
  195. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_usage.py +0 -0
  196. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_web_health.py +0 -0
  197. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_web_links.py +0 -0
  198. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_web_transcript.py +0 -0
  199. {ctrlrelay-0.17.0 → ctrlrelay-0.18.0}/tests/test_web_waiting.py +0 -0
@@ -7,6 +7,123 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.18.0] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - A third agent backend: `agent.type: "opencode"` runs sessions through
15
+ `opencode run --format json --dangerously-skip-permissions` — the spelling
16
+ every opencode from 1.4 to 2.x accepts; its `--auto` alias exists only from
17
+ 1.17.12 — which can target a self-hosted OpenAI-compatible endpoint
18
+ (ollama, llama.cpp, LM Studio, MLX) and so lets the dev pipeline run on a
19
+ local model instead of a hosted API. The endpoint, its credential and the
20
+ model list stay in opencode's own config; ctrlrelay names the model with
21
+ `agent.opencode.model` (`provider/model`) and the binary with
22
+ `agent.opencode.binary` — its own block, because `agent.binary` is claude's
23
+ and most configs set it to the literal `"claude"`.
24
+ - `repos[].agent_type` opts a single repository into another backend, with
25
+ `agent.type` the default for every other one. Per repo rather than global on
26
+ purpose: ctrlrelay works unattended over issue and pull-request text from
27
+ many repositories and opens pull requests, so a weaker model behind the
28
+ whole daemon would produce bad pull requests at machine speed with nobody
29
+ watching. An unroutable session goes to the default backend, never to the
30
+ opt-in one, and repo names match case-insensitively. A value no adapter
31
+ answers to stops the daemon at startup naming the repository, the same way
32
+ an unknown `agent.type` already did (closes #286)
33
+ - The status page and `/api/status` show how this node's runs went over
34
+ the last 7 and 30 days, overall and per repo: runs started, done,
35
+ failed, blocked and still running, success rate, median time to PR and
36
+ to merge, and issues dispatched again after a done run. Cost per run and
37
+ stuck runs are shown as not recorded yet (refs #230)
38
+
39
+ ### Changed
40
+
41
+ - The subprocess machinery every CLI-driven adapter shares — the timeout, the
42
+ kill on cancel, the replacing decode, the checkpoint read and #204's outcome
43
+ report on every exit path — moved from `ClaudeDispatcher` to a
44
+ `CliAgentAdapter` base both adapters inherit. A second copy of it is how the
45
+ next backend gets dispatch failures that nothing counts.
46
+ - `ctrlrelay node check` skips its agent-login row, and says why, when no
47
+ repository routes to `claude` — the global `agent.type` is not `claude` and
48
+ no `repos[].agent_type` names it. That check is a `claude -p` call reading
49
+ the claude subscription, so on another backend it reported a failed login
50
+ against a CLI the deployment does not use; with even one repo still on
51
+ `claude` it runs, so an expired login shows up there and not as that repo's
52
+ next failed job.
53
+
54
+ ### Fixed
55
+
56
+ - The code review no longer runs in the agent's worktree. It runs in a
57
+ fresh clone of the commit the relay pushed, in a new temporary
58
+ directory, removed afterwards. Before, a nested repository the agent
59
+ left in its worktree (a gitlink with its own `.git` and config) made a
60
+ plain `git status` or `git diff` there run a program the agent chose,
61
+ in the reviewer's process, outside the agent sandbox. The copy also
62
+ switches off filter and diff drivers from the operator's own git
63
+ config (for example git-lfs or a textconv), so a committed
64
+ `.gitattributes` cannot point agent files at them. If no clean copy can
65
+ be made, the PR is announced as not reviewed (`copy_failed`); it never
66
+ falls back to the worktree (#263)
67
+ - Personalization auto-pull no longer reports success when it could not
68
+ sync. A dirty working tree now fails the run, logged at error level with
69
+ the dirty entries; every run reports `behind N` (a dirty tree still
70
+ fetches to measure it). `push` pre-flights the git identity and refuses
71
+ with `git identity unresolvable` instead of leaving a dirty tree.
72
+ `docs/personalization.md` describes the open question of app-rewritten
73
+ files such as `settings.json` (refs #287)
74
+
75
+ ### Known limits
76
+
77
+ - The `opencode` backend writes no usage reading. A self-hosted model has no
78
+ subscription window, and recording "unknown" after every run would erase a
79
+ reading the claude backend holds and un-pause a machine that is at its limit.
80
+ - `agent.sandbox: "bwrap"` is **refused**, not ignored, alongside an `opencode`
81
+ backend: that confinement binds `~/.claude/*` writable and copies
82
+ `~/.claude.json`, so it is written for one CLI. Running unconfined under a
83
+ config that says otherwise would be worse than a daemon that will not start.
84
+ Credential stripping (#219) applies to every backend either way.
85
+ - Whether a given local model can drive the pipeline is a separate question
86
+ from whether the adapter is correct, and this change does not answer it.
87
+ Measured on an M1 Max on 2026-11-01: `qwen2.5-coder:32b-instruct-q4_K_M`
88
+ emitted **no tool calls at all** (0/3) while advertising `tools` in its
89
+ metadata, and `qwen3-coder:30b` emitted them (3/3). ctrlrelay's agents signal
90
+ DONE / BLOCKED / FAILED by running a shell command that writes the checkpoint
91
+ file, so a model that cannot call tools cannot finish a session. A declared
92
+ capability is not evidence; only a live call is.
93
+
94
+ ## [0.17.1] - 2026-11-01
95
+
96
+ ### Fixed
97
+
98
+ - Installed bridge and poller units carry the token variable the configured
99
+ transport names, not a fixed Telegram one. `ctrlrelay install
100
+ launchd|systemd` and `ctrlrelay setup --install-daemons` read
101
+ `transport.<type>.bot_token_env` and render `Environment=<that
102
+ name>=<value>` (systemd) or the matching plist entry (launchd), with the
103
+ value taken from that variable in the installing shell. A Mattermost
104
+ install previously got a unit naming `CTRLRELAY_TELEGRAM_TOKEN`, which it
105
+ never sets, so the bridge could not log in once systemd or launchd started
106
+ it — the server here only worked because the line had been added by hand.
107
+ The unresolved-variable hint names the same variable, and the install
108
+ commands take `--config` to say which `orchestrator.yaml` to read
109
+ (closes #267)
110
+ - Updating a repository's clone no longer deletes local branches GitHub does
111
+ not have. The fetch paired `--prune` with `refs/heads/*:refs/heads/*`, which
112
+ tells git to delete every local branch missing from origin: a branch made in
113
+ the clone went with `- [deleted] (none) -> held`. The worktree stayed,
114
+ but its branch ref was gone, so a blocked session whose branch was never
115
+ pushed lost the branch the next time any run touched that repo, and its
116
+ commits could later be garbage-collected. The fetch is now two: the
117
+ `dependabot/*` branches, which never carry local work and are the refs that
118
+ actually pile up, are still pruned; every other branch is fetched without
119
+ `--prune`. The cost is that a branch GitHub deleted now lingers in the
120
+ clone, since a prune cannot tell it from one GitHub never had — the
121
+ branch-reuse path already recognises a local-only ref and decides whether
122
+ its commits are merged. Because a renamed default branch is no longer
123
+ pruned either, each update also asks GitHub which branch is the default
124
+ and points the clone at it when it changed and the branch here is at origin's
125
+ commit (#243).
126
+
10
127
  ## [0.17.0] - 2026-11-01
11
128
 
12
129
  Polling costs one GitHub search instead of one call per repo. **No config
@@ -451,7 +568,7 @@ daemon with any of it.
451
568
  rewind even when every other ref applied. `ensure_bare_repo` read that
452
569
  exit code as failure and aborted the session.
453
570
 
454
- Observed on `AInvirion/zzsites`, whose sweep died reporting
571
+ Observed on one of the swept repositories, whose sweep died reporting
455
572
  `WorktreeError: git failed: ... ! [rejected] ... (non-fast-forward)`
456
573
  in the same fetch that had just advanced `main`. Twelve bare repos
457
574
  were holding a `dependabot/*` ref at the time, each one primed to fail
@@ -482,8 +599,7 @@ daemon with any of it.
482
599
  `✅ Scheduled secops sweep done: N/M ok`, which fires once every repo
483
600
  has been swept. On the 2026-09-09 sweep that was hours after the reply
484
601
  and named no repo: four answers merged 20 Dependabot PRs across
485
- `ai-miner`, `kenos-orchestrator`, `prompt-injection-benchmark` and
486
- `aiproxyguard-promptest`, and not one outbound message went back to
602
+ four repositories, and not one outbound message went back to
487
603
  the operator. Answering looked indistinguishable from being ignored.
488
604
 
489
605
  The inline path now reports the same three outcomes the sweeper
@@ -1019,7 +1135,7 @@ during the dogfood validation:
1019
1135
 
1020
1136
  - **`setup` no longer lists the personalization repo under
1021
1137
  `repos:`.** When the operator's personalization repo lives under
1022
- one of the configured owners (e.g. `alice/dotclaude` while `alice`
1138
+ one of the configured owners (e.g. `alice/agent-config` while `alice`
1023
1139
  is also an enumerated owner), it was being added to the dev
1024
1140
  pipeline's monitored set — the poller would have polled it for
1025
1141
  issues and worktree-cloned it. The repo is the cross-machine sync
@@ -1181,7 +1297,7 @@ personalization flow.
1181
1297
  ### Docs
1182
1298
 
1183
1299
  - **[Personalization sync](docs/personalization.md)** (#125). New
1184
- page (nav order 8) covering setup, the dotclaude repo layout, the
1300
+ page (nav order 8) covering setup, the personalization repo layout, the
1185
1301
  init/status/push/pull lifecycle, `--no-adopt`, auto-pull cron,
1186
1302
  multi-machine bootstrap, and the gotchas (worktrees, edit-through-
1187
1303
  symlink semantics, allowlist enforcement, conflict handling, strict
@@ -2076,7 +2192,9 @@ pipeline).
2076
2192
  per-phase implementation plans (Phase 0 through Phase 4).
2077
2193
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2078
2194
 
2079
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.0...HEAD
2195
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.18.0...HEAD
2196
+ [0.18.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...v0.18.0
2197
+ [0.17.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.0...v0.17.1
2080
2198
  [0.17.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.4...v0.17.0
2081
2199
  [0.16.4]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.3...v0.16.4
2082
2200
  [0.16.3]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.2...v0.16.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
5
  Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
6
  Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
@@ -21,16 +21,52 @@ paths:
21
21
  # AInvirion: AINVIRION # GitHub owner -> on-disk folder name
22
22
  # SemClone: SEMCL.ONE
23
23
 
24
- # Headless coding agent. `type` selects the adapter (currently only
25
- # "claude" is implemented; other backends — codex, opencode, hermes,
26
- # kiro — are planned). Legacy `claude:` key is still accepted as an
27
- # alias for backwards compatibility but is deprecated.
24
+ # Headless coding agent. `type` selects the adapter: "claude" and
25
+ # "opencode" are implemented, and codex/hermes/kiro are still planned.
26
+ # An unimplemented value stops the daemon at startup rather than falling
27
+ # back to claude, so a typo here is loud. Legacy `claude:` key is still
28
+ # accepted as an alias for backwards compatibility but is deprecated.
29
+ #
30
+ # `type` is the DEFAULT for every repo. A single repo can opt into
31
+ # another backend with `repos[].agent_type` — see the repos block at the
32
+ # bottom of this file. That is the intended shape: claude everywhere,
33
+ # one low-stakes repo on a self-hosted model, rather than a global flip.
28
34
  agent:
29
35
  type: "claude"
36
+ # claude's own binary and nothing else's. The opencode block below has
37
+ # its own, because most configs set this to the literal "claude" and
38
+ # a second backend reading it would exec the wrong program.
30
39
  binary: "claude"
31
40
  default_timeout_seconds: 1800
32
41
  output_format: "json"
33
42
 
43
+ # Read only when a backend of type "opencode" is built, so this block
44
+ # is inert on a claude-only deployment and harmless to leave in place.
45
+ #
46
+ # `model` is "provider/model" as opencode spells it. Point a provider
47
+ # at a self-hosted OpenAI-compatible endpoint in opencode's own config
48
+ # (~/.config/opencode/opencode.json), e.g.
49
+ #
50
+ # {"provider": {"local": {"npm": "@ai-sdk/openai-compatible",
51
+ # "options": {"baseURL": "http://127.0.0.1:11434/v1"},
52
+ # "models": {"qwen3-coder:30b": {}}}}}
53
+ #
54
+ # and name it here as "local/qwen3-coder:30b". The endpoint, its
55
+ # credential and the model list live there, not in ctrlrelay — leaving
56
+ # `model` unset lets opencode pick, which is usually what you want.
57
+ #
58
+ # One measured caveat before choosing a model: a model can advertise
59
+ # tool support and emit no tool calls at all. ctrlrelay's agents
60
+ # signal DONE / BLOCKED / FAILED by RUNNING A SHELL COMMAND that
61
+ # writes the checkpoint file, so a model that cannot call tools cannot
62
+ # finish a session however good its prose is. Test with a live call.
63
+ # opencode:
64
+ # binary: "opencode"
65
+ # model: "local/qwen3-coder:30b"
66
+ # # Falls back to agent.default_timeout_seconds when unset. A local
67
+ # # model is usually slower per token than a hosted one.
68
+ # default_timeout_seconds: 3600
69
+
34
70
  # EXACTLY ONE transport is active, and it is the one named by `type`.
35
71
  # The other blocks below are inert: they may stay in the file, but nothing
36
72
  # reads them. Running Telegram and Mattermost at the same time is NOT
@@ -184,6 +220,16 @@ repos: []
184
220
  # - name: "owner/repo"
185
221
  # # Optional when paths.repo_root is set; otherwise required.
186
222
  # local_path: "~/Projects/repo"
223
+ # # Which agent backend runs THIS repo. Omit for agent.type, which
224
+ # # is where every repo is unless it says otherwise. A value no
225
+ # # adapter answers to stops the daemon at startup naming this repo.
226
+ # #
227
+ # # Opt in per repo rather than globally. ctrlrelay works unattended
228
+ # # over issue and pull-request text and opens pull requests, so a
229
+ # # weaker model behind the whole daemon produces bad pull requests
230
+ # # at machine speed with nobody watching. Put a low-stakes repo
231
+ # # here; leave the ones that matter on claude.
232
+ # agent_type: "opencode"
187
233
  # automation:
188
234
  # dependabot_patch: auto
189
235
  # dependabot_minor: ask
@@ -68,7 +68,7 @@ Key flags:
68
68
  | `--telegram-chat-id ID` | none | Required for `--transport=telegram`. |
69
69
  | `--personalization-repo OWNER/REPO` | none | Adds a personalization block. |
70
70
  | `--no-personalization` | off | Skip the personalization prompt entirely. |
71
- | `--install-daemons` | (prompted) | Render and write launchd/systemd unit files. Reads `$CTRLRELAY_TELEGRAM_TOKEN` so the rendered plist isn't a placeholder. |
71
+ | `--install-daemons` | (prompted) | Render and write launchd/systemd unit files. Reads the token variable the chosen transport names (`$CTRLRELAY_TELEGRAM_TOKEN`, `$CTRLRELAY_MATTERMOST_TOKEN`) so the rendered unit isn't a placeholder. |
72
72
  | `--skip-archived/--include-archived` | skip | gh repo list filter. |
73
73
  | `--skip-forks/--include-forks` | skip | gh repo list filter. |
74
74
  | `--yes` / `-y` | off | Accept every default; never prompt. |
@@ -70,6 +70,124 @@ paths:
70
70
  | `repo_root` | path | no | Convention root for repo clones. When set, `repos[].local_path` may be omitted and is derived as `${repo_root}/${owner.lower()}/${repo}` (since v0.4.0). Without `repo_root`, every repo entry must declare its own `local_path` (legacy behaviour). |
71
71
  | `owner_aliases` | object | no | **Deprecated since v0.4.0.** Pre-0.4.0 this remapped the on-disk folder name (e.g. `SemClone` -> `SEMCL.ONE`). The path resolver now always uses `owner.lower()`, so aliases are no longer consulted. Parsing is retained so 0.3.x configs still load; a `DeprecationWarning` is emitted when a non-empty mapping is supplied. To silence: drop the `owner_aliases` block, rename your on-disk folder to match `owner.lower()`, or set a per-repo `local_path` override for any that genuinely deviate. |
72
72
 
73
+ ## agent backends
74
+
75
+ `agent.type` picks the adapter ctrlrelay uses to talk to a coding-agent
76
+ CLI. Two are implemented:
77
+
78
+ | `agent.type` | CLI it runs | notes |
79
+ |---|---|---|
80
+ | `claude` (default) | `claude -p --output-format stream-json --verbose --dangerously-skip-permissions` | Reads the subscription usage; supports `sandbox: "bwrap"`. |
81
+ | `opencode` | `opencode run --format json --dangerously-skip-permissions` | Can target a self-hosted OpenAI-compatible endpoint. No usage reading; no bwrap yet. |
82
+
83
+ Anything else raises at startup with a message naming the type. That is
84
+ deliberate: a typo must stop the daemon rather than silently fall back
85
+ to claude.
86
+
87
+ ### Per-repo opt-in
88
+
89
+ `agent.type` is the default for every repo. A single repo opts into
90
+ another backend with `repos[].agent_type`:
91
+
92
+ ```yaml
93
+ agent:
94
+ type: "claude" # everything, unless a repo says otherwise
95
+ opencode:
96
+ model: "local/qwen3-coder:30b"
97
+
98
+ repos:
99
+ - name: "your-org/important-service" # claude
100
+ - name: "your-org/scratchpad"
101
+ agent_type: "opencode" # the local model
102
+ ```
103
+
104
+ Per repo rather than globally, on purpose. ctrlrelay runs unattended
105
+ over issue and pull-request text from many repositories and opens pull
106
+ requests. A weaker model behind the whole daemon produces bad pull
107
+ requests at machine speed with nobody watching, so the shape that is
108
+ safe is claude everywhere plus an explicit opt-in for low-stakes work.
109
+
110
+ Repo names are matched case-insensitively — GitHub treats `owner/repo`
111
+ that way and the config, an issue payload and `gh` output do not
112
+ reliably agree on the case.
113
+
114
+ ### agent.opencode
115
+
116
+ ```yaml
117
+ agent:
118
+ type: "claude"
119
+ opencode:
120
+ binary: "opencode"
121
+ model: "local/qwen3-coder:30b"
122
+ default_timeout_seconds: 3600
123
+ ```
124
+
125
+ | Key | Type | Default | Description |
126
+ |---|---|---|---|
127
+ | `binary` | string | `"opencode"` | The `opencode` executable. The bare name is resolved like every other tool (PATH, then `~/.local/bin`, `/usr/local/bin`, `/opt/homebrew/bin`), which matters under systemd where the unit's PATH is minimal. An absolute or relative path is used exactly as written. |
128
+ | `model` | string | `null` | `provider/model` as opencode spells it, passed as `--model`. Unset leaves the choice to opencode's own config, which is usually right. |
129
+ | `default_timeout_seconds` | int | `null` | Falls back to `agent.default_timeout_seconds`. A local model is usually slower per token than a hosted one. |
130
+
131
+ This block is its own rather than a reuse of `agent.binary`: that field
132
+ is claude's, most deployed configs set it to the literal `"claude"`, and
133
+ a second backend reading it would exec `claude` with `opencode`'s
134
+ arguments — a failure that looks like the model's fault.
135
+
136
+ **The endpoint is configured in opencode, not here.** Point a provider
137
+ at your self-hosted OpenAI-compatible server in
138
+ `~/.config/opencode/opencode.json`:
139
+
140
+ ```json
141
+ {
142
+ "provider": {
143
+ "local": {
144
+ "npm": "@ai-sdk/openai-compatible",
145
+ "options": { "baseURL": "http://127.0.0.1:11434/v1" },
146
+ "models": { "qwen3-coder:30b": {} }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ and name it here as `local/qwen3-coder:30b`. The credential, if the
153
+ endpoint needs one, lives there too.
154
+
155
+ ### What `opencode` does not do yet
156
+
157
+ - **No usage reading.** A self-hosted model has no subscription window.
158
+ The adapter writes nothing to `usage.json` — not even "unknown" —
159
+ because marking it unknown after every run would erase a reading the
160
+ claude backend legitimately holds and un-pause a machine that is
161
+ genuinely at its limit. On a deployment with no claude backend at all,
162
+ `usage` stays empty and the idle probe does not run.
163
+ - **No `sandbox: "bwrap"`.** The confinement binds `~/.claude/*`
164
+ writable and copies `~/.claude.json`: it is written for one CLI.
165
+ Combining `agent.sandbox: "bwrap"` with an `opencode` backend is
166
+ **refused at startup** rather than silently run unconfined. Set
167
+ `agent.sandbox: "none"` to use it. Credential stripping (#219) applies
168
+ to every backend either way.
169
+ - **`ctrlrelay node check` skips the agent-login row** when no
170
+ repository routes to `claude` — the global `agent.type` is not
171
+ `claude` and no `repos[].agent_type` names it — because that check is
172
+ a `claude` call reading the claude subscription. With a global
173
+ `opencode` and even one repo on `claude`, the row still runs. The check
174
+ never probes `opencode` itself: verify it by running its CLI once by
175
+ hand.
176
+
177
+ ### Picking a model for `opencode`
178
+
179
+ One result worth knowing before you choose, measured on an M1 Max on
180
+ 2026-11-01: **a model can advertise tool support and emit no tool calls
181
+ at all.** `qwen2.5-coder:32b-instruct-q4_K_M` scored 0/3 on tool calling
182
+ while declaring `tools` in its metadata and shipping a tool-aware
183
+ template; `qwen3-coder:30b` scored 3/3.
184
+
185
+ That matters more here than it looks. ctrlrelay's agents signal `DONE`,
186
+ `BLOCKED_NEEDS_INPUT` and `FAILED` by **running a shell command that
187
+ writes the checkpoint file** — so a model that cannot call tools cannot
188
+ finish a session at all, however good its prose is. Test with a live
189
+ call; a declared capability is not evidence.
190
+
73
191
  ## claude
74
192
 
75
193
  Controls how ctrlrelay invokes the `claude` CLI.
@@ -413,7 +531,7 @@ web:
413
531
  |---|---|---|---|
414
532
  | `host` | string | `"127.0.0.1"` | Address to listen on, exactly. Loopback by default so a fresh install is never reachable from other machines. Set it to this machine's LAN address to open the page from the home network. `0.0.0.0` listens on every interface and is warned about at startup. |
415
533
  | `port` | int (1-65535) | `8787` | Port to listen on. |
416
- | `history_days` | int (>= 1) | `7` | Days of feed the page shows, and what `/api/events` returns without `?since=`. |
534
+ | `history_days` | int (>= 1) | `7` | Days of feed the page shows, and what `/api/events` returns without `?since=`. Events older than this are deleted, so the run numbers' time to PR and time to merge are shown only for a window whose runs all started after the oldest event kept. Set it to `30` to get the 30-day ones; they appear once 30 days of events have been kept. |
417
535
  | `refresh_seconds` | int (>= 1) | `5` | How often the page asks `/api/status` again. |
418
536
  | `github_check_seconds` | int (>= 15) | `300` | How often the waiting line asks GitHub (one search, plus one query per `include_labels` label). Every check spends from the GitHub account's budget, which machines on the account share. |
419
537
  | `stale_after_minutes` | int (>= 1) | `15` | A running card is marked "looks stuck" after this long with no new agent step, unless ctrlrelay itself has a CI wait or review open for it. |
@@ -537,6 +655,7 @@ repos:
537
655
  | `name` | string | **yes** | — | GitHub `owner/repo` slug. Used for `gh` calls and bare-repo / worktree naming. |
538
656
  | `local_path` | path | conditional | derived | Where the repo is checked out on disk for human use. Optional when `paths.repo_root` is set (then derived as `${repo_root}/${owner.lower()}/${repo}`, since v0.4.0); required otherwise. An explicit value always wins as override. ctrlrelay itself uses bare mirrors under `paths.bare_repos`. |
539
657
  | `dev_branch_template` | string | no | `"fix/issue-{n}"` | Branch-name template for dev-pipeline runs. `{n}` is replaced by the issue number. |
658
+ | `agent_type` | string | no | `null` | Which agent backend runs this repo's sessions. `null` means `agent.type`. See [agent backends](#agent-backends). A value no adapter answers to stops the daemon at startup, naming this repo. |
540
659
  | `automation` | object | no | (defaults) | See [automation](#repos-automation). |
541
660
  | `code_review` | object | no | (defaults) | Review run over an agent branch before its PR is handed over. See [code_review](#code_review). |
542
661
  | `deploy` | object | no | `null` | Reserved for deploy policy. Currently surfaced in `ctrlrelay config repos` but otherwise inert. |
@@ -34,12 +34,19 @@ and restart on failure — the templates below already do this.
34
34
 
35
35
  Recommended: let `ctrlrelay install launchd` render the plists for you.
36
36
  It substitutes `${USER}`, `${HOME}`, the absolute `ctrlrelay` path,
37
- your working directory, and (when exported) `CTRLRELAY_TELEGRAM_TOKEN`
38
- into the in-package templates and writes them to
39
- `~/Library/LaunchAgents/`.
37
+ your working directory, and (when exported) your chat token into the
38
+ in-package templates and writes them to `~/Library/LaunchAgents/`.
39
+
40
+ The token goes into the plist under the variable your transport names
41
+ in `transport.<type>.bot_token_env` — `CTRLRELAY_TELEGRAM_TOKEN` for
42
+ Telegram, `CTRLRELAY_MATTERMOST_TOKEN` for Mattermost, or whatever you
43
+ renamed that setting to. The command reads your `orchestrator.yaml` to
44
+ find out which; pass `--config` if it is somewhere the usual search
45
+ does not look. Export that variable before running, or the plist keeps
46
+ a placeholder and the command says which variable to export.
40
47
 
41
48
  ```bash
42
- export CTRLRELAY_TELEGRAM_TOKEN=your-bot-token
49
+ export CTRLRELAY_MATTERMOST_TOKEN=your-bot-token # or _TELEGRAM_, per your config
43
50
  ctrlrelay install launchd \
44
51
  --workdir ~/Projects/ctrlrelay \
45
52
  --label-prefix com.yourname \
@@ -158,8 +165,13 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.ctrlrelay-po
158
165
  Recommended: `ctrlrelay install systemd` renders the user unit files
159
166
  for you (no root needed) and writes them to `~/.config/systemd/user/`.
160
167
 
168
+ As on macOS, the bridge and poller units carry the token under the
169
+ variable `transport.<type>.bot_token_env` names, read from your
170
+ `orchestrator.yaml` (`--config` to point at a different one). Export
171
+ that variable before running.
172
+
161
173
  ```bash
162
- export CTRLRELAY_TELEGRAM_TOKEN=your-bot-token
174
+ export CTRLRELAY_MATTERMOST_TOKEN=your-bot-token # or _TELEGRAM_, per your config
163
175
  ctrlrelay install systemd \
164
176
  --workdir ~/Projects/ctrlrelay \
165
177
  --poller-interval 300
@@ -190,6 +202,8 @@ After=network-online.target
190
202
  [Service]
191
203
  Type=simple
192
204
  WorkingDirectory=%h/Projects/ctrlrelay
205
+ # The variable your transport names in bot_token_env — use
206
+ # CTRLRELAY_MATTERMOST_TOKEN on a Mattermost config.
193
207
  Environment=CTRLRELAY_TELEGRAM_TOKEN=your-bot-token-here
194
208
  ExecStart=%h/.local/bin/ctrlrelay bridge start --foreground
195
209
  Restart=always
@@ -211,6 +225,8 @@ After=network-online.target ctrlrelay-bridge.service
211
225
  [Service]
212
226
  Type=simple
213
227
  WorkingDirectory=%h/Projects/ctrlrelay
228
+ # The variable your transport names in bot_token_env — use
229
+ # CTRLRELAY_MATTERMOST_TOKEN on a Mattermost config.
214
230
  Environment=CTRLRELAY_TELEGRAM_TOKEN=your-bot-token-here
215
231
  ExecStart=%h/.local/bin/ctrlrelay poller start --foreground --interval 300
216
232
  Restart=always
@@ -315,6 +331,23 @@ Added in #249:
315
331
  | `node.open_questions` | How many questions are waiting for an answer. |
316
332
  | `fleet` | `{peers: [...]}` with one item per `fleet.peers` URL: `url`, `name`, `reachable`, `checked_at`, `error`, `state`, `node` (that peer's `node` block), `last_known` (its last block while it does not answer) and `needs_you`. `null` when no peers are configured. |
317
333
 
334
+ Added in #230: `numbers` is how this machine's runs went, for the last 7 and
335
+ the last 30 days (`numbers.windows`, each with `days`, `since`/`since_ts`,
336
+ `until`/`until_ts`). The page shows it in the **Numbers** box, with a table
337
+ per repo under it. Numbers across machines are not here; they belong to the
338
+ control plane. Every field is defined in `src/ctrlrelay/web/numbers.py`.
339
+
340
+ | Field (in `overall` and in each `by_repo` item) | What |
341
+ |---|---|
342
+ | `started` | Runs (rows in the sessions table) started in the window, both ends included. A blocked run resumed after an answer is still one run. |
343
+ | `done`, `failed`, `blocked`, `running`, `other` | How many of those runs have that status now. `running` includes a run left behind by a daemon that stopped mid-run. The five add up to `started`. |
344
+ | `success_rate` | `done / (done + failed + blocked)`, 0 to 1; `null` when none has finished. |
345
+ | `time_to_pr_s`, `time_to_merge_s` | `{median, n}`: seconds from the run's start to its first `pr_opened` (or `merged`) event, over the `n` runs that have one. Events stamped after now are not read. `null` when events cannot be read, or when a run in the window started before the oldest event still kept: its PR event may have been deleted. The daemon deletes events older than `web.history_days` (7 by default), so with the default the 30-day times are `null`. **To get them, set `web.history_days` to 30**; they appear once the events kept reach back past every run in the window (up to 30 days after the change, since deleted events do not come back). |
346
+ | `came_back` | Distinct issues dispatched again in the window after an earlier run on the same issue had ended `done`. The window's `came_back_issues` lists them (`repo`, `issue_number`, `url`; at most 50). An issue reopened on GitHub and not picked up again is not counted: ctrlrelay does not record reopening. |
347
+ | `cost_per_run`, `stuck` | Always `null` for now: cost per run is not recorded yet (#223), and no run records that it was stuck (#224). The window's `unavailable` says this, and why any other field is `null`. |
348
+
349
+ `numbers` is `null`, with a line in `errors`, when the sessions cannot be read.
350
+
318
351
  **The page with peers.** A **Machines** strip shows one card per machine,
319
352
  this one first: state, slots, 5-hour and 7-day usage, heartbeat age. A card is
320
353
  red when the machine is paused, silent, unknown, not reachable, or not read
@@ -224,10 +224,14 @@ schedules:
224
224
  The poller registers an APScheduler job that runs `personalization pull`
225
225
  on this schedule. Two safety rails:
226
226
 
227
- - **Skip-on-dirty.** If the working tree has uncommitted changes, the
228
- auto-pull skips with a `working tree dirty` summary. A daemon
229
- rebasing under your unsaved edits is exactly the surprise we don't
230
- want.
227
+ - **Refuse-on-dirty, loudly.** If the working tree has uncommitted
228
+ changes, the auto-pull does not rebase (a daemon rebasing under your
229
+ unsaved edits is exactly the surprise we don't want) but it is **not**
230
+ a quiet success: the run fails, is logged at error level as
231
+ `auto-pull BLOCKED: working tree dirty (...)`, and names the dirty
232
+ entries. It still fetches, so the line carries `behind N`.
233
+ Every run reports `behind N`; a machine drifting is a number in the
234
+ log, not silence (#287: one machine sat 36 commits behind for weeks).
231
235
  - **No adoption.** Adoption is an init-time concern. The auto-pull
232
236
  re-wire phase runs with `adopt=False` so a background sync never
233
237
  silently moves files. New entries in `paths` that have no remote
@@ -293,9 +297,32 @@ unrelated, project-scoped feature.
293
297
  A write to `~/.claude/CLAUDE.md` is a write to
294
298
  `~/.ctrlrelay/personalization/global/CLAUDE.md`. After editing, run
295
299
  `ctrlrelay personalization push`. If you forget, the auto-pull's
296
- skip-on-dirty rail keeps things safe — the daemon won't rebase under
297
- your unsaved work — but your edit doesn't reach other machines until
298
- you push.
300
+ refuse-on-dirty rail keeps things safe — the daemon won't rebase under
301
+ your unsaved work and says so at error level — but your edit doesn't
302
+ reach other machines until you push.
303
+
304
+ ### Files an app rewrites in place (`settings.json`)
305
+
306
+ If a target such as `~/.claude/settings.json` is a symlink into the
307
+ checkout, the app that owns it writes straight into a tracked file: any
308
+ per-viewer preference it records (and a mere key reordering on rewrite)
309
+ dirties the tree and blocks every later pull.
310
+
311
+ **How to handle this is not decided yet** (#287). Two things are known
312
+ not to work: committing the app-written keys (one machine's preference
313
+ would reach every machine as if it were policy), and resetting the file
314
+ on a loop (the app writes it again). One way out is for a machine whose
315
+ live file carries keys the synced one lacks to keep a real file instead
316
+ of the symlink. Until a decision lands, the `BLOCKED` line above is the
317
+ signal that a machine is stuck this way.
318
+
319
+ ### Git identity
320
+
321
+ `push` pre-flights the author identity before staging: it falls back to a
322
+ repo-local `ctrlrelay <ctrlrelay@local>` when none is configured, then
323
+ asks git (`git var GIT_AUTHOR_IDENT`) and refuses with
324
+ `git identity unresolvable` if it still cannot commit, rather than
325
+ leaving a dirty tree behind.
299
326
 
300
327
  ### Allowlist enforcement
301
328