ctrlrelay 0.22.0__tar.gz → 0.24.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.
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/CHANGELOG.md +145 -1
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/PKG-INFO +1 -1
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/architecture.md +8 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/cli.md +14 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/configuration.md +271 -5
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/operations.md +199 -4
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/pyproject.toml +1 -1
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/cli.py +376 -21
- ctrlrelay-0.24.0/src/ctrlrelay/core/blockers.py +105 -0
- ctrlrelay-0.24.0/src/ctrlrelay/core/budget.py +596 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/config.py +198 -2
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/dispatcher.py +140 -37
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/events.py +7 -2
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/github.py +77 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/heartbeat.py +36 -2
- ctrlrelay-0.24.0/src/ctrlrelay/core/intake_hold.py +235 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/poller.py +243 -0
- ctrlrelay-0.24.0/src/ctrlrelay/core/proof.py +1226 -0
- ctrlrelay-0.24.0/src/ctrlrelay/core/reopens.py +109 -0
- ctrlrelay-0.24.0/src/ctrlrelay/core/self_update.py +509 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/state.py +149 -1
- ctrlrelay-0.24.0/src/ctrlrelay/core/weekly_summary.py +212 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/pipelines/base.py +6 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/pipelines/dev.py +414 -27
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/pipelines/post_merge.py +17 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/pipelines/secops.py +40 -5
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/pipelines/task.py +31 -6
- ctrlrelay-0.24.0/src/ctrlrelay/web/drift.py +267 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/fleet.py +7 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/numbers.py +175 -18
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/server.py +58 -1
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/static/index.html +110 -12
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/status.py +71 -2
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/waiting.py +60 -8
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_agent_routing.py +4 -0
- ctrlrelay-0.24.0/tests/test_budget.py +661 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dev_stuck.py +143 -1
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dev_work_steps.py +5 -3
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_events.py +1 -1
- ctrlrelay-0.24.0/tests/test_intake_hold.py +108 -0
- ctrlrelay-0.24.0/tests/test_intake_hold_poller.py +174 -0
- ctrlrelay-0.24.0/tests/test_issue_blockers.py +531 -0
- ctrlrelay-0.24.0/tests/test_poller_priority_order.py +265 -0
- ctrlrelay-0.24.0/tests/test_proof.py +1479 -0
- ctrlrelay-0.24.0/tests/test_reopens.py +269 -0
- ctrlrelay-0.24.0/tests/test_sandbox_visibility.py +155 -0
- ctrlrelay-0.24.0/tests/test_secops_operator_prs.py +239 -0
- ctrlrelay-0.24.0/tests/test_self_update.py +638 -0
- ctrlrelay-0.24.0/tests/test_web_drift.py +613 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_numbers.py +172 -9
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_server.py +24 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_status.py +76 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_waiting.py +58 -1
- ctrlrelay-0.24.0/tests/test_weekly_summary.py +265 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/.gitignore +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/LICENSE +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/README.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/SECURITY.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/_config.yml +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/adding-a-machine.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/bridge.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/development.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/getting-started.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/index.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/llms.txt +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/personalization.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/registered-mode.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/node_tag.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/agent_env.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/claims.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/github_budget.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/issue_questions.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/job_claims.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/node_check.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/orphans.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/publish.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/stuck.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/usage.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/personalization/merge.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/health.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/links.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/src/ctrlrelay/web/transcript.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/_poller_fakes.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/_publish_fakes.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/fixtures/gh_search_issues.json +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/fixtures/transcript_sample.jsonl +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_agent_backend_wiring.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_agent_no_keys.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_agent_sandbox.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_archived.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_binary_resolution.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_bridge_start_command.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_chat_machines.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_claim_reconciliation.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_config.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dev_integration.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dev_plan_step.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dev_step_switches.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dispatch_outcome_hook.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_event_emission.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_fleet.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_github_budget.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_install.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_issue_questions.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_job_claims.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_network.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_no_change_run.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_node_check.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_node_claims.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_obs.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_opencode_adapter.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_orphans.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_personalization_merge.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_poller.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_poller_recheck.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_poller_search.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_pre_dispatch_retry.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_publish_failure_retry.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_question_backoff.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_question_survives_restart.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_redact.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_relay_alert_off_lock.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_relay_git_263.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_resume_prompt_contract.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_review_copy.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_review_gate.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_setup.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_stuck.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_timeout_save.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_unreviewed_pr_announcement.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_usage.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_health.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_links.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_peer_links.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_web_transcript.py +0 -0
- {ctrlrelay-0.22.0 → ctrlrelay-0.24.0}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,148 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.24.0] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Each poll's new issues run in priority order, oldest first (#356). The
|
|
15
|
+
batch is sorted by the position of the issue's priority label in
|
|
16
|
+
`intake.priority_labels` (default `["P0", "P1", "P2", "P3"]`, compared
|
|
17
|
+
without case; an issue with several runs at its earliest; unlabeled
|
|
18
|
+
last), then by when the issue was opened, then by number. An issue whose
|
|
19
|
+
creation time cannot be read runs after the others with its label. `[]`
|
|
20
|
+
keeps the oldest-first order only. Each poll that found issues logs the
|
|
21
|
+
order once as `poll.batch.order`. The re-read before each dispatch and
|
|
22
|
+
the blocker hold are unchanged; the order is per machine and fixed for
|
|
23
|
+
the batch.
|
|
24
|
+
|
|
25
|
+
- Cost per run in the numbers (#230). Each 7- and 30-day window's
|
|
26
|
+
`cost_per_run` (in `/api/status` and the status page's **Numbers** box)
|
|
27
|
+
now gives the median and total tokens per run that ended (`done` or
|
|
28
|
+
`failed`), counted as the run budget counts them (#223), and the median
|
|
29
|
+
and total `total_cost_usd` where the agent reported one. Runs with no
|
|
30
|
+
usage recorded (from before #223, or on a backend that is not measured)
|
|
31
|
+
are left out and counted in `runs_without_usage`, never read as 0; a
|
|
32
|
+
window with no measured run is `null` with the reason in `unavailable`.
|
|
33
|
+
The weekly chat summary gets a `tokens per run` line, `unknown` when not
|
|
34
|
+
measured.
|
|
35
|
+
|
|
36
|
+
- A dev run's PR goes ready only with proof the change works (#221). The
|
|
37
|
+
agent names a proving test in its DONE checkpoint. The relay runs it on
|
|
38
|
+
the pushed commit (must pass) and on the base with only the branch's test
|
|
39
|
+
changes (must fail), in fresh copies, with a time limit, without the
|
|
40
|
+
GitHub token, and under bwrap with no network when the agent was
|
|
41
|
+
sandboxed. It also flags deleted, skipped or loosened tests and empty or
|
|
42
|
+
whitespace-only diffs. A result counts only with evidence the named test
|
|
43
|
+
ran (pytest's junit report, or the exit code and the test in the output
|
|
44
|
+
for other runners), and the PR's head must still be the proven commit when
|
|
45
|
+
it is marked ready. The outcome is `proven`, `failed` or `not proven`;
|
|
46
|
+
only `proven` (or a docs-only diff) lets the PR go ready. Otherwise the PR
|
|
47
|
+
stays a draft and the run fails with `PR #N held as a draft`. The PR gets
|
|
48
|
+
one comment, edited in place, with the test, the command and both results.
|
|
49
|
+
Off by default: set `repos[].automation.require_proof: true` for a repo
|
|
50
|
+
whose tests can run offline in a fresh copy. With it off, the agent is not
|
|
51
|
+
asked for a proof and nothing is posted.
|
|
52
|
+
|
|
53
|
+
- Each machine's agent sandbox mode is shown (#305). `/api/status` carries
|
|
54
|
+
`node.sandbox` (`"none"` or `"bwrap"`; `null` for a peer on an older
|
|
55
|
+
version), each machine's card on the status page says `sandbox off` in the
|
|
56
|
+
warning colour when the agent runs unconfined, and the poller logs
|
|
57
|
+
`poller.sandbox_off` once at start with what to do about it.
|
|
58
|
+
- A token budget for each run (#223). `agent.budget.soft_tokens` warns once
|
|
59
|
+
in chat and the log; `agent.budget.hard_tokens` stops the agent, records the
|
|
60
|
+
run as failed with the reason `budget: used N tokens, hard limit M`, and
|
|
61
|
+
says `💸 Over budget` in chat. `agent.budget.rules` sets other limits by
|
|
62
|
+
issue label, which covers `size:*`; the first match wins. Nothing is limited
|
|
63
|
+
until a limit is set. The count is input + cache writes + output tokens,
|
|
64
|
+
read live from the agent's transcript, and covers the whole issue session
|
|
65
|
+
across resumes and fix rounds. Cache reads are recorded but not counted.
|
|
66
|
+
A run stopped over budget or as stuck after it opened a PR puts the PR back
|
|
67
|
+
to draft (or posts a "Do not merge" comment when that fails), starts no
|
|
68
|
+
merge watcher for it, and names it in chat and the issue comment.
|
|
69
|
+
- Each session records the tokens it used (`sessions.usage_tokens` and
|
|
70
|
+
`sessions.usage_detail`, with the cost the agent CLI reported). Running
|
|
71
|
+
cards on the status page and `/api/status` show `tokens_used`,
|
|
72
|
+
`cache_read_tokens`, `cost_usd`, `token_soft_limit` and `token_hard_limit`.
|
|
73
|
+
A reading that could not be parsed shows as unknown (`null`), never as 0,
|
|
74
|
+
and never stops a run on its own (#223).
|
|
75
|
+
|
|
76
|
+
## [0.23.0] - 2026-11-05
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- `repos[].automation.operator_prs` (`ask` or `ignore`; unset is `ask`) lets
|
|
81
|
+
the secops sweep leave the operator's own open PRs alone on a repo. By
|
|
82
|
+
default every such PR that touches more than `.github/dependabot.yml` is a
|
|
83
|
+
question, which can be asked again on later sweeps while the PR stays
|
|
84
|
+
open. With `ignore` those PRs get no question and are not named in the
|
|
85
|
+
summary; a PR that touches only `.github/dependabot.yml` is handled as
|
|
86
|
+
before (merged if it passes the author, diff and CI checks, a question if
|
|
87
|
+
not), and Dependabot PRs and alerts are unchanged. Unset renders the same
|
|
88
|
+
prompt as before (#335).
|
|
89
|
+
|
|
90
|
+
- The status page shows config drift between machines. Each machine's
|
|
91
|
+
`/api/status` carries a fingerprint of its config: the repo names, and a
|
|
92
|
+
hash per field of each repo's `automation` and `steps` and of
|
|
93
|
+
`schedules`; never a value. Each peer's card names what differs from this
|
|
94
|
+
machine, such as repos only here or only there, or a field that differs
|
|
95
|
+
on some repos, within one peer read. A peer on a different version shows
|
|
96
|
+
as unknown, not as drift. An intended difference, such as a routing label in
|
|
97
|
+
`exclude_labels`, is left out with `fleet.expected_drift` (#307).
|
|
98
|
+
|
|
99
|
+
- Reopened issues are recorded. When the relay sees an issue's PR merge
|
|
100
|
+
(and closes the issue), it notes the finish in the state database. If
|
|
101
|
+
that issue later turns up open in the poller's search, the poller reads
|
|
102
|
+
its GitHub events and, when there is a `reopened` event after the
|
|
103
|
+
finish, records the reopen once and puts a `reopened` event in the
|
|
104
|
+
feed. Ordinary polls make no extra GitHub call. The status page and
|
|
105
|
+
`/api/status` count reopens per window and per repo (`reopened`, with
|
|
106
|
+
`reopened_issues`), whether or not the relay picks the issue up again;
|
|
107
|
+
`null` until the daemon has added the new table (#230).
|
|
108
|
+
- A weekly chat summary. Once a week each node sends one message with its
|
|
109
|
+
last-7-day numbers: runs started, done, failed, blocked and stuck, the
|
|
110
|
+
success rate, time to PR, time to merge and reopens. A number it cannot
|
|
111
|
+
read says `unknown`, never 0. Set with `schedules.weekly_summary`
|
|
112
|
+
(`enabled`, `day`, `hour`; Monday 09:00 in the configured timezone by
|
|
113
|
+
default). The last delivery is kept in the state database, so a restart
|
|
114
|
+
does not send it twice, and a failed send is logged and tried again five
|
|
115
|
+
minutes later without affecting the poller (#230).
|
|
116
|
+
|
|
117
|
+
- `ctrlrelay self-update [--version X]` updates a node in one command, only
|
|
118
|
+
once it is idle. It reads the release from PyPI's JSON, downloads the
|
|
119
|
+
wheel and checks its sha256 (installing by path, because a node's index
|
|
120
|
+
has lagged a release by more than ten minutes), then holds new work with
|
|
121
|
+
a file beside the state database that the poller obeys the way it obeys
|
|
122
|
+
the usage pause. It waits, bounded, until the poller acknowledges that
|
|
123
|
+
nothing is in flight and no session is running, so a poll cannot start a
|
|
124
|
+
run between the check and the restart. Then `uv tool install --force`,
|
|
125
|
+
a restart of this node's systemd user units or LaunchAgents, and a check
|
|
126
|
+
that `/api/status` reports the new version and that a new poller process
|
|
127
|
+
reports it in its heartbeat and stays up (the heartbeat now carries the
|
|
128
|
+
poller's pid, version and start time). If a step after the install
|
|
129
|
+
fails, or the command is interrupted after the install began, the old
|
|
130
|
+
version is installed and restarted again; chat is told either way and
|
|
131
|
+
the command exits non-zero. The hold is removed on every
|
|
132
|
+
exit, and ignored once its process is gone or it expires (#306).
|
|
133
|
+
- The status page marks a peer whose version differs from this machine's:
|
|
134
|
+
its card is red and says which version each runs (#306).
|
|
135
|
+
|
|
136
|
+
- The poller does not start an issue whose GitHub "blocked by" issues are
|
|
137
|
+
still open (#310). Before claiming an issue it reads its blockers from
|
|
138
|
+
GitHub's issue dependencies, including blockers in other repositories.
|
|
139
|
+
An issue with an open blocker is not claimed, is logged as
|
|
140
|
+
`poll.issue.blocked`, is listed on the status page under Skipped as
|
|
141
|
+
"blocked by #A", and is said in chat once; every later poll asks again,
|
|
142
|
+
so it starts on the first poll after its last blocker closes. Blockers
|
|
143
|
+
that cannot be read (an API error, a rate limit, a 404 on a GitHub
|
|
144
|
+
without the dependencies API) hold the issue the same way
|
|
145
|
+
(`poll.issue.blockers_unknown`) and are never read as "no blockers"; the
|
|
146
|
+
chat is told only after 3 failed polls in a row. A held issue stays out
|
|
147
|
+
of `seen_issues` and is recorded in `blocked` in `poller_state.json`, so
|
|
148
|
+
a restart neither loses it nor repeats the chat line, and a failed
|
|
149
|
+
self-assignment check while it waits retries it next poll rather than
|
|
150
|
+
dropping it. Free text such as "Starts after: #A" is not read; use the link.
|
|
151
|
+
|
|
10
152
|
## [0.22.0] - 2026-11-05
|
|
11
153
|
|
|
12
154
|
### Added
|
|
@@ -2449,7 +2591,9 @@ pipeline).
|
|
|
2449
2591
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
2450
2592
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
2451
2593
|
|
|
2452
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.
|
|
2594
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.24.0...HEAD
|
|
2595
|
+
[0.24.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.23.0...v0.24.0
|
|
2596
|
+
[0.23.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.22.0...v0.23.0
|
|
2453
2597
|
[0.22.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.21.0...v0.22.0
|
|
2454
2598
|
[0.21.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.2...v0.21.0
|
|
2455
2599
|
[0.20.2]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.1...v0.20.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.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/
|
|
@@ -140,6 +140,14 @@ to confirm the PR is mergeable and CI is green. If not, it builds a fix prompt
|
|
|
140
140
|
fix"), resumes the same agent session with that prompt, and re-verifies. Up
|
|
141
141
|
to `DEFAULT_MAX_FIX_ATTEMPTS` (3) attempts.
|
|
142
142
|
|
|
143
|
+
Then the proof step
|
|
144
|
+
([`src/ctrlrelay/core/proof.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/proof.py),
|
|
145
|
+
#221) runs the test the agent named, in fresh copies of the pushed commit
|
|
146
|
+
(must pass) and of the base with only the branch's test changes (must fail),
|
|
147
|
+
and scans the diff for weakened tests, when the repo turns it on. Only a proven run's PR is marked
|
|
148
|
+
ready; any other outcome leaves it a draft and fails the run. See
|
|
149
|
+
[`automation.require_proof`](configuration.md#reposautomationrequire_proof).
|
|
150
|
+
|
|
143
151
|
`PRWatcher` (in
|
|
144
152
|
[`src/ctrlrelay/core/pr_watcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/pr_watcher.py))
|
|
145
153
|
polls a PR for merge status; the post-merge handler in
|
|
@@ -383,6 +383,20 @@ Opens the SQLite state DB at `paths.state_db` and prints:
|
|
|
383
383
|
|
|
384
384
|
If the DB doesn't exist yet, prints a hint to run a pipeline first.
|
|
385
385
|
|
|
386
|
+
## `ctrlrelay self-update`
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
ctrlrelay self-update [--version X] [--wait-minutes N] [--no-ack] [-c PATH]
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Installs a version already on PyPI on this node once it is idle: holds new
|
|
393
|
+
work, waits (up to `--wait-minutes`, default 60) for the run in flight,
|
|
394
|
+
installs the wheel after checking its sha256 against PyPI's JSON, restarts
|
|
395
|
+
this node's units and checks `/api/status` reports the new version. On any
|
|
396
|
+
failure the old version stays or is put back, chat is told, and it exits
|
|
397
|
+
non-zero. `--no-ack` is for updating from a poller too old to acknowledge
|
|
398
|
+
the hold. See [Operations: Updating a node](operations.md#updating-a-node).
|
|
399
|
+
|
|
386
400
|
## `ctrlrelay version`
|
|
387
401
|
|
|
388
402
|
```bash
|
|
@@ -40,6 +40,7 @@ repos: [ ... ]
|
|
|
40
40
|
| `transport` | object | **yes** | — | See [transport](#transport). |
|
|
41
41
|
| `dashboard` | object | no | (defaults) | See [dashboard](#dashboard). |
|
|
42
42
|
| `web` | object | no | (defaults) | The read-only status page. See [web](#web). |
|
|
43
|
+
| `intake` | object | no | (defaults) | The order each poll's new issues run in. See [intake](#intake). |
|
|
43
44
|
| `claims` | object | no | (defaults) | Claim timings for several machines on one account. See [claims](#claims). |
|
|
44
45
|
| `repos` | list | no | `[]` | See [repos](#repos). |
|
|
45
46
|
| `schedules` | object | no | (defaults) | See [schedules](#schedules). |
|
|
@@ -217,6 +218,41 @@ modes either need every shell command allow-listed or filter by command text,
|
|
|
217
218
|
which a wrapper script walks past, so they are not used as a guard. The guard
|
|
218
219
|
is the next section: the agent holds no GitHub credentials.
|
|
219
220
|
|
|
221
|
+
### agent.budget
|
|
222
|
+
|
|
223
|
+
A token budget for each run (#223). Both limits unset (the default): no limit,
|
|
224
|
+
and nothing changes. Usage is measured and shown on the status page either
|
|
225
|
+
way. See [run budgets](operations.md#run-budgets) for what is counted.
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
agent:
|
|
229
|
+
budget:
|
|
230
|
+
soft_tokens: 2000000 # warn once in chat and the log
|
|
231
|
+
hard_tokens: 6000000 # stop the agent
|
|
232
|
+
rules: # first rule naming any of the issue's labels wins
|
|
233
|
+
- labels: ["size:S", "size:XS"]
|
|
234
|
+
soft_tokens: 500000
|
|
235
|
+
hard_tokens: 1500000
|
|
236
|
+
- labels: ["size:L", "size:XL"]
|
|
237
|
+
hard_tokens: 15000000 # soft_tokens left out: 2000000 from above
|
|
238
|
+
- labels: ["no-budget"]
|
|
239
|
+
soft_tokens: null # null: no limit for these runs
|
|
240
|
+
hard_tokens: null
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| Key | Type | Default | Description |
|
|
244
|
+
|---|---|---|---|
|
|
245
|
+
| `soft_tokens` | int or null | `null` | When a run's total first reaches it, chat gets one line and the log gets `budget.soft_limit`. Once per session. |
|
|
246
|
+
| `hard_tokens` | int or null | `null` | When a run's total reaches it, the agent is stopped and the run is recorded as failed with the reason `budget: used N tokens, hard limit M`. Must be at least `soft_tokens`. |
|
|
247
|
+
| `rules` | list | `[]` | Other limits by issue label. Each rule has `labels` (a non-empty list) and optionally `soft_tokens` and `hard_tokens`. Labels match without regard to case. A size is a label (`size:S`), so this covers sizes too. A limit a rule leaves out comes from the top level; one set to `null` is no limit. |
|
|
248
|
+
|
|
249
|
+
The count is input tokens + cache writes + output tokens; cache reads are
|
|
250
|
+
recorded but not counted. The budget is for the whole issue session, across
|
|
251
|
+
resumes and CI fix rounds. Unknown keys and a hard limit below the soft one
|
|
252
|
+
(after a rule inherits from the top level) are refused when the config loads.
|
|
253
|
+
Only the `claude` backend is measured. Runs with no issue (secops) get the
|
|
254
|
+
top-level limits for each spawn.
|
|
255
|
+
|
|
220
256
|
### What the agent can reach (#219)
|
|
221
257
|
|
|
222
258
|
The dev and task agents run **without GitHub access**. ctrlrelay starts them
|
|
@@ -290,6 +326,14 @@ your user. With `sandbox: "none"` a deliberately hostile agent can read
|
|
|
290
326
|
and use them by hand, and it can write anywhere your user can, including your
|
|
291
327
|
dotfiles and ctrlrelay's install. On macOS the login keychain is reachable
|
|
292
328
|
whatever the environment says. `sandbox: "bwrap"` closes this on Linux.
|
|
329
|
+
macOS has no sandbox yet, so a macOS node should only take issues from
|
|
330
|
+
repositories you trust.
|
|
331
|
+
|
|
332
|
+
So that an unconfined node is never a surprise (#305), each machine's mode is
|
|
333
|
+
in `/api/status` as `node.sandbox` (`"none"` or `"bwrap"`; `null` for a peer on
|
|
334
|
+
an older version), its card on the status page says `sandbox off` in the
|
|
335
|
+
warning colour when it is `"none"`, and the poller logs `poller.sandbox_off`
|
|
336
|
+
once at start.
|
|
293
337
|
|
|
294
338
|
**`sandbox: "bwrap"` (#236).** The dev and task agents run under bubblewrap
|
|
295
339
|
in a read-only copy of the machine (the secops agent, which needs your GitHub
|
|
@@ -545,6 +589,29 @@ web:
|
|
|
545
589
|
The page has no login and shows repository names, issue titles and the
|
|
546
590
|
questions agents asked. **Never expose it to the internet.**
|
|
547
591
|
|
|
592
|
+
## intake
|
|
593
|
+
|
|
594
|
+
The order each poll's new issues run in (#356). A poll gathers one batch of
|
|
595
|
+
new issues and works through it before the next poll; this sets the order of
|
|
596
|
+
that batch.
|
|
597
|
+
|
|
598
|
+
```yaml
|
|
599
|
+
intake:
|
|
600
|
+
priority_labels: ["P0", "P1", "P2", "P3"]
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
| Key | Type | Default | Description |
|
|
604
|
+
|---|---|---|---|
|
|
605
|
+
| `priority_labels` | list of strings | `["P0", "P1", "P2", "P3"]` | Issues carrying an earlier label in this list run first; labels are compared without case, and an issue carrying several runs at its earliest. Issues carrying none of them run last. Within the same label, the oldest issue runs first (by when it was opened, then by number). An issue whose creation time cannot be read runs after the others with its label. `[]` keeps the oldest-first order only. Each label must be a non-empty string and listed once. |
|
|
606
|
+
|
|
607
|
+
Each poll that found new issues logs the order it will run them in, as one
|
|
608
|
+
`poll.batch.order` event (`order`: `owner/repo#N` in run order). The order is
|
|
609
|
+
fixed when the poll ends: an urgent issue opened while a batch runs waits for
|
|
610
|
+
the next poll. Each issue is still read again just before it starts, and an
|
|
611
|
+
issue blocked by an open issue is still held until the blocker closes; those
|
|
612
|
+
can skip an issue, never reorder the batch. The order applies to this machine
|
|
613
|
+
only, not across machines.
|
|
614
|
+
|
|
548
615
|
## claims
|
|
549
616
|
|
|
550
617
|
How machines that share one GitHub account keep off each other's issues.
|
|
@@ -623,6 +690,8 @@ fleet:
|
|
|
623
690
|
timeout_seconds: 5
|
|
624
691
|
silent_after_seconds: 300
|
|
625
692
|
account_label: "max-2"
|
|
693
|
+
expected_drift:
|
|
694
|
+
- "automation.exclude_labels"
|
|
626
695
|
```
|
|
627
696
|
|
|
628
697
|
| Key | Type | Default | Description |
|
|
@@ -632,6 +701,36 @@ fleet:
|
|
|
632
701
|
| `timeout_seconds` | number (0-60] | `5` | The longest wait for any one network step of a read; a whole read gets twice this before the peer is reported as `timeout`. |
|
|
633
702
|
| `silent_after_seconds` | int (>= 60) | `300` | A machine whose daemon heartbeat is older than this is shown as **silent**: its web server answers, but the daemon's heartbeat is missing or old, so its poller cannot be confirmed to be working. |
|
|
634
703
|
| `account_label` | string | unset | A name for this machine's agent subscription, shown on its card, such as `max-2`. Never an email address (refused). |
|
|
704
|
+
| `expected_drift` | list of strings | `[]` | Config differences with the peers that are intended, so the page does not report them. Each entry is a repo (`owner/name`: left out of the comparison entirely), a section (`automation`, `steps`, `schedules`) or one field of one (`automation.exclude_labels`, `steps.review`, `schedules.secops_cron`). Anything else is refused at load. See [Config drift](#config-drift). |
|
|
705
|
+
|
|
706
|
+
### Config drift
|
|
707
|
+
|
|
708
|
+
Each machine's status page publishes a fingerprint of its config in
|
|
709
|
+
`node.config_fingerprint`. Each time the page or `/api/status` is built, it
|
|
710
|
+
compares the latest fingerprint read from each peer with its own, so a change
|
|
711
|
+
on a peer shows within one peer read. A difference shows on that peer's card, for example
|
|
712
|
+
`config: 2 repos only here: org/a, org/b` or
|
|
713
|
+
`config: automation.exclude_labels differs on 3 repos: …`, and marks the card.
|
|
714
|
+
Identical configs show nothing.
|
|
715
|
+
|
|
716
|
+
What is compared: the list of repos; for each repo in both, every field of
|
|
717
|
+
`automation` and of `steps`; and every field of `schedules`. Only repo
|
|
718
|
+
names and hashes are sent, never a value. The fingerprint is the config the
|
|
719
|
+
server was started with, so an edit shows once the daemon is restarted.
|
|
720
|
+
|
|
721
|
+
A peer on a version that sends no fingerprint, or one in another format,
|
|
722
|
+
shows `config drift unknown`, not as drift. So does a peer on a different
|
|
723
|
+
ctrlrelay version than this machine, even one that sends a fingerprint: two
|
|
724
|
+
versions can hash the same config differently (a changed default, say), so
|
|
725
|
+
the two are compared once both run the same version. A field only one side
|
|
726
|
+
has is not compared.
|
|
727
|
+
|
|
728
|
+
`expected_drift` is read on each machine for its own page only. A routing
|
|
729
|
+
label that only some machines carry in `exclude_labels` is listed as
|
|
730
|
+
`automation.exclude_labels` on every machine whose page should not report
|
|
731
|
+
it. Leaving out a field leaves it out on every repo: a different change to
|
|
732
|
+
that field is not reported either, which is the cost of comparing hashes
|
|
733
|
+
rather than values.
|
|
635
734
|
|
|
636
735
|
## repos
|
|
637
736
|
|
|
@@ -681,14 +780,42 @@ and ask the operator), or `never` (skip).
|
|
|
681
780
|
| `codeql_dismiss` | `ask` | CodeQL alert dismissal. |
|
|
682
781
|
| `secret_alerts` | `never` | Secret-scan alerts. |
|
|
683
782
|
| `deploy_after_merge` | `auto` | Whether to deploy after a merged PR. |
|
|
783
|
+
| `operator_prs` | `ask` | `ask` or `ignore`. What the secops sweep does with your own open PRs that touch more than `.github/dependabot.yml`. See [operator_prs](#reposautomationoperator_prs) below. |
|
|
684
784
|
| `accept_foreign_assignments` | `false` | When `true`, the poller also picks up issues assigned to you by someone else. Default (`false`) runs the dev pipeline only on issues you self-assigned. |
|
|
685
785
|
| `exclude_labels` | `["manual", "operator", "instruction"]` | Issue labels that tell the poller "this isn't for the agent". See [exclude_labels](#reposautomationexclude_labels) below. |
|
|
686
786
|
| `include_labels` | `[]` | Issue labels that opt an issue **into** the dev pipeline regardless of who is (or isn't) assigned. See [include_labels](#reposautomationinclude_labels) below. |
|
|
687
787
|
| `require_labels` | `[]` | Issue labels that gate the plain-assignment trigger: when set, assignment alone is no longer enough — the issue must also carry one of these labels. See [require_labels](#reposautomationrequire_labels) below. |
|
|
688
788
|
| `ci_wait_timeout_seconds` | `600` | Unused since #219 (the agent no longer waits on CI; ctrlrelay does). Still accepted so existing configs load. See [ci_wait_timeout_seconds](#reposautomationci_wait_timeout_seconds) below. |
|
|
789
|
+
| `require_proof` | `false` | A dev run's PR goes ready only when the relay itself proved the change: a test that fails on the base and passes on the branch, and no weakened tests. See [require_proof](#reposautomationrequire_proof) below. |
|
|
790
|
+
|
|
791
|
+
The current secops and dev pipelines read the policy settings to bias their
|
|
792
|
+
prompts to the agent — they're not enforced by hard-coded checks.
|
|
793
|
+
`require_proof` is the exception: the relay enforces it.
|
|
794
|
+
|
|
795
|
+
### repos[].automation.operator_prs
|
|
796
|
+
|
|
797
|
+
The secops sweep looks at the open PRs you authored on each repo. One that
|
|
798
|
+
touches only `.github/dependabot.yml` is a "turn on Dependabot" change: it is
|
|
799
|
+
merged if it passes an author check, a diff check (additions only) and a CI
|
|
800
|
+
check, and is a question if it fails any of them.
|
|
801
|
+
Any other PR of yours is a question by default (`ask`), and while the PR
|
|
802
|
+
stays open that question can be asked again on later sweeps.
|
|
803
|
+
|
|
804
|
+
On a repo where you keep your own PRs open for a while (blog posts, drafts),
|
|
805
|
+
set `ignore`:
|
|
806
|
+
|
|
807
|
+
```yaml
|
|
808
|
+
repos:
|
|
809
|
+
- name: you/your-blog
|
|
810
|
+
automation:
|
|
811
|
+
operator_prs: ignore
|
|
812
|
+
```
|
|
689
813
|
|
|
690
|
-
|
|
691
|
-
|
|
814
|
+
With `ignore`, those PRs are left alone: no question, and they are not named
|
|
815
|
+
in the sweep's summary. A PR that touches only `.github/dependabot.yml` is
|
|
816
|
+
still handled as above, and Dependabot PRs and alerts are handled as before.
|
|
817
|
+
Unset is `ask`, and the sweep then runs exactly as it did before the setting
|
|
818
|
+
existed.
|
|
692
819
|
|
|
693
820
|
### repos[].automation.exclude_labels
|
|
694
821
|
|
|
@@ -864,13 +991,120 @@ repos:
|
|
|
864
991
|
- This does not change the overall session timeout (`agent.default_timeout_seconds`,
|
|
865
992
|
default 1800s) — it only bounds the one `ci wait` call.
|
|
866
993
|
|
|
994
|
+
### repos[].automation.require_proof
|
|
995
|
+
|
|
996
|
+
Off by default (#221). Green CI says a change did not break what was already
|
|
997
|
+
tested; it does not say the issue was fixed. With `require_proof` the relay
|
|
998
|
+
checks real evidence before it hands a dev run's PR over as ready.
|
|
999
|
+
|
|
1000
|
+
The agent names its proof in its DONE checkpoint:
|
|
1001
|
+
|
|
1002
|
+
```json
|
|
1003
|
+
"proof": {"command": ["uv", "run", "--offline", "pytest", "tests/test_x.py::test_y"],
|
|
1004
|
+
"tests": ["tests/test_x.py::test_y"]}
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
After CI is green, the relay runs `command` twice, as an argument list with
|
|
1008
|
+
no shell, each time in a fresh copy of committed content (never the agent's
|
|
1009
|
+
worktree):
|
|
1010
|
+
|
|
1011
|
+
- on the commit it pushed, where it must pass;
|
|
1012
|
+
- on the base commit (the merge base with the default branch) with only the
|
|
1013
|
+
branch's changes to test files applied, where it must fail.
|
|
1014
|
+
|
|
1015
|
+
Each run has a 10-minute limit and never gets the operator's GitHub token.
|
|
1016
|
+
When the agent ran under `agent.sandbox: bwrap`, the proof runs under the
|
|
1017
|
+
same kind of bwrap confinement, with no network and only the copy and the
|
|
1018
|
+
agent's cache dir writable. When the agent ran with `sandbox: none`, it
|
|
1019
|
+
already ran arbitrary code as this user, so the proof runs unconfined too.
|
|
1020
|
+
The copies have no untracked files, so the command must set up what it needs
|
|
1021
|
+
offline.
|
|
1022
|
+
|
|
1023
|
+
A run counts only with evidence that the named tests ran. For a pytest
|
|
1024
|
+
command (`pytest`, `uv run pytest`, `python -m pytest`) the relay adds
|
|
1025
|
+
`--junitxml` and reads the report: on the branch each named test must have
|
|
1026
|
+
passed, on the base each must have failed or errored. pytest exiting 2 to 5
|
|
1027
|
+
(a collection error, a usage error, no tests collected, or `uv run` failing
|
|
1028
|
+
to start it), no report, or a named test missing from it is no evidence.
|
|
1029
|
+
One exception, on the base only: a collection error in the file of a named
|
|
1030
|
+
test (a new test file that imports code only the branch has) counts as
|
|
1031
|
+
failing before, recorded as "could not import on base". A collection error
|
|
1032
|
+
only in other files does not. For any other runner the command must exit 0 on the
|
|
1033
|
+
branch and 1 on the base, and each named test (the part after the last
|
|
1034
|
+
`::`) must appear in its output.
|
|
1035
|
+
|
|
1036
|
+
There are three outcomes:
|
|
1037
|
+
|
|
1038
|
+
| Outcome | When |
|
|
1039
|
+
|---|---|
|
|
1040
|
+
| `proven` | The named tests failed on the base and passed on the branch. |
|
|
1041
|
+
| `failed` | The named tests ran on the branch and did not pass; they ran on the base and passed, so they prove nothing; the diff has no real change (empty or whitespace only); or the diff weakens tests. |
|
|
1042
|
+
| `not proven` | No proof was named, the proof is malformed or names a test outside the test files, there is no evidence the named tests ran (a setup, dependency or network failure, a usage error, a collection error), the command could not start, it timed out, or the branch moved after the proof ran. |
|
|
1043
|
+
|
|
1044
|
+
Only `proven` lets the PR go ready. Two exemptions, recorded as `not
|
|
1045
|
+
required`: a diff that only touches docs (`*.md`, `*.rst`, `*.adoc`,
|
|
1046
|
+
`docs/**`), and a repo with `require_proof: false`.
|
|
1047
|
+
|
|
1048
|
+
**The test weakening scan** reads the diff's test files (`tests/`, `test_*.py`,
|
|
1049
|
+
`*_test.py`, `conftest.py`, `*.test.ts`, `*.spec.js`, `__tests__/` and the
|
|
1050
|
+
like) and the test runner's config. These are blockers:
|
|
1051
|
+
|
|
1052
|
+
- a deleted test file, in any language;
|
|
1053
|
+
- a test file renamed to a name the runner does not collect, or moved out
|
|
1054
|
+
of the tests;
|
|
1055
|
+
- a removed test function;
|
|
1056
|
+
- an added skip, xfail or `.only` marker: decorators, a module-level
|
|
1057
|
+
`pytestmark`, `pytest.importorskip`, unittest skips, and JS `.skip`,
|
|
1058
|
+
`xit`, `it.only`;
|
|
1059
|
+
- an added line in runner config that can deselect or skip: `addopts`,
|
|
1060
|
+
`--deselect`, `-k`, `testpaths`, `norecursedirs`, `collect_ignore` (in
|
|
1061
|
+
`pytest.ini`, `pyproject.toml`, `setup.cfg`, `tox.ini`, `conftest.py`),
|
|
1062
|
+
and jest's `testPathIgnorePatterns`, `testMatch`, `testRegex`;
|
|
1063
|
+
- a bare `return` added as the first statement of an existing Python test;
|
|
1064
|
+
- an assert line removed from a diff hunk that adds no assert.
|
|
1065
|
+
|
|
1066
|
+
An assert line removed from a hunk that also adds one is an edit. The scan
|
|
1067
|
+
cannot tell a corrected expectation from a loosened one, so it is shown as a
|
|
1068
|
+
warning on the PR and does not block. Lines are matched by text, so a test or
|
|
1069
|
+
an assert moved to another file is not a hit. Any blocker makes the run
|
|
1070
|
+
`failed`, unless the plan's acceptance criteria say the change is intended
|
|
1071
|
+
with an item `Test removal intended: <why>`. Python and JS/TS are read; a
|
|
1072
|
+
test file in another language makes the scan report `not checked`, never
|
|
1073
|
+
`clean`.
|
|
1074
|
+
|
|
1075
|
+
If no test can show the change, the plan's acceptance criteria can say `No
|
|
1076
|
+
test possible: <why>`. The run is still `not proven` and the PR stays a
|
|
1077
|
+
draft for a person to decide; the PR comment says why.
|
|
1078
|
+
|
|
1079
|
+
**What the PR shows.** One comment, edited in place on a later run: the
|
|
1080
|
+
outcome and why, the proving test, the command, the fail-before and
|
|
1081
|
+
pass-after results with a short redacted tail of each, and any scan hits.
|
|
1082
|
+
A run that is not proven fails like any required step after the PR exists
|
|
1083
|
+
(see [below](#reposteps)): the PR stays a draft, and chat gets `❌ Failed on
|
|
1084
|
+
#N: PR #M held as a draft: not proven …`. A PR GitHub would not open as a
|
|
1085
|
+
draft gets a comment telling readers not to merge it.
|
|
1086
|
+
|
|
1087
|
+
With `require_proof` on, the PR is opened as a draft, as with
|
|
1088
|
+
`steps.review: required`. When both are on, the review step marks it ready
|
|
1089
|
+
after the proof passed and the review posted. Right before it is marked
|
|
1090
|
+
ready, the relay reads the PR's head from GitHub: if it is not the commit the
|
|
1091
|
+
proof ran on, or cannot be read, the run is `not proven` and the PR stays a
|
|
1092
|
+
draft.
|
|
1093
|
+
|
|
1094
|
+
Turn it on only for a repo whose tests can run offline in a fresh copy of
|
|
1095
|
+
the commit. With it off (the default) the agent is not asked for a proof,
|
|
1096
|
+
the PR is opened as before, and no proof comment is posted.
|
|
1097
|
+
|
|
867
1098
|
### repos[].steps
|
|
868
1099
|
|
|
869
1100
|
The dev pipeline does its work in a fixed order: **plan** (an optional
|
|
870
1101
|
planning session, off unless asked for), **code** (the agent run and
|
|
871
1102
|
any questions it asks), **publish** (the relay pushes the branch and opens the
|
|
872
1103
|
pull request), **verify** (wait for CI and mergeability, asking the agent to
|
|
873
|
-
fix what fails, up to `max_fix_attempts` rounds)
|
|
1104
|
+
fix what fails, up to `max_fix_attempts` rounds), **proof** (the relay runs
|
|
1105
|
+
the agent's proving test before and after; switched by
|
|
1106
|
+
[`automation.require_proof`](#reposautomationrequire_proof), not here) and
|
|
1107
|
+
**review** (the
|
|
874
1108
|
configured reviewer reads the pushed commit and posts its findings). The order
|
|
875
1109
|
is not configurable. `steps` says how strict each step is for this repo.
|
|
876
1110
|
Only the dev pipeline reads it. The task and secops pipelines have no
|
|
@@ -918,7 +1152,10 @@ that can each be checked by a command or by reading the diff. The relay then:
|
|
|
918
1152
|
outside the worktree, so it is never committed;
|
|
919
1153
|
- posts the criteria list as one comment on the issue;
|
|
920
1154
|
- adds the plan to the code session's prompt under `## Plan`, telling the
|
|
921
|
-
agent the criteria are what "done" means
|
|
1155
|
+
agent the criteria are what "done" means;
|
|
1156
|
+
- reads two items from the criteria for the proof step: `Test removal
|
|
1157
|
+
intended: <why>` and `No test possible: <why>` (see
|
|
1158
|
+
[require_proof](#reposautomationrequire_proof)).
|
|
922
1159
|
|
|
923
1160
|
If the issue is too vague to plan, the planning session asks one question,
|
|
924
1161
|
through the same chat loop the code session uses. No code session runs and no
|
|
@@ -989,7 +1226,7 @@ exists.** The ordinary failure path, unchanged:
|
|
|
989
1226
|
- chat gets `❌ Failed on #N: …`, never `✅ PR ready`.
|
|
990
1227
|
|
|
991
1228
|
A resumed session (an answer that arrived after the run stopped to ask) goes
|
|
992
|
-
through the same publish and
|
|
1229
|
+
through the same publish, verify and proof steps. It did not run the review before
|
|
993
1230
|
these switches, and with `review` unset or `on` it still does not; with
|
|
994
1231
|
`review: required` it does.
|
|
995
1232
|
|
|
@@ -1024,12 +1261,41 @@ won't silently disable the job at runtime.
|
|
|
1024
1261
|
schedules:
|
|
1025
1262
|
secops_cron: "0 6 * * *" # daily 06:00 sweep across repos
|
|
1026
1263
|
personalization_cron: "*/15 * * * *" # optional auto-pull
|
|
1264
|
+
weekly_summary: # one chat message a week (#230)
|
|
1265
|
+
enabled: true
|
|
1266
|
+
day: mon
|
|
1267
|
+
hour: 9
|
|
1027
1268
|
```
|
|
1028
1269
|
|
|
1029
1270
|
| Key | Type | Required | Default | Description |
|
|
1030
1271
|
|---|---|---|---|---|
|
|
1031
1272
|
| `secops_cron` | string | no | `"0 6 * * *"` | When to run the secops sweep (`ctrlrelay secops run` equivalent) across all configured repos. Set to e.g. `"0 6 * * 1"` for weekly Mondays. |
|
|
1032
1273
|
| `personalization_cron` | string | no | unset | When to auto-pull the personalization repo on this machine. Only effective when [`personalization`](#personalization) is also set. Skip-on-dirty: never rebases under uncommitted operator edits. Adoption is never performed by auto-pull — that stays an init-time concern. |
|
|
1274
|
+
| `weekly_summary.enabled` | bool | no | `true` | Send this node's last-7-day numbers to the chat once a week. `false` turns it off. Nothing is sent when no chat [`transport`](#transport) is configured. |
|
|
1275
|
+
| `weekly_summary.day` | string | no | `"mon"` | The weekday to send it: `mon` to `sun` (full English names work too). |
|
|
1276
|
+
| `weekly_summary.hour` | int | no | `9` | The hour to send it, 0-23, in the top-level `timezone`. |
|
|
1277
|
+
|
|
1278
|
+
### schedules.weekly_summary
|
|
1279
|
+
|
|
1280
|
+
Once a week, each node sends one chat message with its own numbers for
|
|
1281
|
+
the last 7 days, the same ones its status page shows: runs started, done,
|
|
1282
|
+
failed, blocked and stuck, the success rate, the median time to PR and to
|
|
1283
|
+
merge, how many issues the relay finished were reopened, and the median
|
|
1284
|
+
tokens per run (with the cost, when the agent reported one) over the runs
|
|
1285
|
+
that ended with their usage recorded. A number the node cannot read is
|
|
1286
|
+
written `unknown`, with the reason, never 0.
|
|
1287
|
+
|
|
1288
|
+
The daemon checks every five minutes. It sends at the first check after
|
|
1289
|
+
`day` at `hour`:00 that finds nothing delivered since then, so a node that
|
|
1290
|
+
was off at that time sends when it comes back, and a send that fails (the
|
|
1291
|
+
bridge restarting, the chat unreachable) is logged as
|
|
1292
|
+
`summary.weekly.send_failed` and tried again five minutes later. The time
|
|
1293
|
+
of the last delivery is kept in the state database, so restarting the
|
|
1294
|
+
poller does not send the week a second time. Two cases can still give an
|
|
1295
|
+
extra message: a poller stopped after the message went out but before
|
|
1296
|
+
that time was written, and changing `day` or `hour` to a time that has
|
|
1297
|
+
already passed this week. The first check on a new or upgraded node only
|
|
1298
|
+
notes the time, so its first summary goes at the next `day` and `hour`.
|
|
1033
1299
|
|
|
1034
1300
|
## personalization
|
|
1035
1301
|
|