ctrlrelay 0.16.2__tar.gz → 0.16.3__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.16.2 → ctrlrelay-0.16.3}/CHANGELOG.md +17 -1
  2. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/PKG-INFO +1 -1
  3. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/pyproject.toml +1 -1
  4. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/github.py +11 -7
  5. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/poller.py +24 -1
  6. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_github.py +11 -5
  7. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_node_claims.py +1 -1
  8. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_poller.py +74 -2
  9. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_usage.py +1 -1
  10. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_waiting.py +5 -3
  11. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/dependabot.yml +0 -0
  15. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/build.yml +0 -0
  16. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/cla.yml +0 -0
  17. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/pages.yml +0 -0
  18. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/publish.yml +0 -0
  19. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/test.yml +0 -0
  20. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.gitignore +0 -0
  21. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/CODE_OF_CONDUCT.md +0 -0
  22. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/CONTRIBUTING.md +0 -0
  23. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/LICENSE +0 -0
  24. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/README.md +0 -0
  25. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/SECURITY.md +0 -0
  26. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/config/orchestrator.yaml.example +0 -0
  27. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/Gemfile +0 -0
  28. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/_config.yml +0 -0
  29. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/adding-a-machine.md +0 -0
  30. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/architecture.md +0 -0
  31. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/bridge.md +0 -0
  32. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/cli.md +0 -0
  33. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/configuration.md +0 -0
  34. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/development.md +0 -0
  35. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/feedback-loop.md +0 -0
  36. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/getting-started.md +0 -0
  37. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/index.md +0 -0
  38. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/operations.md +0 -0
  39. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/personalization.md +0 -0
  40. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  41. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  42. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/__init__.py +0 -0
  43. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/__init__.py +0 -0
  44. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/__main__.py +0 -0
  45. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/factory.py +0 -0
  46. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/handler.py +0 -0
  47. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  48. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/node_tag.py +0 -0
  49. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/protocol.py +0 -0
  50. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/server.py +0 -0
  51. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  52. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/cli.py +0 -0
  53. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/__init__.py +0 -0
  54. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/agent_env.py +0 -0
  55. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/archived.py +0 -0
  56. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/audit.py +0 -0
  57. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/binaries.py +0 -0
  58. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/checkpoint.py +0 -0
  59. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/claims.py +0 -0
  60. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/code_review.py +0 -0
  61. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/config.py +0 -0
  62. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/dispatch_health.py +0 -0
  63. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/dispatcher.py +0 -0
  64. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/events.py +0 -0
  65. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/github_budget.py +0 -0
  66. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/heartbeat.py +0 -0
  67. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/job_claims.py +0 -0
  68. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/network.py +0 -0
  69. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/node_check.py +0 -0
  70. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/obs.py +0 -0
  71. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/pr_verifier.py +0 -0
  72. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/pr_watcher.py +0 -0
  73. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/publish.py +0 -0
  74. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/question_expiry.py +0 -0
  75. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/redact.py +0 -0
  76. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/scheduler.py +0 -0
  77. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/state.py +0 -0
  78. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/usage.py +0 -0
  79. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/worktree.py +0 -0
  80. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/dashboard/__init__.py +0 -0
  81. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/dashboard/client.py +0 -0
  82. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/gh_protocol.py +0 -0
  83. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/install.py +0 -0
  84. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/personalization/__init__.py +0 -0
  85. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/personalization/manager.py +0 -0
  86. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/personalization/paths.py +0 -0
  87. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/__init__.py +0 -0
  88. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/base.py +0 -0
  89. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/dev.py +0 -0
  90. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  91. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/secops.py +0 -0
  92. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/task.py +0 -0
  93. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/setup.py +0 -0
  94. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/__init__.py +0 -0
  95. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  96. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  97. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  98. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  99. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  100. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  101. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  102. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  103. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/__init__.py +0 -0
  104. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/base.py +0 -0
  105. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/file_mock.py +0 -0
  106. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/socket_client.py +0 -0
  107. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/__init__.py +0 -0
  108. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/fleet.py +0 -0
  109. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/health.py +0 -0
  110. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/links.py +0 -0
  111. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/server.py +0 -0
  112. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/static/index.html +0 -0
  113. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/status.py +0 -0
  114. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/transcript.py +0 -0
  115. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/waiting.py +0 -0
  116. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/__init__.py +0 -0
  117. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/_publish_fakes.py +0 -0
  118. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/conftest.py +0 -0
  119. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/fixtures/gh_search_issues.json +0 -0
  120. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  121. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/fixtures/transcript_sample.jsonl +0 -0
  122. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_agent_no_keys.py +0 -0
  123. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_agent_sandbox.py +0 -0
  124. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_archived.py +0 -0
  125. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_audit.py +0 -0
  126. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_binary_resolution.py +0 -0
  127. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_bridge_protocol.py +0 -0
  128. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_bridge_server.py +0 -0
  129. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_bridge_start_command.py +0 -0
  130. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_chat_machines.py +0 -0
  131. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_checkpoint.py +0 -0
  132. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_claim_reconciliation.py +0 -0
  133. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_ci_wait.py +0 -0
  134. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_dev.py +0 -0
  135. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_repos.py +0 -0
  136. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_secops.py +0 -0
  137. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_start.py +0 -0
  138. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_version.py +0 -0
  139. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_code_review.py +0 -0
  140. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_config.py +0 -0
  141. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dashboard_client.py +0 -0
  142. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dev_integration.py +0 -0
  143. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dev_pipeline.py +0 -0
  144. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dispatch_health.py +0 -0
  145. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dispatch_outcome_hook.py +0 -0
  146. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dispatcher.py +0 -0
  147. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_docs_site.py +0 -0
  148. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_event_emission.py +0 -0
  149. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_events.py +0 -0
  150. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_fleet.py +0 -0
  151. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_gh_protocol.py +0 -0
  152. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_github_budget.py +0 -0
  153. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_install.py +0 -0
  154. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_issue_outcome_announcement.py +0 -0
  155. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_job_claims.py +0 -0
  156. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_mattermost_handler.py +0 -0
  157. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_network.py +0 -0
  158. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_node_check.py +0 -0
  159. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_notify_transport.py +0 -0
  160. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_obs.py +0 -0
  161. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_personalization.py +0 -0
  162. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pipeline_base.py +0 -0
  163. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_post_merge.py +0 -0
  164. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pr_verifier.py +0 -0
  165. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pr_watcher.py +0 -0
  166. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pre_dispatch_retry.py +0 -0
  167. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_question_backoff.py +0 -0
  168. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_question_expiry.py +0 -0
  169. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_redact.py +0 -0
  170. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_relay_alert_off_lock.py +0 -0
  171. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_resume_prompt_contract.py +0 -0
  172. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_scheduler.py +0 -0
  173. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_secops_integration.py +0 -0
  174. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_secops_pipeline.py +0 -0
  175. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_setup.py +0 -0
  176. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_state.py +0 -0
  177. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_task_pipeline.py +0 -0
  178. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_telegram_handler.py +0 -0
  179. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_transport.py +0 -0
  180. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_transport_resolution.py +0 -0
  181. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_unreviewed_pr_announcement.py +0 -0
  182. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_health.py +0 -0
  183. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_links.py +0 -0
  184. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_server.py +0 -0
  185. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_status.py +0 -0
  186. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_transcript.py +0 -0
  187. {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_worktree.py +0 -0
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.3] - 2026-11-01
11
+
12
+ A security fix: **upgrade every machine.** No config edit is required.
13
+
14
+ ### Fixed
15
+
16
+ - Only issues the operator assigned to themselves are taken, as intended.
17
+ The check read `actor` on GitHub's `assigned` events, which is the account
18
+ assigned (the operator), not the one who assigned it (`assigner`), so an
19
+ issue anyone else assigned to the operator was taken too. It now reads
20
+ `assigner`, refuses an event without one, refuses an unnamed assignment
21
+ after the operator's last one, and reads every page of an issue's events.
22
+ The status page's "assigned by someone else" uses the same check (#275,
23
+ closes #274)
24
+
10
25
  ## [0.16.2] - 2026-11-01
11
26
 
12
27
  Status page changes only. **No config edit is required.**
@@ -2001,7 +2016,8 @@ pipeline).
2001
2016
  per-phase implementation plans (Phase 0 through Phase 4).
2002
2017
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2003
2018
 
2004
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.2...HEAD
2019
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.3...HEAD
2020
+ [0.16.3]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.2...v0.16.3
2005
2021
  [0.16.2]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.1...v0.16.2
2006
2022
  [0.16.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.0...v0.16.1
2007
2023
  [0.16.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.15.0...v0.16.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.16.2
3
+ Version: 0.16.3
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrlrelay"
3
- version = "0.16.2"
3
+ version = "0.16.3"
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"
@@ -424,17 +424,21 @@ class GitHubCLI:
424
424
  ) -> list[dict[str, Any]]:
425
425
  """List ``assigned`` events for an issue in chronological order.
426
426
 
427
- Returns the GitHub issue-events payload filtered to ``event == "assigned"``.
428
- Each entry includes ``actor`` (who performed the assignment) and
429
- ``assignee`` (who was assigned). Used by the poller to verify that the
430
- most recent self-assignment was actually performed by the operator.
427
+ Returns the GitHub issue-events payload filtered to ``event == "assigned"``,
428
+ every page of it. Each entry includes ``assigner`` (who performed the
429
+ assignment) and ``assignee`` (who was assigned); its ``actor`` is the
430
+ assignee too, not who made it (#274). Used by the poller to verify
431
+ that the most recent self-assignment was actually performed by the
432
+ operator.
431
433
  """
434
+ # Every page: on an issue with more than one page of events the
435
+ # latest assignment would otherwise be missed.
432
436
  output = await self._run_gh(
433
- "api",
437
+ "api", "--paginate",
434
438
  f"/repos/{repo}/issues/{issue_number}/events",
435
- "--jq", '[.[] | select(.event=="assigned")]',
439
+ "--jq", '.[] | select(.event=="assigned")',
436
440
  )
437
- return json.loads(output) if output.strip() else []
441
+ return [json.loads(line) for line in output.splitlines() if line.strip()]
438
442
 
439
443
  async def get_issue(
440
444
  self,
@@ -617,6 +617,25 @@ class IssuePoller:
617
617
  return True
618
618
 
619
619
  events = await self.github.list_assignment_events(repo, issue_number)
620
+ # An assignment after the operator's last one that names nobody
621
+ # cannot be ruled out as a newer one by someone else: refuse rather
622
+ # than skip past it. One before it is overtaken and does not count.
623
+ last_mine = max(
624
+ (i for i, e in enumerate(events)
625
+ if (e.get("assignee") or {}).get("login") == self.username),
626
+ default=-1,
627
+ )
628
+ if any(not (e.get("assignee") or {}).get("login")
629
+ for e in events[last_mine + 1:]):
630
+ log_event(
631
+ _logger,
632
+ "poll.issue.foreign_assignment",
633
+ repo=repo,
634
+ number=issue_number,
635
+ assigner_login=None,
636
+ reason="assignment_without_assignee",
637
+ )
638
+ return False
620
639
  relevant = [
621
640
  e
622
641
  for e in events
@@ -634,8 +653,12 @@ class IssuePoller:
634
653
  return False
635
654
 
636
655
  # Events endpoint returns chronological order; the last one wins.
656
+ # Who made the assignment is ``assigner``. On an ``assigned`` event
657
+ # ``actor`` is the account assigned, which is always the operator
658
+ # here, so reading it let anyone's assignment through (#274). No
659
+ # ``assigner`` is treated as foreign.
637
660
  latest = relevant[-1]
638
- assigner_login = (latest.get("actor") or {}).get("login")
661
+ assigner_login = (latest.get("assigner") or {}).get("login")
639
662
  if assigner_login == self.username:
640
663
  return True
641
664
 
@@ -460,17 +460,21 @@ class TestGitHubCLI:
460
460
  """Should return only 'assigned' events for an issue."""
461
461
  from ctrlrelay.core.github import GitHubCLI
462
462
 
463
- mock_output = json.dumps([
463
+ # ``gh api --paginate --jq '.[] | ...'`` prints one event per line,
464
+ # across every page.
465
+ mock_output = "\n".join(json.dumps(e) for e in [
464
466
  {
465
467
  "event": "assigned",
466
468
  "actor": {"login": "alice"},
467
469
  "assignee": {"login": "alice"},
470
+ "assigner": {"login": "alice"},
468
471
  "created_at": "2026-01-01T00:00:00Z",
469
472
  },
470
473
  {
471
474
  "event": "assigned",
472
- "actor": {"login": "bob"},
475
+ "actor": {"login": "alice"},
473
476
  "assignee": {"login": "alice"},
477
+ "assigner": {"login": "bob"},
474
478
  "created_at": "2026-01-02T00:00:00Z",
475
479
  },
476
480
  ])
@@ -481,11 +485,13 @@ class TestGitHubCLI:
481
485
  events = await gh.list_assignment_events("owner/repo", 42)
482
486
 
483
487
  assert len(events) == 2
484
- assert events[0]["actor"]["login"] == "alice"
485
- assert events[1]["actor"]["login"] == "bob"
488
+ assert events[0]["assigner"]["login"] == "alice"
489
+ assert events[1]["assigner"]["login"] == "bob"
486
490
 
487
491
  args = mock_run.call_args[0]
488
- assert "api" in args
492
+ assert "api" in args and "--paginate" in args
493
+ # Only assigned events, one per line across every page.
494
+ assert args[args.index("--jq") + 1] == '.[] | select(.event=="assigned")'
489
495
  assert "/repos/owner/repo/issues/42/events" in args
490
496
 
491
497
  @pytest.mark.asyncio
@@ -842,7 +842,7 @@ class TestThePollerLooksAgainWhenTheClaimCouldGoStale:
842
842
  gh.list_issues_by_label = AsyncMock(return_value=[])
843
843
  gh.list_assignment_events = AsyncMock(return_value=[{
844
844
  "event": "assigned", "actor": {"login": "alice"},
845
- "assignee": {"login": "alice"},
845
+ "assignee": {"login": "alice"}, "assigner": {"login": "alice"},
846
846
  }])
847
847
  return IssuePoller(
848
848
  github=gh, username="alice", repos=["o/r"],
@@ -43,11 +43,15 @@ def state_file(tmp_path: Path) -> Path:
43
43
  return tmp_path / "poller_state.json"
44
44
 
45
45
 
46
- def make_assigned_event(actor_login: str, assignee_login: str) -> dict:
46
+ def make_assigned_event(assigner_login: str, assignee_login: str) -> dict:
47
+ """An ``assigned`` event as GitHub's issue events API returns it: ``actor``
48
+ is the account ASSIGNED (#274), and who made the assignment is in
49
+ ``assigner``. The first argument is the one that made it."""
47
50
  return {
48
51
  "event": "assigned",
49
- "actor": {"login": actor_login},
52
+ "actor": {"login": assignee_login},
50
53
  "assignee": {"login": assignee_login},
54
+ "assigner": {"login": assigner_login},
51
55
  "created_at": "2026-01-01T00:00:00Z",
52
56
  }
53
57
 
@@ -2170,3 +2174,71 @@ class TestArchivedRepoSkip:
2170
2174
 
2171
2175
  assert poller.archived_tracker is tracker
2172
2176
  assert tracker.archived_repos == {"owner/repo-a"}
2177
+
2178
+
2179
+
2180
+ class TestWhoAssignedIt:
2181
+ """#274: who made an assignment is ``assigner``, never ``actor``."""
2182
+
2183
+ # Recorded from AInvirion/zzsites#784 on 2026-11-01: another account
2184
+ # moved the issue to the operator. ``actor`` names the operator.
2185
+ REAL_FOREIGN = [{
2186
+ "event": "assigned", "created_at": "2026-11-01T21:24:13Z",
2187
+ "actor": {"login": "oscarvalenzuelab"},
2188
+ "assignee": {"login": "oscarvalenzuelab"},
2189
+ "assigner": {"login": "mausisa-nico"},
2190
+ }]
2191
+
2192
+ def _poller(self, gh, state_file):
2193
+ return IssuePoller(github=gh, username="oscarvalenzuelab", repos=["o/r"],
2194
+ state_file=state_file)
2195
+
2196
+ @pytest.mark.asyncio
2197
+ async def test_the_recorded_foreign_assignment_is_refused(
2198
+ self, mock_github, state_file,
2199
+ ) -> None:
2200
+ mock_github.list_assignment_events = AsyncMock(return_value=self.REAL_FOREIGN)
2201
+ assert not await self._poller(mock_github, state_file)._is_self_assigned("o/r", 784)
2202
+
2203
+ @pytest.mark.asyncio
2204
+ async def test_an_event_with_no_assigner_is_refused(self, mock_github, state_file) -> None:
2205
+ event = dict(self.REAL_FOREIGN[0])
2206
+ del event["assigner"]
2207
+ mock_github.list_assignment_events = AsyncMock(return_value=[event])
2208
+ assert not await self._poller(mock_github, state_file)._is_self_assigned("o/r", 784)
2209
+
2210
+ # Recorded from AInvirion/ctrlrelay#268 on 2026-11-01: the operator
2211
+ # assigned it to themselves.
2212
+ REAL_SELF = [{
2213
+ "event": "assigned", "created_at": "2026-11-01T21:44:14Z",
2214
+ "actor": {"login": "oscarvalenzuelab"},
2215
+ "assignee": {"login": "oscarvalenzuelab"},
2216
+ "assigner": {"login": "oscarvalenzuelab"},
2217
+ }]
2218
+
2219
+ @pytest.mark.asyncio
2220
+ async def test_a_later_assignment_naming_nobody_is_refused(
2221
+ self, mock_github, state_file,
2222
+ ) -> None:
2223
+ later = {"event": "assigned", "created_at": "2026-11-02T00:00:00Z",
2224
+ "actor": None, "assignee": None, "assigner": {"login": "mausisa-nico"}}
2225
+ mock_github.list_assignment_events = AsyncMock(
2226
+ return_value=[*self.REAL_SELF, later],
2227
+ )
2228
+ assert not await self._poller(mock_github, state_file)._is_self_assigned("o/r", 268)
2229
+
2230
+ @pytest.mark.asyncio
2231
+ async def test_an_earlier_assignment_naming_nobody_does_not_block_a_later_self_one(
2232
+ self, mock_github, state_file,
2233
+ ) -> None:
2234
+ earlier = {"event": "assigned", "created_at": "2026-10-01T00:00:00Z",
2235
+ "actor": None, "assignee": None, "assigner": None}
2236
+ mock_github.list_assignment_events = AsyncMock(
2237
+ return_value=[earlier, *self.REAL_SELF],
2238
+ )
2239
+ assert await self._poller(mock_github, state_file)._is_self_assigned("o/r", 268)
2240
+
2241
+ @pytest.mark.asyncio
2242
+ async def test_a_real_self_assignment_is_taken(self, mock_github, state_file) -> None:
2243
+ mock_github.list_assignment_events = AsyncMock(return_value=self.REAL_SELF)
2244
+ assert await self._poller(mock_github, state_file)._is_self_assigned("o/r", 784)
@@ -361,7 +361,7 @@ class TestAPausedMachineTakesTheIssueAfterTheReset:
361
361
  gh.list_issues_by_label = AsyncMock(return_value=[])
362
362
  gh.list_assignment_events = AsyncMock(return_value=[{
363
363
  "event": "assigned", "actor": {"login": "alice"},
364
- "assignee": {"login": "alice"},
364
+ "assignee": {"login": "alice"}, "assigner": {"login": "alice"},
365
365
  }])
366
366
  poller = IssuePoller(
367
367
  github=gh, username="alice", repos=["o/r"],
@@ -407,12 +407,14 @@ class _FakeGitHub:
407
407
  return [copy.deepcopy(r) for r in self.by_label.get((repo, label), [])]
408
408
 
409
409
 
410
- def _assigned_by(actor: str) -> list[dict]:
411
- """`list_assignment_events` output: GitHub `assigned` issue events."""
410
+ def _assigned_by(assigner: str) -> list[dict]:
411
+ """`list_assignment_events` output, as GitHub returns it: ``actor`` is the
412
+ account assigned, ``assigner`` the one that assigned (#274)."""
412
413
  return [{
413
414
  "event": "assigned",
414
- "actor": {"login": actor},
415
+ "actor": {"login": "octo"},
415
416
  "assignee": {"login": "octo"},
417
+ "assigner": {"login": assigner},
416
418
  "created_at": "2026-10-20T10:00:00Z",
417
419
  }]
418
420
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes