ctrlrelay 0.17.0__tar.gz → 0.17.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 (192) hide show
  1. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/CHANGELOG.md +35 -1
  2. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/PKG-INFO +1 -1
  3. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/cli.md +1 -1
  4. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/operations.md +21 -5
  5. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/pyproject.toml +1 -1
  6. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/cli.py +65 -2
  7. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/worktree.py +117 -20
  8. ctrlrelay-0.17.1/src/ctrlrelay/install.py +323 -0
  9. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/setup.py +34 -8
  10. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/launchd/bridge.plist.template +9 -2
  11. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/launchd/poller.plist.template +9 -2
  12. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/systemd/bridge.service.template +7 -1
  13. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/systemd/poller.service.template +7 -1
  14. ctrlrelay-0.17.1/tests/test_install.py +870 -0
  15. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_setup.py +115 -0
  16. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_worktree.py +216 -13
  17. ctrlrelay-0.17.0/src/ctrlrelay/install.py +0 -204
  18. ctrlrelay-0.17.0/tests/test_install.py +0 -365
  19. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/dependabot.yml +0 -0
  23. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/workflows/build.yml +0 -0
  24. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/workflows/cla.yml +0 -0
  25. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/workflows/pages.yml +0 -0
  26. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/workflows/publish.yml +0 -0
  27. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.github/workflows/test.yml +0 -0
  28. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/.gitignore +0 -0
  29. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/CODE_OF_CONDUCT.md +0 -0
  30. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/CONTRIBUTING.md +0 -0
  31. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/LICENSE +0 -0
  32. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/README.md +0 -0
  33. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/SECURITY.md +0 -0
  34. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/config/orchestrator.yaml.example +0 -0
  35. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/Gemfile +0 -0
  36. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/_config.yml +0 -0
  37. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/adding-a-machine.md +0 -0
  38. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/architecture.md +0 -0
  39. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/bridge.md +0 -0
  40. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/configuration.md +0 -0
  41. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/development.md +0 -0
  42. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/feedback-loop.md +0 -0
  43. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/getting-started.md +0 -0
  44. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/index.md +0 -0
  45. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/personalization.md +0 -0
  46. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  47. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  48. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/__init__.py +0 -0
  49. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/__init__.py +0 -0
  50. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/__main__.py +0 -0
  51. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/factory.py +0 -0
  52. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/handler.py +0 -0
  53. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  54. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/node_tag.py +0 -0
  55. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/protocol.py +0 -0
  56. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/server.py +0 -0
  57. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  58. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/__init__.py +0 -0
  59. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/agent_env.py +0 -0
  60. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/archived.py +0 -0
  61. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/audit.py +0 -0
  62. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/binaries.py +0 -0
  63. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/checkpoint.py +0 -0
  64. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/claims.py +0 -0
  65. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/code_review.py +0 -0
  66. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/config.py +0 -0
  67. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/dispatch_health.py +0 -0
  68. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/dispatcher.py +0 -0
  69. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/events.py +0 -0
  70. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/github.py +0 -0
  71. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/github_budget.py +0 -0
  72. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/heartbeat.py +0 -0
  73. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/job_claims.py +0 -0
  74. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/network.py +0 -0
  75. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/node_check.py +0 -0
  76. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/obs.py +0 -0
  77. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/poller.py +0 -0
  78. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/pr_verifier.py +0 -0
  79. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/pr_watcher.py +0 -0
  80. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/publish.py +0 -0
  81. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/question_expiry.py +0 -0
  82. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/redact.py +0 -0
  83. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/scheduler.py +0 -0
  84. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/state.py +0 -0
  85. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/core/usage.py +0 -0
  86. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/dashboard/__init__.py +0 -0
  87. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/dashboard/client.py +0 -0
  88. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/gh_protocol.py +0 -0
  89. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/personalization/__init__.py +0 -0
  90. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/personalization/manager.py +0 -0
  91. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/personalization/paths.py +0 -0
  92. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/pipelines/__init__.py +0 -0
  93. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/pipelines/base.py +0 -0
  94. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/pipelines/dev.py +0 -0
  95. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  96. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/pipelines/secops.py +0 -0
  97. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/pipelines/task.py +0 -0
  98. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/__init__.py +0 -0
  99. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  100. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  101. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  102. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  103. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/transports/__init__.py +0 -0
  104. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/transports/base.py +0 -0
  105. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/transports/file_mock.py +0 -0
  106. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/transports/socket_client.py +0 -0
  107. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/__init__.py +0 -0
  108. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/fleet.py +0 -0
  109. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/health.py +0 -0
  110. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/links.py +0 -0
  111. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/server.py +0 -0
  112. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/static/index.html +0 -0
  113. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/status.py +0 -0
  114. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/transcript.py +0 -0
  115. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/src/ctrlrelay/web/waiting.py +0 -0
  116. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/__init__.py +0 -0
  117. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/_poller_fakes.py +0 -0
  118. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/_publish_fakes.py +0 -0
  119. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/conftest.py +0 -0
  120. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/fixtures/gh_search_issues.json +0 -0
  121. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  122. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/fixtures/transcript_sample.jsonl +0 -0
  123. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_agent_no_keys.py +0 -0
  124. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_agent_sandbox.py +0 -0
  125. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_archived.py +0 -0
  126. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_audit.py +0 -0
  127. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_binary_resolution.py +0 -0
  128. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_bridge_protocol.py +0 -0
  129. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_bridge_server.py +0 -0
  130. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_bridge_start_command.py +0 -0
  131. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_chat_machines.py +0 -0
  132. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_checkpoint.py +0 -0
  133. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_claim_reconciliation.py +0 -0
  134. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_cli_ci_wait.py +0 -0
  135. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_cli_dev.py +0 -0
  136. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_cli_repos.py +0 -0
  137. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_cli_secops.py +0 -0
  138. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_cli_start.py +0 -0
  139. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_cli_version.py +0 -0
  140. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_code_review.py +0 -0
  141. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_config.py +0 -0
  142. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_dashboard_client.py +0 -0
  143. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_dev_integration.py +0 -0
  144. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_dev_pipeline.py +0 -0
  145. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_dispatch_health.py +0 -0
  146. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_dispatch_outcome_hook.py +0 -0
  147. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_dispatcher.py +0 -0
  148. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_docs_site.py +0 -0
  149. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_event_emission.py +0 -0
  150. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_events.py +0 -0
  151. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_fleet.py +0 -0
  152. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_gh_protocol.py +0 -0
  153. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_github.py +0 -0
  154. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_github_budget.py +0 -0
  155. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_issue_outcome_announcement.py +0 -0
  156. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_job_claims.py +0 -0
  157. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_mattermost_handler.py +0 -0
  158. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_network.py +0 -0
  159. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_node_check.py +0 -0
  160. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_node_claims.py +0 -0
  161. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_notify_transport.py +0 -0
  162. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_obs.py +0 -0
  163. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_personalization.py +0 -0
  164. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_pipeline_base.py +0 -0
  165. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_poller.py +0 -0
  166. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_poller_search.py +0 -0
  167. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_post_merge.py +0 -0
  168. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_pr_verifier.py +0 -0
  169. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_pr_watcher.py +0 -0
  170. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_pre_dispatch_retry.py +0 -0
  171. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_publish_failure_retry.py +0 -0
  172. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_question_backoff.py +0 -0
  173. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_question_expiry.py +0 -0
  174. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_redact.py +0 -0
  175. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_relay_alert_off_lock.py +0 -0
  176. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_resume_prompt_contract.py +0 -0
  177. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_scheduler.py +0 -0
  178. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_secops_integration.py +0 -0
  179. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_secops_pipeline.py +0 -0
  180. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_state.py +0 -0
  181. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_task_pipeline.py +0 -0
  182. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_telegram_handler.py +0 -0
  183. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_transport.py +0 -0
  184. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_transport_resolution.py +0 -0
  185. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_unreviewed_pr_announcement.py +0 -0
  186. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_usage.py +0 -0
  187. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_web_health.py +0 -0
  188. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_web_links.py +0 -0
  189. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_web_server.py +0 -0
  190. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_web_status.py +0 -0
  191. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_web_transcript.py +0 -0
  192. {ctrlrelay-0.17.0 → ctrlrelay-0.17.1}/tests/test_web_waiting.py +0 -0
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.1] - 2026-11-01
11
+
12
+ ### Fixed
13
+
14
+ - Installed bridge and poller units carry the token variable the configured
15
+ transport names, not a fixed Telegram one. `ctrlrelay install
16
+ launchd|systemd` and `ctrlrelay setup --install-daemons` read
17
+ `transport.<type>.bot_token_env` and render `Environment=<that
18
+ name>=<value>` (systemd) or the matching plist entry (launchd), with the
19
+ value taken from that variable in the installing shell. A Mattermost
20
+ install previously got a unit naming `CTRLRELAY_TELEGRAM_TOKEN`, which it
21
+ never sets, so the bridge could not log in once systemd or launchd started
22
+ it — the server here only worked because the line had been added by hand.
23
+ The unresolved-variable hint names the same variable, and the install
24
+ commands take `--config` to say which `orchestrator.yaml` to read
25
+ (closes #267)
26
+ - Updating a repository's clone no longer deletes local branches GitHub does
27
+ not have. The fetch paired `--prune` with `refs/heads/*:refs/heads/*`, which
28
+ tells git to delete every local branch missing from origin: a branch made in
29
+ the clone went with `- [deleted] (none) -> held`. The worktree stayed,
30
+ but its branch ref was gone, so a blocked session whose branch was never
31
+ pushed lost the branch the next time any run touched that repo, and its
32
+ commits could later be garbage-collected. The fetch is now two: the
33
+ `dependabot/*` branches, which never carry local work and are the refs that
34
+ actually pile up, are still pruned; every other branch is fetched without
35
+ `--prune`. The cost is that a branch GitHub deleted now lingers in the
36
+ clone, since a prune cannot tell it from one GitHub never had — the
37
+ branch-reuse path already recognises a local-only ref and decides whether
38
+ its commits are merged. Because a renamed default branch is no longer
39
+ pruned either, each update also asks GitHub which branch is the default
40
+ and points the clone at it when it changed and the branch here is at origin's
41
+ commit (#243).
42
+
10
43
  ## [0.17.0] - 2026-11-01
11
44
 
12
45
  Polling costs one GitHub search instead of one call per repo. **No config
@@ -2076,7 +2109,8 @@ pipeline).
2076
2109
  per-phase implementation plans (Phase 0 through Phase 4).
2077
2110
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2078
2111
 
2079
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.0...HEAD
2112
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...HEAD
2113
+ [0.17.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.0...v0.17.1
2080
2114
  [0.17.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.4...v0.17.0
2081
2115
  [0.16.4]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.3...v0.16.4
2082
2116
  [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.17.1
4
4
  Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
5
  Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
6
  Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
@@ -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. |
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrlrelay"
3
- version = "0.17.0"
3
+ version = "0.17.1"
4
4
  description = "Local-first orchestrator for headless coding agents across multiple GitHub repos"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -5052,6 +5052,12 @@ def setup(
5052
5052
  console.print(" daemon unit files:")
5053
5053
  for p in result.daemon_units:
5054
5054
  console.print(f" - {p}")
5055
+ if result.daemon_unresolved:
5056
+ console.print(
5057
+ " [yellow]warning:[/yellow] the units still carry "
5058
+ f"{', '.join('${'+v+'}' for v in result.daemon_unresolved)}; "
5059
+ "export it and re-run with --force, or edit the files by hand."
5060
+ )
5055
5061
  console.print(
5056
5062
  "\n[bold]Next:[/bold] bootstrap the daemons via:\n"
5057
5063
  " launchctl bootstrap gui/$(id -u) <plist>"
@@ -5463,13 +5469,50 @@ def _print_install_summary(
5463
5469
  f" [yellow]warning:[/yellow] unresolved template variables: "
5464
5470
  f"{', '.join('${'+v+'}' for v in unit.unresolved)}"
5465
5471
  )
5466
- if "CTRLRELAY_TELEGRAM_TOKEN" in unit.unresolved:
5472
+ # Name the variable this install actually needs. It is
5473
+ # whatever `transport.<type>.bot_token_env` says, so hinting
5474
+ # a fixed CTRLRELAY_TELEGRAM_TOKEN sent a Mattermost
5475
+ # operator to export a variable nothing reads (#267).
5476
+ if unit.token_env_var in unit.unresolved:
5467
5477
  console.print(
5468
- " [dim]hint: export CTRLRELAY_TELEGRAM_TOKEN before "
5478
+ f" [dim]hint: export {unit.token_env_var} before "
5469
5479
  "re-running, or edit the file by hand.[/dim]"
5470
5480
  )
5471
5481
 
5472
5482
 
5483
+ def _install_token_env_var(config_path: str | None) -> str:
5484
+ """Which env var the rendered units should carry the chat token in.
5485
+
5486
+ Reads the active transport's ``bot_token_env`` so a Mattermost box
5487
+ gets its own variable (#267). Exits non-zero on a config that was
5488
+ asked for and cannot be loaded: rendering the Telegram default on
5489
+ the strength of a path the operator got wrong would write a unit for
5490
+ a transport they are not running. Finding no config at all is a
5491
+ different case — nothing names a transport, so the historical
5492
+ default stands and the summary flags it as unresolved.
5493
+ """
5494
+ from ctrlrelay.install import DEFAULT_TOKEN_ENV_VAR, token_env_var_from_config
5495
+
5496
+ if config_path is None:
5497
+ try:
5498
+ path = resolve_config_path(None)
5499
+ except ConfigError:
5500
+ console.print(
5501
+ f"[yellow]No orchestrator.yaml found, so nothing names the chat "
5502
+ f"transport: the units carry ${DEFAULT_TOKEN_ENV_VAR}. Pass "
5503
+ "--config to use your transport's token variable.[/yellow]"
5504
+ )
5505
+ return DEFAULT_TOKEN_ENV_VAR
5506
+ else:
5507
+ path = resolve_config_path(config_path)
5508
+ try:
5509
+ config = load_config(path)
5510
+ except ConfigError as e:
5511
+ console.print(f"[red]Error loading config:[/red] {e}")
5512
+ raise typer.Exit(1)
5513
+ return token_env_var_from_config(config)
5514
+
5515
+
5473
5516
  @install_app.command("launchd")
5474
5517
  def install_launchd(
5475
5518
  workdir: Path = typer.Option(
@@ -5499,9 +5542,18 @@ def install_launchd(
5499
5542
  "--dry-run",
5500
5543
  help="Print what would be written without touching the filesystem.",
5501
5544
  ),
5545
+ config_path: str | None = typer.Option(
5546
+ None,
5547
+ "--config",
5548
+ "-c",
5549
+ help="Path to orchestrator.yaml (default: auto-discover; see $CTRLRELAY_CONFIG).",
5550
+ ),
5502
5551
  ) -> None:
5503
5552
  """Render and install bridge + poller plists for macOS launchd.
5504
5553
 
5554
+ The plists carry the chat token in the variable the configured
5555
+ transport names, so export that variable before running.
5556
+
5505
5557
  After running, load the agents with:
5506
5558
  launchctl bootstrap gui/$(id -u) <plist-path>
5507
5559
  """
@@ -5512,6 +5564,7 @@ def install_launchd(
5512
5564
  label_prefix=label_prefix,
5513
5565
  poller_interval=poller_interval,
5514
5566
  target_dir=target_dir,
5567
+ token_env_var=_install_token_env_var(config_path),
5515
5568
  )
5516
5569
  written: list[Path] = []
5517
5570
  if not dry_run:
@@ -5547,9 +5600,18 @@ def install_systemd(
5547
5600
  "--dry-run",
5548
5601
  help="Print what would be written without touching the filesystem.",
5549
5602
  ),
5603
+ config_path: str | None = typer.Option(
5604
+ None,
5605
+ "--config",
5606
+ "-c",
5607
+ help="Path to orchestrator.yaml (default: auto-discover; see $CTRLRELAY_CONFIG).",
5608
+ ),
5550
5609
  ) -> None:
5551
5610
  """Render and install bridge, poller and status-page systemd user units (Linux).
5552
5611
 
5612
+ The bridge and poller units carry the chat token in the variable the
5613
+ configured transport names, so export that variable before running.
5614
+
5553
5615
  After running, enable with:
5554
5616
  systemctl --user daemon-reload
5555
5617
  systemctl --user enable --now ctrlrelay-bridge.service
@@ -5562,6 +5624,7 @@ def install_systemd(
5562
5624
  workdir=workdir,
5563
5625
  poller_interval=poller_interval,
5564
5626
  target_dir=target_dir,
5627
+ token_env_var=_install_token_env_var(config_path),
5565
5628
  )
5566
5629
  written: list[Path] = []
5567
5630
  if not dry_run:
@@ -51,6 +51,13 @@ _GIT_TRANSFER_TIMEOUT_SECONDS = 1800
51
51
  # a non-fast-forward rejection.
52
52
  _BOT_BRANCH_REFSPEC = "+refs/heads/dependabot/*:refs/heads/dependabot/*"
53
53
 
54
+ # ...and the same pattern as a negative refspec, so the fetch that
55
+ # carries every other branch leaves them alone. Keeping the two fetches
56
+ # disjoint is what lets only one of them prune (#243), and it also
57
+ # removes the ordering dependency that made a rebased bot branch a
58
+ # tolerated rejection under the plain refspec.
59
+ _NOT_BOT_BRANCHES_REFSPEC = "^refs/heads/dependabot/*"
60
+
54
61
 
55
62
  def _rejections_are_only_non_fast_forward(stderr: str) -> list[str]:
56
63
  """Return the refs a fetch declined to rewind, or ``[]`` if the
@@ -1335,23 +1342,36 @@ class WorktreeManager:
1335
1342
  gets to inspect divergence and decide.
1336
1343
  - Local and origin equal: no-op.
1337
1344
 
1338
- ``--prune`` drops refs that no longer exist on origin so
1339
- deleted branches don't linger forever.
1340
-
1341
- Two things keep that non-force refspec from failing the
1342
- caller, both learned from a secops sweep that died on
1343
- ``AInvirion/zzsites`` with ``main`` already updated:
1344
-
1345
- - ``dependabot/*`` is fetched with a force refspec first.
1345
+ Two fetches, not one, and only the first prunes (#243).
1346
+ ``--prune`` is per command and scoped to that command's
1347
+ refspecs, so ``--prune`` paired with ``refs/heads/*`` deletes
1348
+ every local branch origin does not have — measured on 31 Oct,
1349
+ a branch made in the clone went with
1350
+ ``- [deleted] (none) -> held``. A blocked session whose
1351
+ branch was never pushed lost it the next time any run touched
1352
+ that repo, and its commits were then reachable only from the
1353
+ worktree's HEAD, one ``git gc`` from gone. So:
1354
+
1355
+ - ``dependabot/*`` with a force refspec **and** ``--prune``.
1346
1356
  Dependabot rebases its branches, which rewrites their tips,
1347
- so under a plain refspec every rebase is a rejection. These
1348
- branches are bot-authored and never carry local work, so
1349
- rewinding one destroys nothing. Twelve bare repos were
1350
- holding such a ref when this was found.
1351
- - Any rejection that survives is tolerated rather than raised.
1352
- Git exits 1 on a declined rewind even when every other ref
1353
- applied, and that exit code was aborting the whole session
1354
- for a branch it had correctly protected.
1357
+ so under a plain refspec every rebase is a rejection — a
1358
+ secops sweep died on ``AInvirion/zzsites`` that way with
1359
+ ``main`` already updated, and twelve bare repos were holding
1360
+ such a ref. These branches are bot-authored, never carry
1361
+ local work, and are the ones that actually pile up, so this
1362
+ is where pruning belongs.
1363
+ - Everything else with the plain refspec and **no** ``--prune``,
1364
+ with ``dependabot/*`` excluded by a negative refspec so the
1365
+ two never overlap. A branch origin deleted therefore lingers
1366
+ here; that is the trade, and the branch-reuse path in
1367
+ :meth:`create_worktree_with_new_branch` already has to
1368
+ recognise a local-only ref and decide whether its commits
1369
+ are already merged.
1370
+
1371
+ Any rejection that survives either fetch is tolerated rather
1372
+ than raised. Git exits 1 on a declined rewind even when every
1373
+ other ref applied, and that exit code was aborting the whole
1374
+ session for a branch it had correctly protected.
1355
1375
 
1356
1376
  The default branch is the exception: tolerating a rejection
1357
1377
  there would swap a loud failure for work done quietly on a stale
@@ -1391,11 +1411,26 @@ class WorktreeManager:
1391
1411
  for branch in await self._checked_out_branches(bare_path)
1392
1412
  if branch != default_branch
1393
1413
  ] if default_branch else []
1414
+ # The bot branches: force refspec, and the only --prune.
1415
+ # `skip` applies here too — git refuses the WHOLE fetch over
1416
+ # a checked-out branch, and a secops run can have a
1417
+ # dependabot branch checked out. Excluding one also spares
1418
+ # it the prune, which is the answer we want anyway.
1419
+ await self._run_git(
1420
+ "fetch", "--prune", "origin",
1421
+ _BOT_BRANCH_REFSPEC,
1422
+ *skip,
1423
+ cwd=bare_path,
1424
+ timeout=_GIT_TRANSFER_TIMEOUT_SECONDS,
1425
+ tolerate_non_fast_forward=True,
1426
+ )
1427
+ # Every other branch: no --prune, so a local-only branch
1428
+ # survives (#243).
1394
1429
  try:
1395
1430
  await self._run_git(
1396
- "fetch", "--prune", "origin",
1397
- _BOT_BRANCH_REFSPEC,
1431
+ "fetch", "origin",
1398
1432
  "refs/heads/*:refs/heads/*",
1433
+ _NOT_BOT_BRANCHES_REFSPEC,
1399
1434
  *skip,
1400
1435
  cwd=bare_path,
1401
1436
  timeout=_GIT_TRANSFER_TIMEOUT_SECONDS,
@@ -1407,11 +1442,72 @@ class WorktreeManager:
1407
1442
  )
1408
1443
  except DefaultBranchDivergedError as e:
1409
1444
  await self._set_aside_and_reclone(repo, bare_path, e)
1445
+ else:
1446
+ await self._follow_origin_default(repo, bare_path, default_branch)
1410
1447
  else:
1411
1448
  await self._clone_bare(repo, bare_path)
1412
1449
 
1413
1450
  return bare_path
1414
1451
 
1452
+ async def _follow_origin_default(
1453
+ self, repo: str, bare_path: Path, default_branch: str,
1454
+ ) -> None:
1455
+ """Point the clone's HEAD at origin's default branch when origin
1456
+ changed it. The fetch no longer prunes branches (#243), so a
1457
+ renamed default's old branch stays here, and without this the
1458
+ clone would keep cutting sessions from it. HEAD moves only when
1459
+ the new default is here at the commit origin reports, so a stale
1460
+ or diverged local branch is never adopted; anything else is left
1461
+ for the next update and logged, never silent."""
1462
+ try:
1463
+ out = await self._run_git(
1464
+ "ls-remote", "--symref", "origin", "HEAD",
1465
+ cwd=bare_path, timeout=_GIT_TRANSFER_TIMEOUT_SECONDS,
1466
+ )
1467
+ except WorktreeError as exc:
1468
+ log_event(
1469
+ _logger, "worktree.default_branch_unchecked", repo=repo,
1470
+ reason=str(exc)[:200],
1471
+ )
1472
+ return
1473
+ prefix, suffix = "ref: refs/heads/", "\tHEAD"
1474
+ new = next(
1475
+ (line[len(prefix):-len(suffix)] for line in out.splitlines()
1476
+ if line.startswith(prefix) and line.endswith(suffix)),
1477
+ "",
1478
+ )
1479
+ remote_sha = next(
1480
+ (line.split("\t", 1)[0] for line in out.splitlines()
1481
+ if line.endswith("\tHEAD") and not line.startswith("ref: ")),
1482
+ "",
1483
+ )
1484
+ if not new or new == default_branch:
1485
+ return
1486
+ try:
1487
+ local_sha = (await self._run_git(
1488
+ "rev-parse", "--verify", "--quiet", f"refs/heads/{new}",
1489
+ cwd=bare_path,
1490
+ )).strip()
1491
+ except WorktreeError:
1492
+ local_sha = ""
1493
+ if not remote_sha or local_sha != remote_sha:
1494
+ log_event(
1495
+ _logger, "worktree.default_branch_not_followed", repo=repo,
1496
+ old=default_branch or None, new=new,
1497
+ reason="not here yet" if not local_sha else "not current",
1498
+ )
1499
+ return
1500
+ try:
1501
+ await self._run_git(
1502
+ "symbolic-ref", "HEAD", f"refs/heads/{new}", cwd=bare_path,
1503
+ )
1504
+ except WorktreeError:
1505
+ return
1506
+ log_event(
1507
+ _logger, "worktree.default_branch_changed", repo=repo,
1508
+ old=default_branch or None, new=new,
1509
+ )
1510
+
1415
1511
  async def _checked_out_branches(self, bare_path: Path) -> list[str]:
1416
1512
  """Branches checked out in this clone's linked worktrees."""
1417
1513
  listing = await self._run_git(
@@ -1444,8 +1540,9 @@ class WorktreeManager:
1444
1540
 
1445
1541
  The old clone is never deleted here, and the operator is told
1446
1542
  where it went so they can delete it. It holds what the failed
1447
- fetch left: that fetch's ``--prune`` may already have dropped
1448
- local branches origin does not have, as every fetch here does.
1543
+ fetch left: the bot-branch fetch that ran before it prunes
1544
+ ``dependabot/*`` refs origin no longer has. Branches outside
1545
+ that namespace are not pruned by any fetch here (#243).
1449
1546
 
1450
1547
  Refused, re-raising ``cause``, while any worktree is registered
1451
1548
  in the clone: a BLOCKED session keeps its worktree for the