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.
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/CHANGELOG.md +17 -1
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/PKG-INFO +1 -1
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/pyproject.toml +1 -1
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/github.py +11 -7
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/poller.py +24 -1
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_github.py +11 -5
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_node_claims.py +1 -1
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_poller.py +74 -2
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_usage.py +1 -1
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_waiting.py +5 -3
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/.gitignore +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/LICENSE +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/README.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/SECURITY.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/Gemfile +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/_config.yml +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/adding-a-machine.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/architecture.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/bridge.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/cli.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/configuration.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/development.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/getting-started.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/index.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/operations.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/personalization.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/node_tag.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/cli.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/agent_env.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/claims.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/config.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/dispatcher.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/events.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/github_budget.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/heartbeat.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/job_claims.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/node_check.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/publish.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/state.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/usage.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/dev.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/secops.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/pipelines/task.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/fleet.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/health.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/links.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/server.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/static/index.html +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/status.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/transcript.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/web/waiting.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/__init__.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/_publish_fakes.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/conftest.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/fixtures/gh_search_issues.json +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/fixtures/transcript_sample.jsonl +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_agent_no_keys.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_agent_sandbox.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_archived.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_audit.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_binary_resolution.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_bridge_start_command.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_chat_machines.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_claim_reconciliation.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_config.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dev_integration.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dispatch_outcome_hook.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_event_emission.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_events.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_fleet.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_github_budget.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_install.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_job_claims.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_network.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_node_check.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_obs.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_pre_dispatch_retry.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_question_backoff.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_redact.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_relay_alert_off_lock.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_resume_prompt_contract.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_setup.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_state.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_transport.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_unreviewed_pr_announcement.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_health.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_links.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_server.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_status.py +0 -0
- {ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/tests/test_web_transcript.py +0 -0
- {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.
|
|
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.
|
|
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/
|
|
@@ -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 ``
|
|
429
|
-
``assignee`` (who was assigned)
|
|
430
|
-
|
|
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", '
|
|
439
|
+
"--jq", '.[] | select(.event=="assigned")',
|
|
436
440
|
)
|
|
437
|
-
return json.loads(output) if
|
|
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("
|
|
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
|
-
|
|
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": "
|
|
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]["
|
|
485
|
-
assert events[1]["
|
|
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(
|
|
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":
|
|
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(
|
|
411
|
-
"""`list_assignment_events` output
|
|
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":
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/docs/superpowers/specs/2026-10-30-status-page-design.md
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/bridge.service.template
RENAMED
|
File without changes
|
{ctrlrelay-0.16.2 → ctrlrelay-0.16.3}/src/ctrlrelay/templates/systemd/poller.service.template
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|