ctrlrelay 0.19.1__tar.gz → 0.20.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/CHANGELOG.md +85 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/PKG-INFO +1 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/configuration.md +25 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/pyproject.toml +1 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/cli.py +40 -4
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/config.py +9 -2
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/dispatcher.py +4 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/github.py +13 -0
- ctrlrelay-0.20.1/src/ctrlrelay/core/orphans.py +433 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/poller.py +87 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/state.py +202 -7
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/worktree.py +319 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/base.py +5 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/dev.py +779 -148
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/secops.py +10 -3
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/task.py +10 -3
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/fleet.py +5 -0
- ctrlrelay-0.20.1/src/ctrlrelay/web/health.py +396 -0
- ctrlrelay-0.20.1/src/ctrlrelay/web/links.py +74 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/static/index.html +42 -12
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/status.py +16 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_start.py +114 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_integration.py +12 -0
- ctrlrelay-0.20.1/tests/test_dev_plan_step.py +447 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_step_switches.py +0 -1
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_work_steps.py +5 -2
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dispatcher.py +2 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_fleet.py +1 -0
- ctrlrelay-0.20.1/tests/test_orphans.py +564 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_poller.py +20 -0
- ctrlrelay-0.20.1/tests/test_poller_recheck.py +240 -0
- ctrlrelay-0.20.1/tests/test_question_survives_restart.py +606 -0
- ctrlrelay-0.20.1/tests/test_timeout_save.py +419 -0
- ctrlrelay-0.20.1/tests/test_web_health.py +624 -0
- ctrlrelay-0.20.1/tests/test_web_peer_links.py +99 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_server.py +66 -3
- ctrlrelay-0.19.1/src/ctrlrelay/web/health.py +0 -167
- ctrlrelay-0.19.1/src/ctrlrelay/web/links.py +0 -35
- ctrlrelay-0.19.1/tests/test_web_health.py +0 -228
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/.gitignore +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/LICENSE +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/README.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/SECURITY.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/Gemfile +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/_config.yml +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/adding-a-machine.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/architecture.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/bridge.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/cli.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/development.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/getting-started.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/index.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/llms.txt +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/operations.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/personalization.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/registered-mode.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/node_tag.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/agent_env.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/claims.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/events.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/github_budget.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/heartbeat.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/job_claims.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/node_check.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/publish.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/core/usage.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/merge.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/numbers.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/server.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/transcript.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/src/ctrlrelay/web/waiting.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/__init__.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/_poller_fakes.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/_publish_fakes.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/conftest.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/fixtures/gh_search_issues.json +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/fixtures/transcript_sample.jsonl +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_backend_wiring.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_no_keys.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_routing.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_agent_sandbox.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_archived.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_audit.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_binary_resolution.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_bridge_start_command.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_chat_machines.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_claim_reconciliation.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_config.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_dispatch_outcome_hook.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_event_emission.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_events.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_github.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_github_budget.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_install.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_job_claims.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_network.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_node_check.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_node_claims.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_obs.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_opencode_adapter.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_personalization_merge.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_poller_search.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_pre_dispatch_retry.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_publish_failure_retry.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_question_backoff.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_redact.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_relay_alert_off_lock.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_relay_git_263.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_resume_prompt_contract.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_review_copy.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_setup.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_state.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_transport.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_unreviewed_pr_announcement.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_usage.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_links.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_numbers.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_status.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_transcript.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_web_waiting.py +0 -0
- {ctrlrelay-0.19.1 → ctrlrelay-0.20.1}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.20.1] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The status page's server panel reads memory and uptime on macOS too. It
|
|
15
|
+
read those two from `/proc`, which macOS does not have, so on a Mac those
|
|
16
|
+
rows were empty and the page printed the read error. On macOS it now
|
|
17
|
+
uses `sysctl` and `vm_stat`. macOS has no `MemAvailable`, so "available"
|
|
18
|
+
there is free plus inactive pages: an estimate, and not the same
|
|
19
|
+
measurement as Linux's (#321).
|
|
20
|
+
|
|
21
|
+
- A run waiting for the operator's answer is no longer lost when the poller
|
|
22
|
+
stops during the wait. The question is now saved, and the session marked
|
|
23
|
+
`blocked`, before the wait starts rather than after it ends (if that
|
|
24
|
+
write fails it is logged and the wait goes ahead as before). If the
|
|
25
|
+
poller stops, a later answer in chat reaches the saved question and
|
|
26
|
+
resumes the same agent session; when the poller starts again it drops the
|
|
27
|
+
repo lock such a run held, if the process that ran it is gone, so the
|
|
28
|
+
repo is not wedged until then. An answer that arrives during the wait
|
|
29
|
+
still resumes the run at once and closes the saved question; if a reply
|
|
30
|
+
already answered the saved question first, that first answer stands and
|
|
31
|
+
the run is resumed once, with it. A planning session's question resumes
|
|
32
|
+
the plan, not the code. The claim on the issue is not marked held while the
|
|
33
|
+
question waits, so after a stop it goes stale as before (#324).
|
|
34
|
+
|
|
35
|
+
- An issue is read again from GitHub just before its run starts. A poll
|
|
36
|
+
returns a batch that is handled one issue at a time, so with one slot the
|
|
37
|
+
last issue could start an hour after the poll saw it, after its trigger
|
|
38
|
+
label was removed or the issue was closed. Now an issue that is closed,
|
|
39
|
+
has gained an `exclude_labels` label, or no longer meets any trigger is
|
|
40
|
+
skipped and left for the next poll to judge; so is one that cannot be read.
|
|
41
|
+
The run is routed on the labels read then, not the poll's. Who added a
|
|
42
|
+
trigger is not asked again: a trigger the operator removed and someone
|
|
43
|
+
else put back in that window still counts (#326).
|
|
44
|
+
|
|
45
|
+
## [0.20.0] - 2026-11-04
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `repos[].steps.plan` (`off`, `on`, `required`; unset is `off`) runs a
|
|
50
|
+
planning session before the dev pipeline's code session. It changes no
|
|
51
|
+
files and writes a plan with numbered acceptance criteria, saved under
|
|
52
|
+
`<paths.contexts>/<owner>-<repo>/plans/issue-<n>.md` (outside the worktree),
|
|
53
|
+
posted on the issue as the criteria list, and added to the code session's
|
|
54
|
+
prompt. A vague issue gets one question instead of a pull request. A plan
|
|
55
|
+
without criteria is a failed plan: `on` reports it and codes without one,
|
|
56
|
+
`required` fails the run (#220).
|
|
57
|
+
- On the status page, a peer machine's name links to that machine's own
|
|
58
|
+
page: in the machine strip, in "Who has what" and on a "Needs you" card.
|
|
59
|
+
The address is the `fleet.peers` entry from this machine's config, cut to
|
|
60
|
+
`<scheme>://<host>[:port]/`; nothing a peer answers changes it, and an
|
|
61
|
+
entry with userinfo or a non-http scheme gives plain text (#319).
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- A run whose relay process died mid-run (the machine ran out of memory, or
|
|
66
|
+
the service was stopped) no longer stays `running` for ever. Sessions now
|
|
67
|
+
record the process running them (`owner_pid`, `owner_run`) and their claim
|
|
68
|
+
id. When the poller starts, it finds `running` sessions whose process is
|
|
69
|
+
gone, and in whose worktree no process is still working, and marks them
|
|
70
|
+
failed, dropping any repo lock they held. A dev or task issue goes back to
|
|
71
|
+
the next poll unless an open pull request was found for its branch. Where
|
|
72
|
+
the run had claimed the issue, the relay posts a comment that names the
|
|
73
|
+
worktree and releases the claim. The relay deletes nothing in the
|
|
74
|
+
worktree. It detaches the worktree's HEAD, when that can be done safely,
|
|
75
|
+
so the next run can check the issue's branch out. Rows written by an older
|
|
76
|
+
version name no process and are left alone (#312).
|
|
77
|
+
- A dev run whose agent session hits its time limit no longer throws away
|
|
78
|
+
the agent's work. Before the worktree is removed, the files on disk are
|
|
79
|
+
committed on top of the commit it sits on, and that commit, or the
|
|
80
|
+
unpushed commits alone, are pushed to a new `wip/issue-<n>-<run id>`
|
|
81
|
+
branch. The run's own branch is not pushed, no pull request is opened, and
|
|
82
|
+
the local run branch is kept, since it can hold commits the worktree's
|
|
83
|
+
HEAD does not reach. The failure comment on the issue names the wip branch
|
|
84
|
+
and, when it is known, how long the run took. If the work cannot be saved,
|
|
85
|
+
the worktree is kept on disk and the comment names it. A run with nothing
|
|
86
|
+
new loses only its worktree. Files the worktree's `.gitignore` ignores are
|
|
87
|
+
not saved; files that cannot be read are not saved either, and then the
|
|
88
|
+
worktree is kept and the comment says so. The files are read by a git with
|
|
89
|
+
a private git dir, index, config and attributes override, so filters,
|
|
90
|
+
hooks and attributes named by the agent do not take effect (#311).
|
|
91
|
+
|
|
10
92
|
## [0.19.1] - 2026-11-03
|
|
11
93
|
|
|
12
94
|
### Fixed
|
|
@@ -2266,7 +2348,9 @@ pipeline).
|
|
|
2266
2348
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
2267
2349
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
2268
2350
|
|
|
2269
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.
|
|
2351
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.1...HEAD
|
|
2352
|
+
[0.20.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.0...v0.20.1
|
|
2353
|
+
[0.20.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.1...v0.20.0
|
|
2270
2354
|
[0.19.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...v0.19.1
|
|
2271
2355
|
[0.19.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.18.0...v0.19.0
|
|
2272
2356
|
[0.18.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...v0.18.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.1
|
|
4
4
|
Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
|
|
5
5
|
Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
|
|
6
6
|
Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
|
|
@@ -865,7 +865,8 @@ repos:
|
|
|
865
865
|
|
|
866
866
|
### repos[].steps
|
|
867
867
|
|
|
868
|
-
The dev pipeline does its work in a fixed order: **
|
|
868
|
+
The dev pipeline does its work in a fixed order: **plan** (an optional
|
|
869
|
+
planning session, off unless asked for), **code** (the agent run and
|
|
869
870
|
any questions it asks), **publish** (the relay pushes the branch and opens the
|
|
870
871
|
pull request), **verify** (wait for CI and mergeability, asking the agent to
|
|
871
872
|
fix what fails, up to `max_fix_attempts` rounds) and **review** (the
|
|
@@ -893,6 +894,7 @@ repos:
|
|
|
893
894
|
|
|
894
895
|
| Step | Accepts | Unset means |
|
|
895
896
|
|---|---|---|
|
|
897
|
+
| `plan` | `off`, `on`, `required` | `off`: no planning session. |
|
|
896
898
|
| `code` | `required` | A failed or crashed agent run stops the run. Same as `required`. |
|
|
897
899
|
| `publish` | `required` | A refused or failed push stops the run (the branch is kept). Same as `required`. |
|
|
898
900
|
| `verify` | `off`, `required` | CI still red after the fix rounds stops the run. CI that is still pending when the wait times out hands the PR over. |
|
|
@@ -905,6 +907,28 @@ PR as ready while its CI is red, with no line in the announcement saying so.
|
|
|
905
907
|
Unknown step names (a typo such as `verfy`) are refused as well. A bare `on`
|
|
906
908
|
or `off` works unquoted.
|
|
907
909
|
|
|
910
|
+
**`plan: on`** or **`plan: required`** runs one planning session in the
|
|
911
|
+
run's worktree before the code session. It is told to read the issue and the
|
|
912
|
+
repo and change no files, and returns a plan with four headings: `## Change`,
|
|
913
|
+
`## Files`, `## Risks` and `## Acceptance criteria`, a numbered list of items
|
|
914
|
+
that can each be checked by a command or by reading the diff. The relay then:
|
|
915
|
+
|
|
916
|
+
- saves the plan to `<paths.contexts>/<owner>-<repo>/plans/issue-<n>.md`,
|
|
917
|
+
outside the worktree, so it is never committed;
|
|
918
|
+
- posts the criteria list as one comment on the issue;
|
|
919
|
+
- adds the plan to the code session's prompt under `## Plan`, telling the
|
|
920
|
+
agent the criteria are what "done" means.
|
|
921
|
+
|
|
922
|
+
If the issue is too vague to plan, the planning session asks one question,
|
|
923
|
+
through the same chat loop the code session uses. No code session runs and no
|
|
924
|
+
PR is opened until it is answered; an answer that arrives after the run
|
|
925
|
+
stopped resumes the planning session, then codes with the plan.
|
|
926
|
+
|
|
927
|
+
A plan with no numbered acceptance criteria is a failed plan, as is a
|
|
928
|
+
planning session that fails or crashes. With `on` the failure is posted on the
|
|
929
|
+
issue and the code session runs without a plan; with `required` the run fails
|
|
930
|
+
and no code session runs.
|
|
931
|
+
|
|
908
932
|
**`verify: required`** also fails the run when the CI wait times out, before
|
|
909
933
|
or after a fix round. (Checks that cannot be read at all fail the run in every
|
|
910
934
|
mode that waits on CI, not only under `required`.)
|
|
@@ -3801,7 +3801,8 @@ def poller_start(
|
|
|
3801
3801
|
)
|
|
3802
3802
|
try:
|
|
3803
3803
|
state_db.mark_pending_resume_resumed(
|
|
3804
|
-
session_id
|
|
3804
|
+
session_id, answered_at=row["answered_at"],
|
|
3805
|
+
answer=row["answer"],
|
|
3805
3806
|
)
|
|
3806
3807
|
except Exception:
|
|
3807
3808
|
pass
|
|
@@ -3844,7 +3845,8 @@ def poller_start(
|
|
|
3844
3845
|
)
|
|
3845
3846
|
try:
|
|
3846
3847
|
state_db.mark_pending_resume_resumed(
|
|
3847
|
-
session_id
|
|
3848
|
+
session_id, answered_at=row["answered_at"],
|
|
3849
|
+
answer=row["answer"],
|
|
3848
3850
|
)
|
|
3849
3851
|
except Exception:
|
|
3850
3852
|
pass
|
|
@@ -3862,7 +3864,10 @@ def poller_start(
|
|
|
3862
3864
|
# same broken row. Operator can inspect via the
|
|
3863
3865
|
# sessions table.
|
|
3864
3866
|
try:
|
|
3865
|
-
state_db.mark_pending_resume_resumed(
|
|
3867
|
+
state_db.mark_pending_resume_resumed(
|
|
3868
|
+
session_id, answered_at=row["answered_at"],
|
|
3869
|
+
answer=row["answer"],
|
|
3870
|
+
)
|
|
3866
3871
|
except Exception:
|
|
3867
3872
|
pass
|
|
3868
3873
|
continue
|
|
@@ -3926,7 +3931,10 @@ def poller_start(
|
|
|
3926
3931
|
continue
|
|
3927
3932
|
|
|
3928
3933
|
try:
|
|
3929
|
-
state_db.mark_pending_resume_resumed(
|
|
3934
|
+
state_db.mark_pending_resume_resumed(
|
|
3935
|
+
session_id, answered_at=row["answered_at"],
|
|
3936
|
+
answer=row["answer"],
|
|
3937
|
+
)
|
|
3930
3938
|
except Exception:
|
|
3931
3939
|
pass
|
|
3932
3940
|
|
|
@@ -4203,6 +4211,34 @@ def poller_start(
|
|
|
4203
4211
|
skip_initial_persist=True,
|
|
4204
4212
|
)
|
|
4205
4213
|
|
|
4214
|
+
# Runs whose process died with them (#312): settle them before
|
|
4215
|
+
# the first poll, so the issues they held are taken again by
|
|
4216
|
+
# that poll rather than skipped as already seen.
|
|
4217
|
+
from ctrlrelay.core.orphans import reap_orphaned_sessions
|
|
4218
|
+
reaped = await reap_orphaned_sessions(
|
|
4219
|
+
state_db, poller=poller, github=github, worktree=worktree,
|
|
4220
|
+
)
|
|
4221
|
+
for r in reaped:
|
|
4222
|
+
where = (
|
|
4223
|
+
f"#{r['issue_number']} in {r['repo']}"
|
|
4224
|
+
if r["issue_number"] is not None else r["repo"]
|
|
4225
|
+
)
|
|
4226
|
+
kept = (
|
|
4227
|
+
f" Its worktree is kept at {r['worktree_path']}."
|
|
4228
|
+
if r["worktree_path"]
|
|
4229
|
+
and Path(r["worktree_path"]).exists() else ""
|
|
4230
|
+
)
|
|
4231
|
+
console.print(
|
|
4232
|
+
f"[yellow]Run {r['session_id']} for {where} stopped "
|
|
4233
|
+
f"when the relay did; marked failed.{kept}[/yellow]"
|
|
4234
|
+
)
|
|
4235
|
+
# A run that stopped while waiting for an answer (#324) is
|
|
4236
|
+
# `blocked` with its question saved, and is resumed when the
|
|
4237
|
+
# answer comes; the repo lock it held would only wedge the
|
|
4238
|
+
# repo until then.
|
|
4239
|
+
from ctrlrelay.core.orphans import release_dead_blocked_locks
|
|
4240
|
+
release_dead_blocked_locks(state_db)
|
|
4241
|
+
|
|
4206
4242
|
try:
|
|
4207
4243
|
await github_budget.refresh()
|
|
4208
4244
|
await run_poll_loop(
|
|
@@ -507,6 +507,9 @@ _REPO_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
|
|
|
507
507
|
# reads this table as well, so a caller that bypasses the config cannot
|
|
508
508
|
# get a mode accepted and ignored.
|
|
509
509
|
DEV_STEP_MODES: dict[str, tuple[str, ...]] = {
|
|
510
|
+
# ``plan`` (#220) runs before ``code``. Unset is ``off``: a repo that
|
|
511
|
+
# does not ask for a plan runs exactly as it did before the step.
|
|
512
|
+
"plan": ("off", "on", "required"),
|
|
510
513
|
"code": ("required",),
|
|
511
514
|
"publish": ("required",),
|
|
512
515
|
"verify": ("off", "required"),
|
|
@@ -514,6 +517,7 @@ DEV_STEP_MODES: dict[str, tuple[str, ...]] = {
|
|
|
514
517
|
}
|
|
515
518
|
|
|
516
519
|
_STEP_REFUSALS: dict[str, str] = {
|
|
520
|
+
"plan": "",
|
|
517
521
|
"code": (
|
|
518
522
|
"a dev run without the agent run has nothing to publish"
|
|
519
523
|
),
|
|
@@ -567,7 +571,7 @@ class DevStepsConfig(BaseModel):
|
|
|
567
571
|
``verify`` stops it when CI is still red after the fix rounds, but
|
|
568
572
|
hands the pull request over when the CI wait times out; ``review``
|
|
569
573
|
reports a review that did not post and hands the pull request over
|
|
570
|
-
anyway (``on``).
|
|
574
|
+
anyway (``on``); ``plan`` does not run (``off``).
|
|
571
575
|
|
|
572
576
|
Unknown keys are refused, so a typo such as ``verfy: required``
|
|
573
577
|
cannot load as "unset".
|
|
@@ -575,12 +579,15 @@ class DevStepsConfig(BaseModel):
|
|
|
575
579
|
|
|
576
580
|
model_config = {"extra": "forbid"}
|
|
577
581
|
|
|
582
|
+
plan: str | None = None
|
|
578
583
|
code: str | None = None
|
|
579
584
|
publish: str | None = None
|
|
580
585
|
verify: str | None = None
|
|
581
586
|
review: str | None = None
|
|
582
587
|
|
|
583
|
-
@field_validator(
|
|
588
|
+
@field_validator(
|
|
589
|
+
"plan", "code", "publish", "verify", "review", mode="before"
|
|
590
|
+
)
|
|
584
591
|
@classmethod
|
|
585
592
|
def _known_mode(cls, v: Any, info: Any) -> str | None:
|
|
586
593
|
return check_dev_step_mode(info.field_name, v)
|
|
@@ -133,6 +133,9 @@ class SessionResult:
|
|
|
133
133
|
stdout: str = ""
|
|
134
134
|
stderr: str = ""
|
|
135
135
|
agent_session_id: str | None = None
|
|
136
|
+
# The relay killed the agent at its time limit (#311). Set only by the
|
|
137
|
+
# relay's own timeout path, never parsed from anything the agent wrote.
|
|
138
|
+
timed_out: bool = False
|
|
136
139
|
|
|
137
140
|
@property
|
|
138
141
|
def success(self) -> bool:
|
|
@@ -352,6 +355,7 @@ class CliAgentAdapter:
|
|
|
352
355
|
exit_code=-1,
|
|
353
356
|
state=None,
|
|
354
357
|
stderr="Session timed out",
|
|
358
|
+
timed_out=True,
|
|
355
359
|
)
|
|
356
360
|
except asyncio.CancelledError as e:
|
|
357
361
|
# Scheduler/shutdown cancel: kill the child BEFORE re-raising
|
|
@@ -561,6 +561,19 @@ class GitHubCLI:
|
|
|
561
561
|
)
|
|
562
562
|
return json.loads(output)
|
|
563
563
|
|
|
564
|
+
async def get_issue_gate(self, repo: str, issue_number: int) -> dict[str, Any]:
|
|
565
|
+
"""Return ``{"state", "labels", "assignees"}`` for one issue, read
|
|
566
|
+
live over REST: the fields that decide whether the relay may still
|
|
567
|
+
start it (#326). ``labels`` and ``assignees`` are lists of names.
|
|
568
|
+
"""
|
|
569
|
+
output = await self._run_gh(
|
|
570
|
+
"api", f"repos/{repo}/issues/{issue_number}",
|
|
571
|
+
"--jq",
|
|
572
|
+
"{state: .state, labels: [.labels[].name], "
|
|
573
|
+
"assignees: [.assignees[].login]}",
|
|
574
|
+
)
|
|
575
|
+
return json.loads(output)
|
|
576
|
+
|
|
564
577
|
async def create_pr(
|
|
565
578
|
self,
|
|
566
579
|
repo: str,
|