ctrlrelay 0.14.0__tar.gz → 0.16.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.14.0 → ctrlrelay-0.16.0}/CHANGELOG.md +164 -1
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/PKG-INFO +1 -1
- ctrlrelay-0.16.0/docs/adding-a-machine.md +120 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/architecture.md +8 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/configuration.md +271 -7
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/feedback-loop.md +6 -3
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/operations.md +251 -4
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +20 -2
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/pyproject.toml +1 -1
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/__main__.py +8 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/mattermost_handler.py +13 -0
- ctrlrelay-0.16.0/src/ctrlrelay/bridge/node_tag.py +50 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/server.py +110 -4
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/telegram_handler.py +52 -1
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/cli.py +492 -4
- ctrlrelay-0.16.0/src/ctrlrelay/core/agent_env.py +528 -0
- ctrlrelay-0.16.0/src/ctrlrelay/core/claims.py +628 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/config.py +161 -1
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/dispatcher.py +301 -19
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/github.py +199 -6
- ctrlrelay-0.16.0/src/ctrlrelay/core/github_budget.py +416 -0
- ctrlrelay-0.16.0/src/ctrlrelay/core/heartbeat.py +99 -0
- ctrlrelay-0.16.0/src/ctrlrelay/core/job_claims.py +154 -0
- ctrlrelay-0.16.0/src/ctrlrelay/core/node_check.py +217 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/poller.py +112 -3
- ctrlrelay-0.16.0/src/ctrlrelay/core/publish.py +189 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/scheduler.py +57 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/state.py +45 -0
- ctrlrelay-0.16.0/src/ctrlrelay/core/usage.py +465 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/worktree.py +439 -26
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/dev.py +506 -59
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/secops.py +46 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/task.py +198 -28
- ctrlrelay-0.16.0/src/ctrlrelay/web/fleet.py +515 -0
- ctrlrelay-0.16.0/src/ctrlrelay/web/health.py +167 -0
- ctrlrelay-0.16.0/src/ctrlrelay/web/links.py +35 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/server.py +88 -4
- ctrlrelay-0.16.0/src/ctrlrelay/web/static/index.html +964 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/status.py +212 -34
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/transcript.py +49 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/waiting.py +118 -14
- ctrlrelay-0.16.0/tests/_publish_fakes.py +25 -0
- ctrlrelay-0.16.0/tests/fixtures/stream_json_rate_limit.jsonl +4 -0
- ctrlrelay-0.16.0/tests/test_agent_no_keys.py +1216 -0
- ctrlrelay-0.16.0/tests/test_agent_sandbox.py +500 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_bridge_server.py +38 -16
- ctrlrelay-0.16.0/tests/test_chat_machines.py +407 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_claim_reconciliation.py +65 -1
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_config.py +2 -2
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dev_integration.py +27 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dev_pipeline.py +77 -70
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dispatch_outcome_hook.py +45 -30
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dispatcher.py +57 -38
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_event_emission.py +29 -0
- ctrlrelay-0.16.0/tests/test_fleet.py +549 -0
- ctrlrelay-0.16.0/tests/test_github_budget.py +584 -0
- ctrlrelay-0.16.0/tests/test_job_claims.py +385 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_mattermost_handler.py +1 -1
- ctrlrelay-0.16.0/tests/test_node_check.py +394 -0
- ctrlrelay-0.16.0/tests/test_node_claims.py +1031 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_obs.py +4 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_pipeline_base.py +1 -0
- ctrlrelay-0.16.0/tests/test_pre_dispatch_retry.py +257 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_question_backoff.py +2 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_relay_alert_off_lock.py +3 -2
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_resume_prompt_contract.py +14 -4
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_secops_integration.py +2 -1
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_secops_pipeline.py +21 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_task_pipeline.py +12 -0
- ctrlrelay-0.16.0/tests/test_usage.py +627 -0
- ctrlrelay-0.16.0/tests/test_web_health.py +228 -0
- ctrlrelay-0.16.0/tests/test_web_links.py +60 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_server.py +287 -2
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_status.py +415 -9
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_transcript.py +96 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_web_waiting.py +266 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_worktree.py +379 -41
- ctrlrelay-0.14.0/src/ctrlrelay/web/static/index.html +0 -477
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/.gitignore +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/LICENSE +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/README.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/SECURITY.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/config/orchestrator.yaml.example +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/_config.yml +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/bridge.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/cli.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/development.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/getting-started.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/index.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/personalization.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/factory.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/handler.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/archived.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/binaries.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/code_review.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/events.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/network.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/question_expiry.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/core/redact.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/src/ctrlrelay/web/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/fixtures/gh_search_issues.json +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/fixtures/transcript_sample.jsonl +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_archived.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_binary_resolution.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_code_review.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_dispatch_health.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_events.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_install.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_issue_outcome_announcement.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_network.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_notify_transport.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_poller.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_question_expiry.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_redact.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_setup.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_transport_resolution.py +0 -0
- {ctrlrelay-0.14.0 → ctrlrelay-0.16.0}/tests/test_unreviewed_pr_announcement.py +0 -0
|
@@ -7,6 +7,167 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
Several machines can now run ctrlrelay on one GitHub account, each claiming
|
|
13
|
+
an issue before it works it, and the dev and task agents can run in a
|
|
14
|
+
read-only sandbox. Every new config value has a default and nothing is added
|
|
15
|
+
to the database, so for most installs **no config edit is required**. Check
|
|
16
|
+
three things before upgrading:
|
|
17
|
+
|
|
18
|
+
- **A placeholder `node_id` is refused at startup**: `localhost`,
|
|
19
|
+
`localhost.localdomain`, `unknown`, `my-laptop`, `studio-mac` (both were
|
|
20
|
+
examples in earlier docs), `changeme`, `example`, `node`, `hostname`. Set a
|
|
21
|
+
name no other machine on the account uses. A missing `node_id` still
|
|
22
|
+
defaults to the hostname.
|
|
23
|
+
- **If you already run `agent.sandbox: bwrap`**, it is now much stricter:
|
|
24
|
+
by default the agent can write its worktree, its git dirs, a private
|
|
25
|
+
`/tmp`, its own empty `gh` and runtime dirs, the entries of `~/.claude`,
|
|
26
|
+
its own cache and any `sandbox_writable_paths`, minus the read-only and
|
|
27
|
+
hidden paths inside those. Anything else a run wrote
|
|
28
|
+
(a cache in your home, `git config`, submodules, commit signing) fails
|
|
29
|
+
there. It also refuses to start with `CLAUDE_CONFIG_DIR` set or with a
|
|
30
|
+
read-only path that is a replaceable link. Read the sandbox section of the
|
|
31
|
+
configuration docs before upgrading such a machine.
|
|
32
|
+
- **Upgrade every machine on an account to the same version.** Claims now
|
|
33
|
+
name the machine and the process that wrote them, and an older version
|
|
34
|
+
reads them differently. A claim comment left by an older version counts
|
|
35
|
+
as taken by `unknown` and never goes stale: once nothing is working that
|
|
36
|
+
issue, delete the comment to free it. With a shared Mattermost bot, a
|
|
37
|
+
question asked before the upgrade carries no machine tag: answer it with
|
|
38
|
+
`node-a: ...`.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Several machines on one account: before a dev or task run, the machine
|
|
43
|
+
claims the issue with a comment naming itself, waits
|
|
44
|
+
`claims.race_wait_seconds` and backs off if another machine's claim came
|
|
45
|
+
first; claims are refreshed while the run lasts and taken over once stale
|
|
46
|
+
(`claims.refresh_seconds`, `claims.stale_after_seconds`). A run whose
|
|
47
|
+
machine cannot refresh its claim for longer than the stale time (a long
|
|
48
|
+
GitHub outage) can be taken over while it still runs, so keep the
|
|
49
|
+
machines' clocks synchronised and the stale time well above any outage
|
|
50
|
+
you expect. A run that fails after being resumed from a question leaves
|
|
51
|
+
its claim held; delete the claim comment to free that issue (#252,
|
|
52
|
+
closes #245)
|
|
53
|
+
- The scheduled security sweep runs at most once per repository and
|
|
54
|
+
scheduled time across machines: the first to create the ref
|
|
55
|
+
`refs/ctrlrelay/claims/secops/<window>` in the repository runs it. A
|
|
56
|
+
machine that dies holding a window, or a ref that cannot be created,
|
|
57
|
+
leaves that repository unswept for that window; the next window is new.
|
|
58
|
+
Keep `repos`, `schedules` and `timezone` the same on every machine, or
|
|
59
|
+
they name different windows (#259, closes #246)
|
|
60
|
+
- Chat with several machines: questions are tagged with the machine's name
|
|
61
|
+
and a machine ignores replies to another's. Mattermost machines can share
|
|
62
|
+
one bot and channel with `transport.mattermost.shared: true` (a message
|
|
63
|
+
that is not a reply must start with `node-b: ...`); Telegram needs one bot
|
|
64
|
+
per machine. When a second process starts reading the same bot, Telegram
|
|
65
|
+
cuts off the one already running, which now says so once, in its log and
|
|
66
|
+
in the chat, and stops reading (#260, closes #248)
|
|
67
|
+
- Subscription usage: read from each agent run and shown on the status
|
|
68
|
+
page. New issues wait while the 5-hour or 7-day window is at or above
|
|
69
|
+
`usage.pause_at` (default 90%), or the status is anything but `allowed`,
|
|
70
|
+
until the window's reset (or an hour, when the reset time cannot be read,
|
|
71
|
+
and then the next reading decides); runs already going finish, and a usage that
|
|
72
|
+
could not be read never pauses. An idle probe every `usage.probe_minutes`
|
|
73
|
+
keeps the reading fresh (#254, closes #247)
|
|
74
|
+
- GitHub API budget: watched every `github_api.check_minutes`; polling slows
|
|
75
|
+
by `github_api.slow_factor` while the `core` or `graphql` bucket is below
|
|
76
|
+
`github_api.low_at` (not `search`, which resets every minute), with one
|
|
77
|
+
chat line. When a bucket is used up, calls known to spend it
|
|
78
|
+
(`gh api`, `gh search`) fail at once as rate limited until its reset, and
|
|
79
|
+
the poller resumes after it; other `gh` commands are not held back (#256,
|
|
80
|
+
closes #237)
|
|
81
|
+
- Status page: a Machines strip and who-has-what for every machine listed in
|
|
82
|
+
`fleet.peers` (each machine's `web.allowed_hosts` must hold the name the
|
|
83
|
+
others use for it), plus this machine's usage and API budget (#258,
|
|
84
|
+
closes #249)
|
|
85
|
+
- `ctrlrelay node check [--no-agent]`: node id, GitHub login, agent login
|
|
86
|
+
and peers, exit 1 on any failure; the poller warns at startup and while
|
|
87
|
+
running if another process uses this machine's `node_id`; new guide
|
|
88
|
+
`docs/adding-a-machine.md` (#261, closes #250)
|
|
89
|
+
- `agent.sandbox: bwrap` is now a read-only copy of the machine: by default
|
|
90
|
+
only the run's worktree, its git dirs, a private `/tmp`, the entries of
|
|
91
|
+
`~/.claude` and an agent-only cache (`agent.sandbox_cache_dir`) are
|
|
92
|
+
writable; the
|
|
93
|
+
agent CLI's settings, hooks, plugins, skills and project memory, and the
|
|
94
|
+
repository's git config and hooks, stay read-only; more credentials and
|
|
95
|
+
the session's sockets are hidden; `~/.claude.json` is a private copy per
|
|
96
|
+
run. New `agent.sandbox_writable_paths` and `agent.sandbox_readonly_paths`.
|
|
97
|
+
Still off by default. The configuration docs list what it costs a run
|
|
98
|
+
(no git config changes, no commit signing, no submodule updates) and how
|
|
99
|
+
to set it up (#262, closes #236)
|
|
100
|
+
- Status page: PR and issue numbers in Needs you questions are links (#242,
|
|
101
|
+
closes #241)
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- The agent runs with `--output-format stream-json` (the only output that
|
|
106
|
+
carries the usage reading); `agent.output_format` is read and ignored
|
|
107
|
+
(#254)
|
|
108
|
+
- A repository whose history was replaced upstream is re-cloned, unless a
|
|
109
|
+
worktree still uses the clone (finish or clean up the run first); an issue
|
|
110
|
+
that failed before the agent started is retried on the next polls, up to
|
|
111
|
+
three tries, then left as seen like any other failed run, with a chat
|
|
112
|
+
line saying so (#244, closes #234)
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- A dev run whose PR went BEHIND mid-run no longer dies fetching its own
|
|
117
|
+
checked-out branch, and a late failure names the PR it opened (#255,
|
|
118
|
+
closes #253)
|
|
119
|
+
|
|
120
|
+
### Known limits
|
|
121
|
+
|
|
122
|
+
- With several machines, chat and status-page names are only as unique as
|
|
123
|
+
each machine's `node_id`; the duplicate-name warning does not catch every
|
|
124
|
+
case
|
|
125
|
+
- The relay's own git calls in a worktree after the agent exits still read
|
|
126
|
+
that repository's config; only the push ignores it (#263)
|
|
127
|
+
|
|
128
|
+
## [0.15.0] - 2026-10-31
|
|
129
|
+
|
|
130
|
+
The dev and task agents no longer inherit the usual GitHub credentials;
|
|
131
|
+
ctrlrelay does the pushing, PR opening and commenting itself
|
|
132
|
+
after the agent exits. One new
|
|
133
|
+
database table is added automatically and the new config values have
|
|
134
|
+
defaults, so **no config edit is required** to upgrade.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- Dev and task agents run with inherited `GH_*` and `GITHUB_*` variables
|
|
139
|
+
and gh's login removed from their environment, git credential helpers and
|
|
140
|
+
git's SSH disabled, and the D-Bus and runtime-dir variables pointed away
|
|
141
|
+
from the desktop keyring. After the agent exits the
|
|
142
|
+
relay pushes the run branch from a relay-private git dir to an explicit
|
|
143
|
+
GitHub URL, opens the PR from the DONE checkpoint (or reuses an open PR on the run
|
|
144
|
+
branch), pushes fix rounds,
|
|
145
|
+
fetches failed CI logs and posts task reports. It refuses to push the
|
|
146
|
+
default branch and only overwrites a remote branch whose head it recorded
|
|
147
|
+
pushing. secops keeps GitHub access (#235, closes #219)
|
|
148
|
+
- Status page: the phase bar shows the furthest phase seen in the recent
|
|
149
|
+
transcript and pipeline events, CI waits are
|
|
150
|
+
shown as waiting rather than reading, and the PR is linked (#239, closes
|
|
151
|
+
#238)
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
|
|
155
|
+
- Optional `agent.sandbox: bwrap` with `agent.sandbox_hide_paths` to run the
|
|
156
|
+
dev and task agents under bubblewrap with credential paths hidden. Off by
|
|
157
|
+
default (#235)
|
|
158
|
+
- A `relay_pushes` table recording the heads ctrlrelay pushed (#235)
|
|
159
|
+
- Status page: a live counter and dot, server health, GitHub links, and the
|
|
160
|
+
list of skipped issues (#233, closes #232)
|
|
161
|
+
|
|
162
|
+
### Known limits
|
|
163
|
+
|
|
164
|
+
- The agent still runs as the same OS user. Without the bwrap sandbox it
|
|
165
|
+
can read credential files on disk; with it, only the paths in
|
|
166
|
+
`agent.sandbox_hide_paths` are hidden. It can still run ssh directly, reach
|
|
167
|
+
the Linux keyring by resetting the D-Bus address, or read a token kept
|
|
168
|
+
under some other variable name, and the macOS keychain is not covered. Running the agent as a separate user
|
|
169
|
+
is tracked in #236
|
|
170
|
+
|
|
10
171
|
## [0.14.0] - 2026-10-31
|
|
11
172
|
|
|
12
173
|
A read-only status page. `state.db` switches to WAL mode and gains an
|
|
@@ -1796,7 +1957,9 @@ pipeline).
|
|
|
1796
1957
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
1797
1958
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
1798
1959
|
|
|
1799
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.
|
|
1960
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.16.0...HEAD
|
|
1961
|
+
[0.16.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.15.0...v0.16.0
|
|
1962
|
+
[0.15.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.14.0...v0.15.0
|
|
1800
1963
|
[0.14.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.13.0...v0.14.0
|
|
1801
1964
|
[0.13.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.4...v0.13.0
|
|
1802
1965
|
[0.12.4]: https://github.com/AInvirion/ctrlrelay/compare/v0.12.2...v0.12.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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/
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Adding a machine
|
|
3
|
+
layout: default
|
|
4
|
+
nav_order: 11
|
|
5
|
+
description: "Run ctrlrelay on a second machine, on the same GitHub account and repositories, without the two taking the same work."
|
|
6
|
+
permalink: /adding-a-machine/
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Adding a machine
|
|
10
|
+
|
|
11
|
+
Several machines can run ctrlrelay on the same GitHub account and the same
|
|
12
|
+
repositories. Each one takes issues the others have not claimed, each
|
|
13
|
+
scheduled sweep runs once across them, and any machine's status page shows
|
|
14
|
+
all of them. This page sets up a second machine, `node-b`, next to a running
|
|
15
|
+
`node-a`.
|
|
16
|
+
|
|
17
|
+
Read [Operations: several machines on one account]({{ '/operations/' | relative_url }}#several-machines-on-one-account)
|
|
18
|
+
for how the claims work.
|
|
19
|
+
|
|
20
|
+
## 1. Install
|
|
21
|
+
|
|
22
|
+
Install ctrlrelay the same way as on the first machine
|
|
23
|
+
([Getting started]({{ '/getting-started/' | relative_url }})), the **same
|
|
24
|
+
version** on every machine. A machine on a version from before the
|
|
25
|
+
multi-machine work reads every failure comment as freeing every claim and
|
|
26
|
+
posts claims with no machine name.
|
|
27
|
+
|
|
28
|
+
## 2. Its own GitHub login
|
|
29
|
+
|
|
30
|
+
Log `gh` in on this machine with its own token:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gh auth login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use the same GitHub **account** as the other machines (claims only count when
|
|
37
|
+
they come from the account the relay runs as), but a **token of its own**.
|
|
38
|
+
A token copied between machines is revoked for all of them at once.
|
|
39
|
+
|
|
40
|
+
## 3. Its own agent login
|
|
41
|
+
|
|
42
|
+
Log the agent CLI in on this machine. Each machine can use its own
|
|
43
|
+
subscription; it pauses on its own when its usage nears the limit
|
|
44
|
+
(`usage.pause_at`). Two machines on one subscription share one set of usage
|
|
45
|
+
numbers and pause together, which is correct.
|
|
46
|
+
|
|
47
|
+
## 4. A config with its own `node_id`
|
|
48
|
+
|
|
49
|
+
Copy `orchestrator.yaml` from the first machine and change:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
node_id: "node-b" # a name no other machine on this account uses
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The `node_id` is on every claim, every chat message and the status page, so
|
|
56
|
+
two machines with the same one look like one. If you leave it out, the
|
|
57
|
+
hostname is used, which is fine when hostnames differ. ctrlrelay refuses to
|
|
58
|
+
start with a placeholder such as `my-laptop`, `localhost` or `unknown`.
|
|
59
|
+
|
|
60
|
+
Keep `repos`, `schedules` and `timezone` the same on every machine: the
|
|
61
|
+
scheduled sweep is split by its scheduled time, so machines must agree on the
|
|
62
|
+
schedule.
|
|
63
|
+
|
|
64
|
+
## 5. Peers, for the status page
|
|
65
|
+
|
|
66
|
+
To see every machine on any machine's page, list the others' status pages:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# on node-b
|
|
70
|
+
web:
|
|
71
|
+
host: "192.168.1.21" # an address node-a can reach
|
|
72
|
+
allowed_hosts: ["node-b"] # the name node-a uses for this machine
|
|
73
|
+
fleet:
|
|
74
|
+
peers: ["http://node-a:8787"]
|
|
75
|
+
account_label: "max-2" # optional: which subscription, never an email
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
and the mirror image on `node-a`. A peer refuses a name it does not know
|
|
79
|
+
(HTTP 421, shown as "bad host header"): each machine's `web.allowed_hosts`
|
|
80
|
+
must hold the name the others use for it.
|
|
81
|
+
|
|
82
|
+
## 6. Chat
|
|
83
|
+
|
|
84
|
+
- **Mattermost:** machines can share one bot and channel. Set
|
|
85
|
+
`transport.mattermost.shared: true` on every machine. Answer a question by
|
|
86
|
+
replying to it; a message that is not a reply must start with the machine's
|
|
87
|
+
name, `node-b: ...`.
|
|
88
|
+
- **Telegram:** each machine needs **its own bot** (create one with
|
|
89
|
+
@BotFather). Telegram lets one process read a bot at a time.
|
|
90
|
+
|
|
91
|
+
See [Operations: chat with several machines]({{ '/operations/' | relative_url }}#chat-with-several-machines).
|
|
92
|
+
|
|
93
|
+
## 7. Check it
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
ctrlrelay node check
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
✓ node_id: node-b (named on claims, chat and the page)
|
|
101
|
+
✓ GitHub login: <the account>
|
|
102
|
+
✓ agent login: works; 12% of the 5-hour window
|
|
103
|
+
✓ peer http://node-a:8787: reachable, node-a
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--no-agent` skips the agent check, which makes one tiny call on the
|
|
107
|
+
subscription. A peer that answers with this machine's own name means two
|
|
108
|
+
machines share a `node_id` (or the URL points back here).
|
|
109
|
+
|
|
110
|
+
## 8. Start it
|
|
111
|
+
|
|
112
|
+
Start the bridge, the poller and the status page as on the first machine
|
|
113
|
+
([Operations]({{ '/operations/' | relative_url }})). At startup, and while it
|
|
114
|
+
runs, the poller warns, in its log and in chat, if another process uses its
|
|
115
|
+
name: at startup, a peer whose status page answers with its name; while it
|
|
116
|
+
runs, a claim under its name that another process is refreshing, on an issue
|
|
117
|
+
it looks at. Neither is certain to catch every case, so give each machine
|
|
118
|
+
its own `node_id` rather than relying on the warning.
|
|
119
|
+
|
|
120
|
+
Then open either machine's status page: the **Machines** strip shows both.
|
|
@@ -70,6 +70,14 @@ is the only place ctrlrelay calls out to Claude Code.
|
|
|
70
70
|
- **Environment additions:** `CTRLRELAY_SESSION_ID` and `CTRLRELAY_STATE_FILE`
|
|
71
71
|
are the only contract between dispatcher and child. Anything else the agent
|
|
72
72
|
needs (issue body, PR rules, etc.) goes into the prompt.
|
|
73
|
+
- **Credentials (#219):** every spawn says whether it gets GitHub access
|
|
74
|
+
(`spawn_session(..., github_access=...)`, no default). dev and task spawn
|
|
75
|
+
without it: GitHub tokens, `gh`'s login, git credential helpers, SSH and
|
|
76
|
+
the desktop keyring are cut off (`core/agent_env.py`), and ctrlrelay
|
|
77
|
+
pushes the branch, opens the PR or posts the report itself afterwards
|
|
78
|
+
(`core/publish.py`, `WorktreeManager.push_run_branch`). secops still
|
|
79
|
+
spawns with access. See
|
|
80
|
+
[What the agent can reach](configuration.md#what-the-agent-can-reach-219).
|
|
73
81
|
- **Result:** `SessionResult(session_id, exit_code, state, stdout, stderr,
|
|
74
82
|
agent_session_id)`. `session_id` is our composite orchestrator id;
|
|
75
83
|
`agent_session_id` is Claude's own UUID, parsed from the JSON stdout and
|