ctrlrelay 0.19.0__tar.gz → 0.20.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.19.0 → ctrlrelay-0.20.0}/CHANGELOG.md +59 -1
- ctrlrelay-0.20.0/PKG-INFO +164 -0
- ctrlrelay-0.20.0/README.md +123 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/_config.yml +7 -2
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/adding-a-machine.md +9 -6
- ctrlrelay-0.20.0/docs/architecture.md +231 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/bridge.md +115 -87
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/cli.md +32 -21
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/configuration.md +45 -16
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/development.md +18 -15
- ctrlrelay-0.20.0/docs/feedback-loop.md +209 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/getting-started.md +32 -21
- ctrlrelay-0.20.0/docs/index.md +89 -0
- ctrlrelay-0.20.0/docs/llms.txt +36 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/operations.md +17 -10
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/pyproject.toml +1 -1
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/cli.py +22 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/config.py +9 -2
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/dispatcher.py +4 -0
- ctrlrelay-0.20.0/src/ctrlrelay/core/orphans.py +388 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/state.py +65 -1
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/worktree.py +319 -1
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/merge.py +37 -2
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/base.py +5 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/dev.py +653 -136
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/secops.py +10 -3
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/task.py +10 -3
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/fleet.py +5 -0
- ctrlrelay-0.20.0/src/ctrlrelay/web/links.py +74 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/static/index.html +42 -12
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_start.py +54 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_integration.py +4 -0
- ctrlrelay-0.20.0/tests/test_dev_plan_step.py +395 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_step_switches.py +0 -1
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_work_steps.py +5 -2
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dispatcher.py +2 -0
- ctrlrelay-0.20.0/tests/test_orphans.py +564 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_personalization_merge.py +39 -21
- ctrlrelay-0.20.0/tests/test_timeout_save.py +419 -0
- ctrlrelay-0.20.0/tests/test_web_peer_links.py +99 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_server.py +66 -3
- ctrlrelay-0.19.0/PKG-INFO +0 -263
- ctrlrelay-0.19.0/README.md +0 -222
- ctrlrelay-0.19.0/docs/architecture.md +0 -222
- ctrlrelay-0.19.0/docs/feedback-loop.md +0 -197
- ctrlrelay-0.19.0/docs/index.md +0 -62
- ctrlrelay-0.19.0/src/ctrlrelay/web/links.py +0 -35
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/.gitignore +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/LICENSE +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/SECURITY.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/personalization.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/registered-mode.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/node_tag.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/agent_env.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/claims.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/events.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/github.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/github_budget.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/heartbeat.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/job_claims.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/node_check.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/poller.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/publish.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/core/usage.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/health.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/numbers.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/server.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/status.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/transcript.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/src/ctrlrelay/web/waiting.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/_poller_fakes.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/_publish_fakes.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/fixtures/gh_search_issues.json +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/fixtures/transcript_sample.jsonl +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_backend_wiring.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_no_keys.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_routing.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_agent_sandbox.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_archived.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_binary_resolution.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_bridge_start_command.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_chat_machines.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_claim_reconciliation.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_config.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_dispatch_outcome_hook.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_event_emission.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_events.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_fleet.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_github_budget.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_install.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_job_claims.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_mattermost_handler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_network.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_node_check.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_node_claims.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_obs.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_opencode_adapter.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_poller.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_poller_search.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_pre_dispatch_retry.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_publish_failure_retry.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_question_backoff.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_redact.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_relay_alert_off_lock.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_relay_git_263.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_resume_prompt_contract.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_review_copy.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_setup.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_unreviewed_pr_announcement.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_usage.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_health.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_links.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_numbers.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_status.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_transcript.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_web_waiting.py +0 -0
- {ctrlrelay-0.19.0 → ctrlrelay-0.20.0}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.20.0] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `repos[].steps.plan` (`off`, `on`, `required`; unset is `off`) runs a
|
|
15
|
+
planning session before the dev pipeline's code session. It changes no
|
|
16
|
+
files and writes a plan with numbered acceptance criteria, saved under
|
|
17
|
+
`<paths.contexts>/<owner>-<repo>/plans/issue-<n>.md` (outside the worktree),
|
|
18
|
+
posted on the issue as the criteria list, and added to the code session's
|
|
19
|
+
prompt. A vague issue gets one question instead of a pull request. A plan
|
|
20
|
+
without criteria is a failed plan: `on` reports it and codes without one,
|
|
21
|
+
`required` fails the run (#220).
|
|
22
|
+
- On the status page, a peer machine's name links to that machine's own
|
|
23
|
+
page: in the machine strip, in "Who has what" and on a "Needs you" card.
|
|
24
|
+
The address is the `fleet.peers` entry from this machine's config, cut to
|
|
25
|
+
`<scheme>://<host>[:port]/`; nothing a peer answers changes it, and an
|
|
26
|
+
entry with userinfo or a non-http scheme gives plain text (#319).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- A run whose relay process died mid-run (the machine ran out of memory, or
|
|
31
|
+
the service was stopped) no longer stays `running` for ever. Sessions now
|
|
32
|
+
record the process running them (`owner_pid`, `owner_run`) and their claim
|
|
33
|
+
id. When the poller starts, it finds `running` sessions whose process is
|
|
34
|
+
gone, and in whose worktree no process is still working, and marks them
|
|
35
|
+
failed, dropping any repo lock they held. A dev or task issue goes back to
|
|
36
|
+
the next poll unless an open pull request was found for its branch. Where
|
|
37
|
+
the run had claimed the issue, the relay posts a comment that names the
|
|
38
|
+
worktree and releases the claim. The relay deletes nothing in the
|
|
39
|
+
worktree. It detaches the worktree's HEAD, when that can be done safely,
|
|
40
|
+
so the next run can check the issue's branch out. Rows written by an older
|
|
41
|
+
version name no process and are left alone (#312).
|
|
42
|
+
- A dev run whose agent session hits its time limit no longer throws away
|
|
43
|
+
the agent's work. Before the worktree is removed, the files on disk are
|
|
44
|
+
committed on top of the commit it sits on, and that commit, or the
|
|
45
|
+
unpushed commits alone, are pushed to a new `wip/issue-<n>-<run id>`
|
|
46
|
+
branch. The run's own branch is not pushed, no pull request is opened, and
|
|
47
|
+
the local run branch is kept, since it can hold commits the worktree's
|
|
48
|
+
HEAD does not reach. The failure comment on the issue names the wip branch
|
|
49
|
+
and, when it is known, how long the run took. If the work cannot be saved,
|
|
50
|
+
the worktree is kept on disk and the comment names it. A run with nothing
|
|
51
|
+
new loses only its worktree. Files the worktree's `.gitignore` ignores are
|
|
52
|
+
not saved; files that cannot be read are not saved either, and then the
|
|
53
|
+
worktree is kept and the comment says so. The files are read by a git with
|
|
54
|
+
a private git dir, index, config and attributes override, so filters,
|
|
55
|
+
hooks and attributes named by the agent do not take effect (#311).
|
|
56
|
+
|
|
57
|
+
## [0.19.1] - 2026-11-03
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- A synced or live `settings.json` nested more than 64 levels deep is now
|
|
62
|
+
refused right after it is read. On Python 3.14 the parser accepts about
|
|
63
|
+
65,000 levels, and writing that back out used all the memory on the
|
|
64
|
+
machine instead of failing (#313).
|
|
65
|
+
|
|
10
66
|
## [0.19.0] - 2026-11-03
|
|
11
67
|
|
|
12
68
|
### Added
|
|
@@ -2257,7 +2313,9 @@ pipeline).
|
|
|
2257
2313
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
2258
2314
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
2259
2315
|
|
|
2260
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.
|
|
2316
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.20.0...HEAD
|
|
2317
|
+
[0.20.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.1...v0.20.0
|
|
2318
|
+
[0.19.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.19.0...v0.19.1
|
|
2261
2319
|
[0.19.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.18.0...v0.19.0
|
|
2262
2320
|
[0.18.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.1...v0.18.0
|
|
2263
2321
|
[0.17.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.17.0...v0.17.1
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ctrlrelay
|
|
3
|
+
Version: 0.20.0
|
|
4
|
+
Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
|
|
5
|
+
Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
|
|
6
|
+
Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
|
|
7
|
+
Project-URL: Repository, https://github.com/AInvirion/ctrlrelay
|
|
8
|
+
Project-URL: Issues, https://github.com/AInvirion/ctrlrelay/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/AInvirion/ctrlrelay/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Oscar Valenzuela <oscar.valenzuela.b@gmail.com>
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: automation,claude,github,orchestrator
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
24
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
25
|
+
Classifier: Topic :: System :: Systems Administration
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Requires-Dist: apscheduler>=3.10.0
|
|
28
|
+
Requires-Dist: httpx>=0.27.0
|
|
29
|
+
Requires-Dist: pydantic>=2.0.0
|
|
30
|
+
Requires-Dist: python-telegram-bot>=21.0
|
|
31
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
32
|
+
Requires-Dist: rich>=13.0.0
|
|
33
|
+
Requires-Dist: typer>=0.12.0
|
|
34
|
+
Requires-Dist: websockets>=12.0
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# ctrlrelay
|
|
43
|
+
|
|
44
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
45
|
+
[](https://pypi.org/project/ctrlrelay/)
|
|
46
|
+
[](https://pypi.org/project/ctrlrelay/)
|
|
47
|
+
[](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml)
|
|
48
|
+
|
|
49
|
+
**Assign an issue. Get a reviewed pull request. Answer questions from your phone.**
|
|
50
|
+
|
|
51
|
+
ctrlrelay turns the coding agent you already use into a teammate that works
|
|
52
|
+
your GitHub backlog in the background. It picks up issues, writes the code in
|
|
53
|
+
a clean branch, opens a pull request, waits for CI, gets the change reviewed,
|
|
54
|
+
and pings you on **Telegram** or **Mattermost** only when it truly needs you.
|
|
55
|
+
|
|
56
|
+
It runs on your own machine. Your code, your credentials, your agent
|
|
57
|
+
subscription. No hosted service in the middle.
|
|
58
|
+
|
|
59
|
+
**[Documentation](https://ainvirion.github.io/ctrlrelay/)** ·
|
|
60
|
+
**[Get started](https://ainvirion.github.io/ctrlrelay/getting-started/)** ·
|
|
61
|
+
**[Why ctrlrelay](https://ainvirion.github.io/ctrlrelay/#why-ctrlrelay)**
|
|
62
|
+
|
|
63
|
+
## Why ctrlrelay
|
|
64
|
+
|
|
65
|
+
Coding agents are great when you sit and watch them. ctrlrelay is for the
|
|
66
|
+
work you don't want to sit and watch.
|
|
67
|
+
|
|
68
|
+
- **Your backlog is the to-do list.** No new tool to learn. Assign an issue
|
|
69
|
+
(or add a label) and the work starts. The result is a normal pull request
|
|
70
|
+
your team already knows how to review.
|
|
71
|
+
- **Runs where your code already lives.** ctrlrelay is a small program on your
|
|
72
|
+
laptop or server — not a cloud service. Nothing leaves your machine except
|
|
73
|
+
the calls your agent and GitHub already make. No per-seat platform fee on
|
|
74
|
+
top of the agent you already pay for.
|
|
75
|
+
- **It asks instead of guessing.** When the agent is stuck, you get the
|
|
76
|
+
question in chat. You reply from your phone and the *same* session picks up
|
|
77
|
+
where it stopped.
|
|
78
|
+
- **Safe by default.** Every issue gets its own isolated copy of the repo.
|
|
79
|
+
One job per repo at a time. Optional Linux sandbox that hides your secrets
|
|
80
|
+
from the agent. Nothing merges without your rules allowing it.
|
|
81
|
+
- **Done means done.** It waits for CI, fixes failing checks, and posts an
|
|
82
|
+
independent review on the pull request — so what reaches you is ready to
|
|
83
|
+
read, not a first draft.
|
|
84
|
+
- **Pick your agent per repo.** Claude Code today, or OpenCode — which can
|
|
85
|
+
run on a local model — for the repos where that fits.
|
|
86
|
+
- **Grows with you.** Start on one laptop. Add more machines later; they share
|
|
87
|
+
the work without stepping on each other.
|
|
88
|
+
- **Open source.** Apache 2.0. Read every line of what runs on your machine.
|
|
89
|
+
|
|
90
|
+
## What it does
|
|
91
|
+
|
|
92
|
+
- **Issues → pull requests.** Watches every repo you choose and works each new
|
|
93
|
+
issue end to end.
|
|
94
|
+
- **Questions in your chat.** Telegram (a private bot) or Mattermost (a channel
|
|
95
|
+
on your own server).
|
|
96
|
+
- **Automatic review.** Every agent pull request gets a second-opinion review
|
|
97
|
+
comment before you look at it.
|
|
98
|
+
- **Daily security sweep.** Reviews Dependabot alerts and pull requests across
|
|
99
|
+
your repos every morning.
|
|
100
|
+
- **Status page.** See what each agent is doing, what is waiting, and what
|
|
101
|
+
needs you — at a glance.
|
|
102
|
+
- **Many machines, one account.** Spread work across machines, with one chat
|
|
103
|
+
and one status page.
|
|
104
|
+
- **Settings follow you.** Optional sync of your agent setup across machines
|
|
105
|
+
through a private GitHub repo.
|
|
106
|
+
|
|
107
|
+
## How it works
|
|
108
|
+
|
|
109
|
+
```mermaid
|
|
110
|
+
flowchart LR
|
|
111
|
+
GH[("GitHub<br/>issues")] -->|new issue| CR["ctrlrelay<br/>on your machine"]
|
|
112
|
+
CR -->|works in a clean branch| AG["Coding agent<br/>Claude Code · OpenCode"]
|
|
113
|
+
AG -->|stuck?| CH["Your chat<br/>Telegram · Mattermost"]
|
|
114
|
+
CH -->|your answer| AG
|
|
115
|
+
AG -->|opens| PR["Pull request"]
|
|
116
|
+
PR -->|CI + review| YOU(["You merge"])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
1. **You assign an issue** to yourself, or add a label you pick.
|
|
120
|
+
2. **ctrlrelay starts the agent** in a fresh, isolated copy of the repo.
|
|
121
|
+
3. **If the agent has a question**, it lands in your chat. Your reply goes
|
|
122
|
+
straight back to the same session.
|
|
123
|
+
4. **A pull request opens.** ctrlrelay waits for CI, fixes what fails, and
|
|
124
|
+
posts a review.
|
|
125
|
+
5. **You merge** — or ask for changes, like with any teammate.
|
|
126
|
+
|
|
127
|
+
## Quick start
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pipx install ctrlrelay
|
|
131
|
+
ctrlrelay setup # finds your repos and writes your config
|
|
132
|
+
ctrlrelay poller start # starts working assigned issues
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
You need Python 3.12+, the GitHub CLI, and a coding agent CLI. The
|
|
136
|
+
**[getting started guide](https://ainvirion.github.io/ctrlrelay/getting-started/)**
|
|
137
|
+
covers prerequisites, chat setup and running it as a background service.
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
Everything lives at **[ainvirion.github.io/ctrlrelay](https://ainvirion.github.io/ctrlrelay/)**:
|
|
142
|
+
|
|
143
|
+
- [Getting started](https://ainvirion.github.io/ctrlrelay/getting-started/) — install and first run
|
|
144
|
+
- [Chat bridge](https://ainvirion.github.io/ctrlrelay/bridge/) — set up Telegram or Mattermost
|
|
145
|
+
- [Configuration](https://ainvirion.github.io/ctrlrelay/configuration/) — every setting
|
|
146
|
+
- [Operations](https://ainvirion.github.io/ctrlrelay/operations/) — background services, status page, several machines
|
|
147
|
+
- [CLI reference](https://ainvirion.github.io/ctrlrelay/cli/) — every command
|
|
148
|
+
|
|
149
|
+
## Contributing
|
|
150
|
+
|
|
151
|
+
Contributions are welcome. Read the [contributing guide](CONTRIBUTING.md) and
|
|
152
|
+
the [code of conduct](CODE_OF_CONDUCT.md). First-time contributors sign a
|
|
153
|
+
one-time Contributor Assignment Agreement in the pull request.
|
|
154
|
+
|
|
155
|
+
## Security
|
|
156
|
+
|
|
157
|
+
Found a vulnerability? Please follow the [security policy](SECURITY.md) and open
|
|
158
|
+
a private advisory — not a public issue.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
163
|
+
|
|
164
|
+
Copyright (c) 2026 AInvirion LLC. All Rights Reserved.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ctrlrelay
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
[](https://pypi.org/project/ctrlrelay/)
|
|
5
|
+
[](https://pypi.org/project/ctrlrelay/)
|
|
6
|
+
[](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml)
|
|
7
|
+
|
|
8
|
+
**Assign an issue. Get a reviewed pull request. Answer questions from your phone.**
|
|
9
|
+
|
|
10
|
+
ctrlrelay turns the coding agent you already use into a teammate that works
|
|
11
|
+
your GitHub backlog in the background. It picks up issues, writes the code in
|
|
12
|
+
a clean branch, opens a pull request, waits for CI, gets the change reviewed,
|
|
13
|
+
and pings you on **Telegram** or **Mattermost** only when it truly needs you.
|
|
14
|
+
|
|
15
|
+
It runs on your own machine. Your code, your credentials, your agent
|
|
16
|
+
subscription. No hosted service in the middle.
|
|
17
|
+
|
|
18
|
+
**[Documentation](https://ainvirion.github.io/ctrlrelay/)** ·
|
|
19
|
+
**[Get started](https://ainvirion.github.io/ctrlrelay/getting-started/)** ·
|
|
20
|
+
**[Why ctrlrelay](https://ainvirion.github.io/ctrlrelay/#why-ctrlrelay)**
|
|
21
|
+
|
|
22
|
+
## Why ctrlrelay
|
|
23
|
+
|
|
24
|
+
Coding agents are great when you sit and watch them. ctrlrelay is for the
|
|
25
|
+
work you don't want to sit and watch.
|
|
26
|
+
|
|
27
|
+
- **Your backlog is the to-do list.** No new tool to learn. Assign an issue
|
|
28
|
+
(or add a label) and the work starts. The result is a normal pull request
|
|
29
|
+
your team already knows how to review.
|
|
30
|
+
- **Runs where your code already lives.** ctrlrelay is a small program on your
|
|
31
|
+
laptop or server — not a cloud service. Nothing leaves your machine except
|
|
32
|
+
the calls your agent and GitHub already make. No per-seat platform fee on
|
|
33
|
+
top of the agent you already pay for.
|
|
34
|
+
- **It asks instead of guessing.** When the agent is stuck, you get the
|
|
35
|
+
question in chat. You reply from your phone and the *same* session picks up
|
|
36
|
+
where it stopped.
|
|
37
|
+
- **Safe by default.** Every issue gets its own isolated copy of the repo.
|
|
38
|
+
One job per repo at a time. Optional Linux sandbox that hides your secrets
|
|
39
|
+
from the agent. Nothing merges without your rules allowing it.
|
|
40
|
+
- **Done means done.** It waits for CI, fixes failing checks, and posts an
|
|
41
|
+
independent review on the pull request — so what reaches you is ready to
|
|
42
|
+
read, not a first draft.
|
|
43
|
+
- **Pick your agent per repo.** Claude Code today, or OpenCode — which can
|
|
44
|
+
run on a local model — for the repos where that fits.
|
|
45
|
+
- **Grows with you.** Start on one laptop. Add more machines later; they share
|
|
46
|
+
the work without stepping on each other.
|
|
47
|
+
- **Open source.** Apache 2.0. Read every line of what runs on your machine.
|
|
48
|
+
|
|
49
|
+
## What it does
|
|
50
|
+
|
|
51
|
+
- **Issues → pull requests.** Watches every repo you choose and works each new
|
|
52
|
+
issue end to end.
|
|
53
|
+
- **Questions in your chat.** Telegram (a private bot) or Mattermost (a channel
|
|
54
|
+
on your own server).
|
|
55
|
+
- **Automatic review.** Every agent pull request gets a second-opinion review
|
|
56
|
+
comment before you look at it.
|
|
57
|
+
- **Daily security sweep.** Reviews Dependabot alerts and pull requests across
|
|
58
|
+
your repos every morning.
|
|
59
|
+
- **Status page.** See what each agent is doing, what is waiting, and what
|
|
60
|
+
needs you — at a glance.
|
|
61
|
+
- **Many machines, one account.** Spread work across machines, with one chat
|
|
62
|
+
and one status page.
|
|
63
|
+
- **Settings follow you.** Optional sync of your agent setup across machines
|
|
64
|
+
through a private GitHub repo.
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
flowchart LR
|
|
70
|
+
GH[("GitHub<br/>issues")] -->|new issue| CR["ctrlrelay<br/>on your machine"]
|
|
71
|
+
CR -->|works in a clean branch| AG["Coding agent<br/>Claude Code · OpenCode"]
|
|
72
|
+
AG -->|stuck?| CH["Your chat<br/>Telegram · Mattermost"]
|
|
73
|
+
CH -->|your answer| AG
|
|
74
|
+
AG -->|opens| PR["Pull request"]
|
|
75
|
+
PR -->|CI + review| YOU(["You merge"])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
1. **You assign an issue** to yourself, or add a label you pick.
|
|
79
|
+
2. **ctrlrelay starts the agent** in a fresh, isolated copy of the repo.
|
|
80
|
+
3. **If the agent has a question**, it lands in your chat. Your reply goes
|
|
81
|
+
straight back to the same session.
|
|
82
|
+
4. **A pull request opens.** ctrlrelay waits for CI, fixes what fails, and
|
|
83
|
+
posts a review.
|
|
84
|
+
5. **You merge** — or ask for changes, like with any teammate.
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pipx install ctrlrelay
|
|
90
|
+
ctrlrelay setup # finds your repos and writes your config
|
|
91
|
+
ctrlrelay poller start # starts working assigned issues
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You need Python 3.12+, the GitHub CLI, and a coding agent CLI. The
|
|
95
|
+
**[getting started guide](https://ainvirion.github.io/ctrlrelay/getting-started/)**
|
|
96
|
+
covers prerequisites, chat setup and running it as a background service.
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
|
|
100
|
+
Everything lives at **[ainvirion.github.io/ctrlrelay](https://ainvirion.github.io/ctrlrelay/)**:
|
|
101
|
+
|
|
102
|
+
- [Getting started](https://ainvirion.github.io/ctrlrelay/getting-started/) — install and first run
|
|
103
|
+
- [Chat bridge](https://ainvirion.github.io/ctrlrelay/bridge/) — set up Telegram or Mattermost
|
|
104
|
+
- [Configuration](https://ainvirion.github.io/ctrlrelay/configuration/) — every setting
|
|
105
|
+
- [Operations](https://ainvirion.github.io/ctrlrelay/operations/) — background services, status page, several machines
|
|
106
|
+
- [CLI reference](https://ainvirion.github.io/ctrlrelay/cli/) — every command
|
|
107
|
+
|
|
108
|
+
## Contributing
|
|
109
|
+
|
|
110
|
+
Contributions are welcome. Read the [contributing guide](CONTRIBUTING.md) and
|
|
111
|
+
the [code of conduct](CODE_OF_CONDUCT.md). First-time contributors sign a
|
|
112
|
+
one-time Contributor Assignment Agreement in the pull request.
|
|
113
|
+
|
|
114
|
+
## Security
|
|
115
|
+
|
|
116
|
+
Found a vulnerability? Please follow the [security policy](SECURITY.md) and open
|
|
117
|
+
a private advisory — not a public issue.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
122
|
+
|
|
123
|
+
Copyright (c) 2026 AInvirion LLC. All Rights Reserved.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
title: ctrlrelay
|
|
2
2
|
description: >-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Assign a GitHub issue, get a reviewed pull request. ctrlrelay runs your
|
|
4
|
+
coding agent in the background and asks you on Telegram or Mattermost
|
|
5
|
+
when it needs you.
|
|
5
6
|
url: "https://ainvirion.github.io"
|
|
6
7
|
baseurl: "/ctrlrelay"
|
|
7
8
|
|
|
@@ -26,6 +27,10 @@ search:
|
|
|
26
27
|
|
|
27
28
|
heading_anchors: true
|
|
28
29
|
|
|
30
|
+
# Render ```mermaid fences as diagrams
|
|
31
|
+
mermaid:
|
|
32
|
+
version: "10.9.1"
|
|
33
|
+
|
|
29
34
|
# Aux links appear in the top right of the layout
|
|
30
35
|
aux_links:
|
|
31
36
|
"GitHub":
|
|
@@ -39,10 +39,12 @@ A token copied between machines is revoked for all of them at once.
|
|
|
39
39
|
|
|
40
40
|
## 3. Its own agent login
|
|
41
41
|
|
|
42
|
-
Log the agent CLI in on this machine
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
Log the agent CLI in on this machine — whichever your repos use (`claude`,
|
|
43
|
+
`opencode` or both, per `agent.type` and `repos[].agent_type`). For Claude Code, each
|
|
44
|
+
machine can use its own subscription; it pauses on its own when its usage
|
|
45
|
+
nears the limit (`usage.pause_at`). Two machines on one subscription share one
|
|
46
|
+
set of usage numbers and pause together, which is correct. OpenCode has no
|
|
47
|
+
usage reading of its own.
|
|
46
48
|
|
|
47
49
|
## 4. A config with its own `node_id`
|
|
48
50
|
|
|
@@ -103,8 +105,9 @@ ctrlrelay node check
|
|
|
103
105
|
✓ peer http://node-a:8787: reachable, node-a
|
|
104
106
|
```
|
|
105
107
|
|
|
106
|
-
`--no-agent` skips the agent check, which makes one tiny call on the
|
|
107
|
-
subscription.
|
|
108
|
+
`--no-agent` skips the agent check, which makes one tiny `claude` call on the
|
|
109
|
+
subscription. The row is skipped when no repo routes to `claude`, and it never
|
|
110
|
+
checks `opencode`: run that CLI once by hand. A peer that answers with this machine's own name means two
|
|
108
111
|
machines share a `node_id` (or the URL points back here).
|
|
109
112
|
|
|
110
113
|
## 8. Start it
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Architecture
|
|
3
|
+
layout: default
|
|
4
|
+
nav_order: 9
|
|
5
|
+
description: "Layer diagram, dispatcher / agent interaction, state-DB shape, and worktree lifecycle for contributors."
|
|
6
|
+
permalink: /architecture/
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Architecture
|
|
10
|
+
|
|
11
|
+
This page is for people working _on_ ctrlrelay, not just _with_ it. For an
|
|
12
|
+
operator-level view of the BLOCKED protocol see
|
|
13
|
+
[Feedback loop]({{ '/feedback-loop/' | relative_url }}).
|
|
14
|
+
|
|
15
|
+
## Layered overview
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TD
|
|
19
|
+
CLI["CLI / daemons<br/>cli.py (Typer app)<br/>bridge/__main__.py (bridge daemon)"]
|
|
20
|
+
PIPE["Pipelines — pipelines/<br/>dev · secops · task · post_merge · base"]
|
|
21
|
+
BRIDGE["Bridge — bridge/<br/>server · protocol · factory · node_tag<br/>telegram_handler · mattermost_handler"]
|
|
22
|
+
CORE["Core — core/<br/>Dispatcher (claude / opencode) · WorktreeManager<br/>GitHub CLI wrapper · Checkpoint · StateDB (SQLite)<br/>Poller · PRWatcher · Config (pydantic)"]
|
|
23
|
+
TRANS["Transports — transports/<br/>SocketTransport (to the bridge)<br/>FileMockTransport (tests)"]
|
|
24
|
+
DASH["Dashboard (optional, push-only) — dashboard/<br/>DashboardClient"]
|
|
25
|
+
CHAT["Chat: Telegram or Mattermost"]
|
|
26
|
+
CLI --> PIPE
|
|
27
|
+
CLI --> BRIDGE
|
|
28
|
+
PIPE --> CORE
|
|
29
|
+
PIPE --> TRANS
|
|
30
|
+
PIPE --> DASH
|
|
31
|
+
TRANS -- "Unix socket" --> BRIDGE
|
|
32
|
+
BRIDGE --> CHAT
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The CLI is a thin shell over async functions in `pipelines/` and `core/`. The
|
|
36
|
+
bridge is a separate daemon that pipelines reach over a Unix socket — it is
|
|
37
|
+
deliberately decoupled so it can be restarted without affecting in-flight
|
|
38
|
+
pipeline work.
|
|
39
|
+
|
|
40
|
+
## Dispatcher ↔ agent interaction
|
|
41
|
+
|
|
42
|
+
[`src/ctrlrelay/core/dispatcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/dispatcher.py)
|
|
43
|
+
is the only place ctrlrelay calls out to a coding agent. It has two adapters
|
|
44
|
+
with the same async `spawn_session` shape, so pipelines stay agent-agnostic:
|
|
45
|
+
`ClaudeDispatcher` (Claude Code) and `OpenCodeDispatcher` (OpenCode).
|
|
46
|
+
`make_repo_agent_dispatcher` picks one per repo: `agent.type` is the default,
|
|
47
|
+
`repos[].agent_type` overrides it for a single repo.
|
|
48
|
+
|
|
49
|
+
- **Spawn shape:** Claude Code runs
|
|
50
|
+
`claude -p <prompt> --output-format stream-json --verbose --dangerously-skip-permissions [--resume <id>]`;
|
|
51
|
+
OpenCode runs `opencode run ... --format json --dangerously-skip-permissions [--session <id>]`.
|
|
52
|
+
- **Working directory:** the session's worktree (so file edits land on the
|
|
53
|
+
right branch).
|
|
54
|
+
- **Environment additions:** `CTRLRELAY_SESSION_ID` and `CTRLRELAY_STATE_FILE`
|
|
55
|
+
are the only contract between dispatcher and child. Anything else the agent
|
|
56
|
+
needs (issue body, PR rules, etc.) goes into the prompt.
|
|
57
|
+
- **Credentials (#219):** every spawn says whether it gets GitHub access
|
|
58
|
+
(`spawn_session(..., github_access=...)`, no default). dev and task spawn
|
|
59
|
+
without it: GitHub tokens, `gh`'s login, git credential helpers, SSH and
|
|
60
|
+
the desktop keyring are cut off (`core/agent_env.py`), and ctrlrelay
|
|
61
|
+
pushes the branch, opens the PR or posts the report itself afterwards
|
|
62
|
+
(`core/publish.py`, `WorktreeManager.push_run_branch`). secops still
|
|
63
|
+
spawns with access. See
|
|
64
|
+
[What the agent can reach](configuration.md#what-the-agent-can-reach-219).
|
|
65
|
+
- **Result:** `SessionResult(session_id, exit_code, state, stdout, stderr,
|
|
66
|
+
agent_session_id)`. `session_id` is our composite orchestrator id;
|
|
67
|
+
`agent_session_id` is the agent's own session id, parsed from its JSON
|
|
68
|
+
stdout and the value its resume flag actually expects. `state` is the parsed
|
|
69
|
+
`CheckpointState` if a state file was written, else `None`. The wrapper
|
|
70
|
+
exposes `.success`, `.blocked`, `.failed` properties for pattern-matching
|
|
71
|
+
pipelines.
|
|
72
|
+
- **Resume:** `resume_session_id` translates to `--resume <id>` (Claude
|
|
73
|
+
Code) or `--session <id>` (OpenCode). Pipelines look up the agent's own id
|
|
74
|
+
(persisted as `sessions.agent_session_id` by the prior spawn) and pass it
|
|
75
|
+
through — newer `claude` CLI builds reject non-UUID values. If state_db
|
|
76
|
+
has no id (session predates capture or stdout wasn't JSON), the spawn falls back to a fresh session rather than
|
|
77
|
+
hard-failing.
|
|
78
|
+
|
|
79
|
+
The dispatcher does NOT proxy or wrap the agent session — it shells out the
|
|
80
|
+
same way you'd shell out to `gh` or `git`. This is deliberate.
|
|
81
|
+
|
|
82
|
+
## State DB shape
|
|
83
|
+
|
|
84
|
+
Schema in
|
|
85
|
+
[`src/ctrlrelay/core/state.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/state.py).
|
|
86
|
+
SQLite, single file at `paths.state_db`.
|
|
87
|
+
|
|
88
|
+
| Table | Columns | Notes |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `sessions` | `id`, `pipeline`, `repo`, `issue_number`, `worktree_path`, `status`, `blocked_question`, `started_at`, `ended_at`, `claude_exit_code`, `summary`, `agent_session_id`, `output_tail` | Indexed on `repo` and `status`. One row per pipeline run. `agent_session_id` stores the agent's own session id for resume; `claude_exit_code` holds the worker's exit code whichever backend ran; `output_tail` is a scrubbed, capped tail of its output (#204). |
|
|
91
|
+
| `repo_locks` | `repo` (PK), `session_id`, `acquired_at` | Acquired with `acquire_lock`, released in the pipeline's `finally`. |
|
|
92
|
+
| `github_cursor` | `repo`, `last_checked_at`, `last_seen_issue_update` | Bounds GitHub API calls in the poller. |
|
|
93
|
+
| `pending_resumes` | `session_id` (PK), `pipeline`, `repo`, `question`, `created_at`, `answer`, `answered_at`, `resumed_at`, `expired_at`, `expired_reason`, `held_until`, `posted`, `last_answered_at` | One row per BLOCKED session awaiting an answer, whichever chat it was asked on. An answer arriving after the in-session wait lands here and the poller's sweeper drives the resume. |
|
|
94
|
+
| `pr_watches` | `repo`, `pr_number` (PK pair), `session_id`, `issue_number`, `pr_url`, `started_at`, `cleanup_phase` | In-flight post-merge watchers, rehydrated on poller start. |
|
|
95
|
+
| `automation_decisions` | `id`, `repo`, `operation`, `policy`, `item_id`, `decision`, `decided_by`, `decided_at`, `context` | Audit trail for `ask`-policy decisions. |
|
|
96
|
+
| `dispatch_health` | single row: `consecutive_failures`, `alerted_at`, `alert_claim_token`, `alert_delivered_at`, `last_failure_at`, `last_success_at` | Whether the relay can still run a worker at all (#204). |
|
|
97
|
+
| `relay_pushes` | `id`, `repo`, `branch`, `session_id`, `sha`, `pushed_at` | Every branch head ctrlrelay pushed for an agent (#219); the push lease is taken from here. |
|
|
98
|
+
| `events` | `id`, `ts`, `kind`, `repo`, `issue_number`, `session_id`, `detail` | Feed for the status page, written best-effort. |
|
|
99
|
+
| `telegram_pending` | `request_id`, `session_id`, `question`, `asked_at`, `answered_at`, `answer` | Legacy: still created by the schema, but no code reads or writes it. Outstanding questions live in `pending_resumes`. |
|
|
100
|
+
|
|
101
|
+
Methods on `StateDB`: `acquire_lock`, `release_lock`, `get_lock_holder`,
|
|
102
|
+
`list_locks`, `set_agent_session_id`, `get_agent_session_id`, plus raw
|
|
103
|
+
`execute` / `commit`. The pipeline code uses raw SQL for the `sessions`
|
|
104
|
+
table on purpose — kept thin and grep-able.
|
|
105
|
+
|
|
106
|
+
## Worktree lifecycle
|
|
107
|
+
|
|
108
|
+
`WorktreeManager` (in
|
|
109
|
+
[`src/ctrlrelay/core/worktree.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/worktree.py))
|
|
110
|
+
is a thin wrapper over `git worktree`. Conventions:
|
|
111
|
+
|
|
112
|
+
- **Bare repo path:** `<paths.bare_repos>/<owner>-<repo>.git`. Cloned on first
|
|
113
|
+
use, fetched on each subsequent use.
|
|
114
|
+
- **Worktree path:** `<paths.worktrees>/<owner>-<repo>-<session_id>`.
|
|
115
|
+
- **Branch:** for the dev pipeline, derived from `dev_branch_template` with
|
|
116
|
+
`{n}` → issue number. Default `fix/issue-{n}`.
|
|
117
|
+
|
|
118
|
+
Lifecycle in `run_dev_issue`:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
ensure_bare_repo(repo)
|
|
122
|
+
branch_preexisted = await branch_exists_locally(repo, branch)
|
|
123
|
+
worktree_path = create_worktree_with_new_branch(repo, session_id, branch)
|
|
124
|
+
... agent session runs ...
|
|
125
|
+
on DONE: remove_worktree (keep branch — open PR refs it)
|
|
126
|
+
on BLOCKED: keep both (operator may resume)
|
|
127
|
+
on FAILED: remove_worktree
|
|
128
|
+
if not branch_preexisted and not on remote: delete_branch
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The "preexisted" guard prevents a retry from clobbering a branch that already
|
|
132
|
+
holds an open PR from a previous successful session.
|
|
133
|
+
|
|
134
|
+
## PR verifier and post-merge
|
|
135
|
+
|
|
136
|
+
After a `DONE`-with-`pr_url` checkpoint, the dev pipeline runs
|
|
137
|
+
[`PRVerifier`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/pr_verifier.py)
|
|
138
|
+
to confirm the PR is mergeable and CI is green. If not, it builds a fix prompt
|
|
139
|
+
(merge conflicts → "rebase and resolve", failing checks → "investigate and
|
|
140
|
+
fix"), resumes the same agent session with that prompt, and re-verifies. Up
|
|
141
|
+
to `DEFAULT_MAX_FIX_ATTEMPTS` (3) attempts.
|
|
142
|
+
|
|
143
|
+
`PRWatcher` (in
|
|
144
|
+
[`src/ctrlrelay/core/pr_watcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/pr_watcher.py))
|
|
145
|
+
polls a PR for merge status; the post-merge handler in
|
|
146
|
+
[`src/ctrlrelay/pipelines/post_merge.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/pipelines/post_merge.py)
|
|
147
|
+
closes the originating issue and notifies the operator once the PR lands.
|
|
148
|
+
The poller starts one `pr_watch_task` per PR the dev pipeline hands off, and
|
|
149
|
+
persists it in `pr_watches` so a poller restart resumes the watch.
|
|
150
|
+
|
|
151
|
+
## Bridge architecture
|
|
152
|
+
|
|
153
|
+
The bridge daemon (`ctrlrelay/bridge/server.py`) listens on a Unix socket
|
|
154
|
+
(mode `0o600` — owner-only). It speaks newline-delimited JSON, defined in
|
|
155
|
+
`bridge/protocol.py`:
|
|
156
|
+
|
|
157
|
+
The bridge builds exactly one chat handler from `transport.type`
|
|
158
|
+
(`bridge/factory.py`): `TelegramHandler` or `MattermostHandler`. The server
|
|
159
|
+
knows only the handler interface, so the flow is the same for both:
|
|
160
|
+
|
|
161
|
+
```mermaid
|
|
162
|
+
sequenceDiagram
|
|
163
|
+
participant Cl as Client (pipeline)
|
|
164
|
+
participant S as Bridge server
|
|
165
|
+
participant H as Chat handler
|
|
166
|
+
participant Ch as Telegram or Mattermost
|
|
167
|
+
Cl->>S: ask(q)
|
|
168
|
+
S->>H: ask(q, options)
|
|
169
|
+
H->>Ch: post (sendMessage / POST /api/v4/posts)
|
|
170
|
+
Note over S,H: server records post id → session
|
|
171
|
+
Ch-->>H: reply (getUpdates long-poll / WebSocket posted event)
|
|
172
|
+
H-->>S: on_reply(text, reply_to_post_id)
|
|
173
|
+
S-->>Cl: answer(text)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The handler reports the post the reply points at — Telegram's
|
|
177
|
+
`reply_to_message_id`, or Mattermost's thread `root_id`. Matching reply →
|
|
178
|
+
pending question:
|
|
179
|
+
|
|
180
|
+
1. The reply points at a live pending question — route to that question.
|
|
181
|
+
This is the only route that is right when several are outstanding.
|
|
182
|
+
2. The reply points at a question that is no longer live (its ask timed
|
|
183
|
+
out, or the bridge restarted) — hand it to the orphan router, which
|
|
184
|
+
attaches the answer to that session's `pending_resumes` row.
|
|
185
|
+
3. A fresh message with exactly one question outstanding — route there.
|
|
186
|
+
4. A fresh message with several outstanding — refuse, and tell the operator
|
|
187
|
+
to reply to the question they mean. There is no FIFO fallback.
|
|
188
|
+
|
|
189
|
+
When several machines share one Mattermost bot and channel
|
|
190
|
+
(`transport.mattermost.shared: true`), each machine prefixes its posts with
|
|
191
|
+
`[<node>]` and ignores replies to other machines' posts (`bridge/node_tag.py`).
|
|
192
|
+
|
|
193
|
+
Multiple in-flight asks are supported. The bridge retains the original
|
|
194
|
+
socket for each ask so the answer routes back to the correct caller.
|
|
195
|
+
|
|
196
|
+
Bridge restart safety: pending in-memory state is lost on bridge restart.
|
|
197
|
+
Pipelines waiting on `transport.ask` stop waiting and persist the session as
|
|
198
|
+
`BLOCKED` with a `pending_resumes` row; a later answer still drives the
|
|
199
|
+
resume through the sweeper. Restart the bridge **before** restarting the
|
|
200
|
+
poller to minimise this window.
|
|
201
|
+
|
|
202
|
+
## Why this shape
|
|
203
|
+
|
|
204
|
+
A few decisions worth flagging:
|
|
205
|
+
|
|
206
|
+
- **Local-first, file-based protocol.** Everything between ctrlrelay and the
|
|
207
|
+
agent is a file under the worktree. No long-lived shared memory, no socket
|
|
208
|
+
contract with the agent. Survives orchestrator restart trivially.
|
|
209
|
+
- **Subprocess-not-library.** `claude` or `opencode` is invoked as a
|
|
210
|
+
subprocess. ctrlrelay has no Python coupling to either agent's internals — it exchanges a prompt
|
|
211
|
+
in and a JSON file out. Versioning is decoupled.
|
|
212
|
+
- **Per-repo serial, parallel across repos.** Repo locks in SQLite enforce
|
|
213
|
+
this. A failed lock acquisition fails the session immediately rather than
|
|
214
|
+
queueing.
|
|
215
|
+
- **No background event loop.** Each CLI command spins up its own
|
|
216
|
+
`asyncio.run(...)`. The poller and bridge each own one event loop while they
|
|
217
|
+
run. Simpler to reason about than a long-lived shared loop.
|
|
218
|
+
- **Bridge as separate process.** Restartable independent of pipelines. The
|
|
219
|
+
cost is the small in-memory pending-asks state that does not survive a
|
|
220
|
+
bridge restart.
|
|
221
|
+
|
|
222
|
+
## Where to read next
|
|
223
|
+
|
|
224
|
+
- [`src/ctrlrelay/core/config.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/config.py)
|
|
225
|
+
— pydantic schema, the source of truth for the YAML.
|
|
226
|
+
- [`src/ctrlrelay/pipelines/dev.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/pipelines/dev.py)
|
|
227
|
+
— full dev-pipeline orchestration including BLOCKED loop and PR-fix loop.
|
|
228
|
+
- [`src/ctrlrelay/core/dispatcher.py`](https://github.com/AInvirion/ctrlrelay/blob/main/src/ctrlrelay/core/dispatcher.py)
|
|
229
|
+
— the only place we call `claude` or `opencode`.
|
|
230
|
+
- [`tests/`](https://github.com/AInvirion/ctrlrelay/tree/main/tests) — covers
|
|
231
|
+
every public surface; treat tests as executable specifications.
|