ctrlrelay 0.14.0__tar.gz → 0.16.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 (187) hide show
  1. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/CHANGELOG.md +164 -1
  2. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/PKG-INFO +1 -1
  3. ctrlrelay-0.16.0/docs/adding-a-machine.md +120 -0
  4. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/architecture.md +8 -0
  5. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/configuration.md +271 -7
  6. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/feedback-loop.md +6 -3
  7. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/operations.md +251 -4
  8. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +20 -2
  9. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/pyproject.toml +1 -1
  10. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/__main__.py +8 -0
  11. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/mattermost_handler.py +13 -0
  12. ctrlrelay-0.16.0/src/ctrlrelay/bridge/node_tag.py +50 -0
  13. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/server.py +110 -4
  14. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/telegram_handler.py +52 -1
  15. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/cli.py +492 -4
  16. ctrlrelay-0.16.0/src/ctrlrelay/core/agent_env.py +528 -0
  17. ctrlrelay-0.16.0/src/ctrlrelay/core/claims.py +628 -0
  18. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/config.py +161 -1
  19. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/dispatcher.py +301 -19
  20. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/github.py +199 -6
  21. ctrlrelay-0.16.0/src/ctrlrelay/core/github_budget.py +416 -0
  22. ctrlrelay-0.16.0/src/ctrlrelay/core/heartbeat.py +99 -0
  23. ctrlrelay-0.16.0/src/ctrlrelay/core/job_claims.py +154 -0
  24. ctrlrelay-0.16.0/src/ctrlrelay/core/node_check.py +217 -0
  25. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/poller.py +112 -3
  26. ctrlrelay-0.16.0/src/ctrlrelay/core/publish.py +189 -0
  27. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/scheduler.py +57 -0
  28. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/state.py +45 -0
  29. ctrlrelay-0.16.0/src/ctrlrelay/core/usage.py +465 -0
  30. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/worktree.py +439 -26
  31. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/dev.py +506 -59
  32. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/secops.py +46 -0
  33. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/task.py +198 -28
  34. ctrlrelay-0.16.0/src/ctrlrelay/web/fleet.py +515 -0
  35. ctrlrelay-0.16.0/src/ctrlrelay/web/health.py +167 -0
  36. ctrlrelay-0.16.0/src/ctrlrelay/web/links.py +35 -0
  37. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/server.py +88 -4
  38. ctrlrelay-0.16.0/src/ctrlrelay/web/static/index.html +964 -0
  39. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/status.py +212 -34
  40. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/transcript.py +49 -0
  41. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/waiting.py +118 -14
  42. ctrlrelay-0.16.0/tests/_publish_fakes.py +25 -0
  43. ctrlrelay-0.16.0/tests/fixtures/stream_json_rate_limit.jsonl +4 -0
  44. ctrlrelay-0.16.0/tests/test_agent_no_keys.py +1216 -0
  45. ctrlrelay-0.16.0/tests/test_agent_sandbox.py +500 -0
  46. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_bridge_server.py +38 -16
  47. ctrlrelay-0.16.0/tests/test_chat_machines.py +407 -0
  48. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_claim_reconciliation.py +65 -1
  49. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_config.py +2 -2
  50. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dev_integration.py +27 -0
  51. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dev_pipeline.py +77 -70
  52. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dispatch_outcome_hook.py +45 -30
  53. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dispatcher.py +57 -38
  54. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_event_emission.py +29 -0
  55. ctrlrelay-0.16.0/tests/test_fleet.py +549 -0
  56. ctrlrelay-0.16.0/tests/test_github_budget.py +584 -0
  57. ctrlrelay-0.16.0/tests/test_job_claims.py +385 -0
  58. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_mattermost_handler.py +1 -1
  59. ctrlrelay-0.16.0/tests/test_node_check.py +394 -0
  60. ctrlrelay-0.16.0/tests/test_node_claims.py +1031 -0
  61. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_obs.py +4 -0
  62. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_pipeline_base.py +1 -0
  63. ctrlrelay-0.16.0/tests/test_pre_dispatch_retry.py +257 -0
  64. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_question_backoff.py +2 -0
  65. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_relay_alert_off_lock.py +3 -2
  66. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_resume_prompt_contract.py +14 -4
  67. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_secops_integration.py +2 -1
  68. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_secops_pipeline.py +21 -0
  69. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_task_pipeline.py +12 -0
  70. ctrlrelay-0.16.0/tests/test_usage.py +627 -0
  71. ctrlrelay-0.16.0/tests/test_web_health.py +228 -0
  72. ctrlrelay-0.16.0/tests/test_web_links.py +60 -0
  73. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_server.py +287 -2
  74. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_status.py +415 -9
  75. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_transcript.py +96 -0
  76. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_waiting.py +266 -0
  77. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_worktree.py +379 -41
  78. ctrlrelay-0.14.0/src/ctrlrelay/web/static/index.html +0 -477
  79. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  80. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  81. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  82. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/dependabot.yml +0 -0
  83. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/build.yml +0 -0
  84. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/cla.yml +0 -0
  85. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/pages.yml +0 -0
  86. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/publish.yml +0 -0
  87. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/test.yml +0 -0
  88. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.gitignore +0 -0
  89. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/CODE_OF_CONDUCT.md +0 -0
  90. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/CONTRIBUTING.md +0 -0
  91. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/LICENSE +0 -0
  92. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/README.md +0 -0
  93. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/SECURITY.md +0 -0
  94. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/config/orchestrator.yaml.example +0 -0
  95. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/Gemfile +0 -0
  96. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/_config.yml +0 -0
  97. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/bridge.md +0 -0
  98. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/cli.md +0 -0
  99. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/development.md +0 -0
  100. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/getting-started.md +0 -0
  101. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/index.md +0 -0
  102. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/personalization.md +0 -0
  103. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  104. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/__init__.py +0 -0
  105. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  106. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/factory.py +0 -0
  107. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/handler.py +0 -0
  108. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  109. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/__init__.py +0 -0
  110. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/archived.py +0 -0
  111. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/audit.py +0 -0
  112. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/binaries.py +0 -0
  113. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/checkpoint.py +0 -0
  114. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/code_review.py +0 -0
  115. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
  116. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/events.py +0 -0
  117. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/network.py +0 -0
  118. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/obs.py +0 -0
  119. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  120. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  121. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/question_expiry.py +0 -0
  122. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/redact.py +0 -0
  123. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  124. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/dashboard/client.py +0 -0
  125. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/gh_protocol.py +0 -0
  126. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/install.py +0 -0
  127. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/personalization/__init__.py +0 -0
  128. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/personalization/manager.py +0 -0
  129. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/personalization/paths.py +0 -0
  130. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  131. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/base.py +0 -0
  132. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  133. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/setup.py +0 -0
  134. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/__init__.py +0 -0
  135. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  136. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  137. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  138. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  139. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  140. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  141. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  142. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  143. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/__init__.py +0 -0
  144. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/base.py +0 -0
  145. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/file_mock.py +0 -0
  146. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/socket_client.py +0 -0
  147. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/__init__.py +0 -0
  148. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/__init__.py +0 -0
  149. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/conftest.py +0 -0
  150. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/fixtures/gh_search_issues.json +0 -0
  151. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/fixtures/transcript_sample.jsonl +0 -0
  152. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_archived.py +0 -0
  153. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_audit.py +0 -0
  154. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_binary_resolution.py +0 -0
  155. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_bridge_protocol.py +0 -0
  156. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_checkpoint.py +0 -0
  157. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_ci_wait.py +0 -0
  158. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_dev.py +0 -0
  159. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_repos.py +0 -0
  160. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_secops.py +0 -0
  161. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_start.py +0 -0
  162. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_version.py +0 -0
  163. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_code_review.py +0 -0
  164. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dashboard_client.py +0 -0
  165. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dispatch_health.py +0 -0
  166. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_docs_site.py +0 -0
  167. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_events.py +0 -0
  168. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_gh_protocol.py +0 -0
  169. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_github.py +0 -0
  170. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_install.py +0 -0
  171. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_issue_outcome_announcement.py +0 -0
  172. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_network.py +0 -0
  173. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_notify_transport.py +0 -0
  174. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_personalization.py +0 -0
  175. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_poller.py +0 -0
  176. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_post_merge.py +0 -0
  177. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_pr_verifier.py +0 -0
  178. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_pr_watcher.py +0 -0
  179. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_question_expiry.py +0 -0
  180. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_redact.py +0 -0
  181. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_scheduler.py +0 -0
  182. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_setup.py +0 -0
  183. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_state.py +0 -0
  184. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_telegram_handler.py +0 -0
  185. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_transport.py +0 -0
  186. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_transport_resolution.py +0 -0
  187. {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_unreviewed_pr_announcement.py +0 -0
@@ -7,6 +7,167 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.0] - 2026-11-01
11
+
12
+ Several machines can now run ctrlrelay on one GitHub account, each claiming
13
+ an issue before it works it, and the dev and task agents can run in a
14
+ read-only sandbox. Every new config value has a default and nothing is added
15
+ to the database, so for most installs **no config edit is required**. Check
16
+ three things before upgrading:
17
+
18
+ - **A placeholder `node_id` is refused at startup**: `localhost`,
19
+ `localhost.localdomain`, `unknown`, `my-laptop`, `studio-mac` (both were
20
+ examples in earlier docs), `changeme`, `example`, `node`, `hostname`. Set a
21
+ name no other machine on the account uses. A missing `node_id` still
22
+ defaults to the hostname.
23
+ - **If you already run `agent.sandbox: bwrap`**, it is now much stricter:
24
+ by default the agent can write its worktree, its git dirs, a private
25
+ `/tmp`, its own empty `gh` and runtime dirs, the entries of `~/.claude`,
26
+ its own cache and any `sandbox_writable_paths`, minus the read-only and
27
+ hidden paths inside those. Anything else a run wrote
28
+ (a cache in your home, `git config`, submodules, commit signing) fails
29
+ there. It also refuses to start with `CLAUDE_CONFIG_DIR` set or with a
30
+ read-only path that is a replaceable link. Read the sandbox section of the
31
+ configuration docs before upgrading such a machine.
32
+ - **Upgrade every machine on an account to the same version.** Claims now
33
+ name the machine and the process that wrote them, and an older version
34
+ reads them differently. A claim comment left by an older version counts
35
+ as taken by `unknown` and never goes stale: once nothing is working that
36
+ issue, delete the comment to free it. With a shared Mattermost bot, a
37
+ question asked before the upgrade carries no machine tag: answer it with
38
+ `node-a: ...`.
39
+
40
+ ### Added
41
+
42
+ - Several machines on one account: before a dev or task run, the machine
43
+ claims the issue with a comment naming itself, waits
44
+ `claims.race_wait_seconds` and backs off if another machine's claim came
45
+ first; claims are refreshed while the run lasts and taken over once stale
46
+ (`claims.refresh_seconds`, `claims.stale_after_seconds`). A run whose
47
+ machine cannot refresh its claim for longer than the stale time (a long
48
+ GitHub outage) can be taken over while it still runs, so keep the
49
+ machines' clocks synchronised and the stale time well above any outage
50
+ you expect. A run that fails after being resumed from a question leaves
51
+ its claim held; delete the claim comment to free that issue (#252,
52
+ closes #245)
53
+ - The scheduled security sweep runs at most once per repository and
54
+ scheduled time across machines: the first to create the ref
55
+ `refs/ctrlrelay/claims/secops/<window>` in the repository runs it. A
56
+ machine that dies holding a window, or a ref that cannot be created,
57
+ leaves that repository unswept for that window; the next window is new.
58
+ Keep `repos`, `schedules` and `timezone` the same on every machine, or
59
+ they name different windows (#259, closes #246)
60
+ - Chat with several machines: questions are tagged with the machine's name
61
+ and a machine ignores replies to another's. Mattermost machines can share
62
+ one bot and channel with `transport.mattermost.shared: true` (a message
63
+ that is not a reply must start with `node-b: ...`); Telegram needs one bot
64
+ per machine. When a second process starts reading the same bot, Telegram
65
+ cuts off the one already running, which now says so once, in its log and
66
+ in the chat, and stops reading (#260, closes #248)
67
+ - Subscription usage: read from each agent run and shown on the status
68
+ page. New issues wait while the 5-hour or 7-day window is at or above
69
+ `usage.pause_at` (default 90%), or the status is anything but `allowed`,
70
+ until the window's reset (or an hour, when the reset time cannot be read,
71
+ and then the next reading decides); runs already going finish, and a usage that
72
+ could not be read never pauses. An idle probe every `usage.probe_minutes`
73
+ keeps the reading fresh (#254, closes #247)
74
+ - GitHub API budget: watched every `github_api.check_minutes`; polling slows
75
+ by `github_api.slow_factor` while the `core` or `graphql` bucket is below
76
+ `github_api.low_at` (not `search`, which resets every minute), with one
77
+ chat line. When a bucket is used up, calls known to spend it
78
+ (`gh api`, `gh search`) fail at once as rate limited until its reset, and
79
+ the poller resumes after it; other `gh` commands are not held back (#256,
80
+ closes #237)
81
+ - Status page: a Machines strip and who-has-what for every machine listed in
82
+ `fleet.peers` (each machine's `web.allowed_hosts` must hold the name the
83
+ others use for it), plus this machine's usage and API budget (#258,
84
+ closes #249)
85
+ - `ctrlrelay node check [--no-agent]`: node id, GitHub login, agent login
86
+ and peers, exit 1 on any failure; the poller warns at startup and while
87
+ running if another process uses this machine's `node_id`; new guide
88
+ `docs/adding-a-machine.md` (#261, closes #250)
89
+ - `agent.sandbox: bwrap` is now a read-only copy of the machine: by default
90
+ only the run's worktree, its git dirs, a private `/tmp`, the entries of
91
+ `~/.claude` and an agent-only cache (`agent.sandbox_cache_dir`) are
92
+ writable; the
93
+ agent CLI's settings, hooks, plugins, skills and project memory, and the
94
+ repository's git config and hooks, stay read-only; more credentials and
95
+ the session's sockets are hidden; `~/.claude.json` is a private copy per
96
+ run. New `agent.sandbox_writable_paths` and `agent.sandbox_readonly_paths`.
97
+ Still off by default. The configuration docs list what it costs a run
98
+ (no git config changes, no commit signing, no submodule updates) and how
99
+ to set it up (#262, closes #236)
100
+ - Status page: PR and issue numbers in Needs you questions are links (#242,
101
+ closes #241)
102
+
103
+ ### Changed
104
+
105
+ - The agent runs with `--output-format stream-json` (the only output that
106
+ carries the usage reading); `agent.output_format` is read and ignored
107
+ (#254)
108
+ - A repository whose history was replaced upstream is re-cloned, unless a
109
+ worktree still uses the clone (finish or clean up the run first); an issue
110
+ that failed before the agent started is retried on the next polls, up to
111
+ three tries, then left as seen like any other failed run, with a chat
112
+ line saying so (#244, closes #234)
113
+
114
+ ### Fixed
115
+
116
+ - A dev run whose PR went BEHIND mid-run no longer dies fetching its own
117
+ checked-out branch, and a late failure names the PR it opened (#255,
118
+ closes #253)
119
+
120
+ ### Known limits
121
+
122
+ - With several machines, chat and status-page names are only as unique as
123
+ each machine's `node_id`; the duplicate-name warning does not catch every
124
+ case
125
+ - The relay's own git calls in a worktree after the agent exits still read
126
+ that repository's config; only the push ignores it (#263)
127
+
128
+ ## [0.15.0] - 2026-10-31
129
+
130
+ The dev and task agents no longer inherit the usual GitHub credentials;
131
+ ctrlrelay does the pushing, PR opening and commenting itself
132
+ after the agent exits. One new
133
+ database table is added automatically and the new config values have
134
+ defaults, so **no config edit is required** to upgrade.
135
+
136
+ ### Changed
137
+
138
+ - Dev and task agents run with inherited `GH_*` and `GITHUB_*` variables
139
+ and gh's login removed from their environment, git credential helpers and
140
+ git's SSH disabled, and the D-Bus and runtime-dir variables pointed away
141
+ from the desktop keyring. After the agent exits the
142
+ relay pushes the run branch from a relay-private git dir to an explicit
143
+ GitHub URL, opens the PR from the DONE checkpoint (or reuses an open PR on the run
144
+ branch), pushes fix rounds,
145
+ fetches failed CI logs and posts task reports. It refuses to push the
146
+ default branch and only overwrites a remote branch whose head it recorded
147
+ pushing. secops keeps GitHub access (#235, closes #219)
148
+ - Status page: the phase bar shows the furthest phase seen in the recent
149
+ transcript and pipeline events, CI waits are
150
+ shown as waiting rather than reading, and the PR is linked (#239, closes
151
+ #238)
152
+
153
+ ### Added
154
+
155
+ - Optional `agent.sandbox: bwrap` with `agent.sandbox_hide_paths` to run the
156
+ dev and task agents under bubblewrap with credential paths hidden. Off by
157
+ default (#235)
158
+ - A `relay_pushes` table recording the heads ctrlrelay pushed (#235)
159
+ - Status page: a live counter and dot, server health, GitHub links, and the
160
+ list of skipped issues (#233, closes #232)
161
+
162
+ ### Known limits
163
+
164
+ - The agent still runs as the same OS user. Without the bwrap sandbox it
165
+ can read credential files on disk; with it, only the paths in
166
+ `agent.sandbox_hide_paths` are hidden. It can still run ssh directly, reach
167
+ the Linux keyring by resetting the D-Bus address, or read a token kept
168
+ under some other variable name, and the macOS keychain is not covered. Running the agent as a separate user
169
+ is tracked in #236
170
+
10
171
  ## [0.14.0] - 2026-10-31
11
172
 
12
173
  A read-only status page. `state.db` switches to WAL mode and gains an
@@ -1796,7 +1957,9 @@ pipeline).
1796
1957
  per-phase implementation plans (Phase 0 through Phase 4).
1797
1958
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
1798
1959
 
1799
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.14.0...HEAD
1960
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.0...HEAD
1961
+ [0.16.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.15.0...v0.16.0
1962
+ [0.15.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.14.0...v0.15.0
1800
1963
  [0.14.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.13.0...v0.14.0
1801
1964
  [0.13.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.4...v0.13.0
1802
1965
  [0.12.4]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.2...v0.12.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.14.0
3
+ Version: 0.16.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/
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Adding a machine
3
+ layout: default
4
+ nav_order: 11
5
+ description: "Run ctrlrelay on a second machine, on the same GitHub account and repositories, without the two taking the same work."
6
+ permalink: /adding-a-machine/
7
+ ---
8
+
9
+ # Adding a machine
10
+
11
+ Several machines can run ctrlrelay on the same GitHub account and the same
12
+ repositories. Each one takes issues the others have not claimed, each
13
+ scheduled sweep runs once across them, and any machine's status page shows
14
+ all of them. This page sets up a second machine, `node-b`, next to a running
15
+ `node-a`.
16
+
17
+ Read [Operations: several machines on one account]({{ '/operations/' | relative_url }}#several-machines-on-one-account)
18
+ for how the claims work.
19
+
20
+ ## 1. Install
21
+
22
+ Install ctrlrelay the same way as on the first machine
23
+ ([Getting started]({{ '/getting-started/' | relative_url }})), the **same
24
+ version** on every machine. A machine on a version from before the
25
+ multi-machine work reads every failure comment as freeing every claim and
26
+ posts claims with no machine name.
27
+
28
+ ## 2. Its own GitHub login
29
+
30
+ Log `gh` in on this machine with its own token:
31
+
32
+ ```bash
33
+ gh auth login
34
+ ```
35
+
36
+ Use the same GitHub **account** as the other machines (claims only count when
37
+ they come from the account the relay runs as), but a **token of its own**.
38
+ A token copied between machines is revoked for all of them at once.
39
+
40
+ ## 3. Its own agent login
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.
46
+
47
+ ## 4. A config with its own `node_id`
48
+
49
+ Copy `orchestrator.yaml` from the first machine and change:
50
+
51
+ ```yaml
52
+ node_id: "node-b" # a name no other machine on this account uses
53
+ ```
54
+
55
+ The `node_id` is on every claim, every chat message and the status page, so
56
+ two machines with the same one look like one. If you leave it out, the
57
+ hostname is used, which is fine when hostnames differ. ctrlrelay refuses to
58
+ start with a placeholder such as `my-laptop`, `localhost` or `unknown`.
59
+
60
+ Keep `repos`, `schedules` and `timezone` the same on every machine: the
61
+ scheduled sweep is split by its scheduled time, so machines must agree on the
62
+ schedule.
63
+
64
+ ## 5. Peers, for the status page
65
+
66
+ To see every machine on any machine's page, list the others' status pages:
67
+
68
+ ```yaml
69
+ # on node-b
70
+ web:
71
+ host: "192.168.1.21" # an address node-a can reach
72
+ allowed_hosts: ["node-b"] # the name node-a uses for this machine
73
+ fleet:
74
+ peers: ["http://node-a:8787"]
75
+ account_label: "max-2" # optional: which subscription, never an email
76
+ ```
77
+
78
+ and the mirror image on `node-a`. A peer refuses a name it does not know
79
+ (HTTP 421, shown as "bad host header"): each machine's `web.allowed_hosts`
80
+ must hold the name the others use for it.
81
+
82
+ ## 6. Chat
83
+
84
+ - **Mattermost:** machines can share one bot and channel. Set
85
+ `transport.mattermost.shared: true` on every machine. Answer a question by
86
+ replying to it; a message that is not a reply must start with the machine's
87
+ name, `node-b: ...`.
88
+ - **Telegram:** each machine needs **its own bot** (create one with
89
+ @BotFather). Telegram lets one process read a bot at a time.
90
+
91
+ See [Operations: chat with several machines]({{ '/operations/' | relative_url }}#chat-with-several-machines).
92
+
93
+ ## 7. Check it
94
+
95
+ ```bash
96
+ ctrlrelay node check
97
+ ```
98
+
99
+ ```
100
+ ✓ node_id: node-b (named on claims, chat and the page)
101
+ ✓ GitHub login: <the account>
102
+ ✓ agent login: works; 12% of the 5-hour window
103
+ ✓ peer http://node-a:8787: reachable, node-a
104
+ ```
105
+
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
+ machines share a `node_id` (or the URL points back here).
109
+
110
+ ## 8. Start it
111
+
112
+ Start the bridge, the poller and the status page as on the first machine
113
+ ([Operations]({{ '/operations/' | relative_url }})). At startup, and while it
114
+ runs, the poller warns, in its log and in chat, if another process uses its
115
+ name: at startup, a peer whose status page answers with its name; while it
116
+ runs, a claim under its name that another process is refreshing, on an issue
117
+ it looks at. Neither is certain to catch every case, so give each machine
118
+ its own `node_id` rather than relying on the warning.
119
+
120
+ Then open either machine's status page: the **Machines** strip shows both.
@@ -70,6 +70,14 @@ is the only place ctrlrelay calls out to Claude Code.
70
70
  - **Environment additions:** `CTRLRELAY_SESSION_ID` and `CTRLRELAY_STATE_FILE`
71
71
  are the only contract between dispatcher and child. Anything else the agent
72
72
  needs (issue body, PR rules, etc.) goes into the prompt.
73
+ - **Credentials (#219):** every spawn says whether it gets GitHub access
74
+ (`spawn_session(..., github_access=...)`, no default). dev and task spawn
75
+ without it: GitHub tokens, `gh`'s login, git credential helpers, SSH and
76
+ the desktop keyring are cut off (`core/agent_env.py`), and ctrlrelay
77
+ pushes the branch, opens the PR or posts the report itself afterwards
78
+ (`core/publish.py`, `WorktreeManager.push_run_branch`). secops still
79
+ spawns with access. See
80
+ [What the agent can reach](configuration.md#what-the-agent-can-reach-219).
73
81
  - **Result:** `SessionResult(session_id, exit_code, state, stdout, stderr,
74
82
  agent_session_id)`. `session_id` is our composite orchestrator id;
75
83
  `agent_session_id` is Claude's own UUID, parsed from the JSON stdout and