ctrlrelay 0.27.0__tar.gz → 0.28.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.
Files changed (253) hide show
  1. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/CHANGELOG.md +171 -1
  2. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/PKG-INFO +10 -5
  3. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/README.md +9 -4
  4. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/config/orchestrator.yaml.example +31 -0
  5. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/architecture.md +27 -13
  6. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/cli.md +5 -2
  7. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/configuration.md +477 -23
  8. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/feedback-loop.md +1 -1
  9. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/getting-started.md +22 -0
  10. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/index.md +2 -1
  11. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/llms.txt +1 -0
  12. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/operations.md +1 -1
  13. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/registered-mode.md +32 -1
  14. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/pyproject.toml +1 -1
  15. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/cli.py +174 -0
  16. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/__init__.py +14 -0
  17. ctrlrelay-0.28.0/src/ctrlrelay/core/agent_check.py +1024 -0
  18. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/agent_env.py +108 -1
  19. ctrlrelay-0.28.0/src/ctrlrelay/core/agent_plugins.py +100 -0
  20. ctrlrelay-0.28.0/src/ctrlrelay/core/agent_presets.py +179 -0
  21. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/budget.py +30 -1
  22. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/checkpoint.py +95 -8
  23. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/config.py +263 -3
  24. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/dispatcher.py +797 -74
  25. ctrlrelay-0.28.0/src/ctrlrelay/core/generic_cli.py +582 -0
  26. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/obs.py +32 -0
  27. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/proof.py +4 -0
  28. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/state.py +79 -4
  29. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/worktree.py +162 -20
  30. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/pipelines/dev.py +104 -10
  31. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/pipelines/secops.py +94 -17
  32. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/pipelines/task.py +77 -8
  33. ctrlrelay-0.28.0/tests/test_agent_check.py +1362 -0
  34. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_agent_no_keys.py +11 -3
  35. ctrlrelay-0.28.0/tests/test_agent_plugins.py +514 -0
  36. ctrlrelay-0.28.0/tests/test_agent_presets.py +493 -0
  37. ctrlrelay-0.28.0/tests/test_agent_process_group.py +392 -0
  38. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_agent_routing.py +2 -0
  39. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_archived.py +1 -0
  40. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_config.py +2 -2
  41. ctrlrelay-0.28.0/tests/test_generic_cli_adapter.py +669 -0
  42. ctrlrelay-0.28.0/tests/test_generic_cli_usage.py +916 -0
  43. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_opencode_adapter.py +33 -1
  44. ctrlrelay-0.28.0/tests/test_provider_credentials.py +252 -0
  45. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_question_survives_restart.py +73 -1
  46. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_run_resume.py +29 -0
  47. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_secops_pipeline.py +1 -0
  48. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_state.py +84 -0
  49. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_worktree.py +343 -0
  50. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  51. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  52. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  53. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/dependabot.yml +0 -0
  54. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/workflows/build.yml +0 -0
  55. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/workflows/cla.yml +0 -0
  56. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/workflows/pages.yml +0 -0
  57. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/workflows/publish.yml +0 -0
  58. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.github/workflows/test.yml +0 -0
  59. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/.gitignore +0 -0
  60. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/CODE_OF_CONDUCT.md +0 -0
  61. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/CONTRIBUTING.md +0 -0
  62. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/LICENSE +0 -0
  63. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/SECURITY.md +0 -0
  64. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/Gemfile +0 -0
  65. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/_config.yml +0 -0
  66. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/adding-a-machine.md +0 -0
  67. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/bridge.md +0 -0
  68. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/control-plane-brain.md +0 -0
  69. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/development.md +0 -0
  70. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/personalization.md +0 -0
  71. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  72. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  73. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/__init__.py +0 -0
  74. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  75. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/__main__.py +0 -0
  76. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/factory.py +0 -0
  77. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/handler.py +0 -0
  78. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  79. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/node_tag.py +0 -0
  80. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  81. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/server.py +0 -0
  82. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  83. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/archived.py +0 -0
  84. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/audit.py +0 -0
  85. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/binaries.py +0 -0
  86. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/blockers.py +0 -0
  87. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/claims.py +0 -0
  88. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/code_review.py +0 -0
  89. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/dispatch_health.py +0 -0
  90. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/events.py +0 -0
  91. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/github.py +0 -0
  92. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/github_budget.py +0 -0
  93. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/heartbeat.py +0 -0
  94. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/intake_hold.py +0 -0
  95. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/issue_questions.py +0 -0
  96. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/job_claims.py +0 -0
  97. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/network.py +0 -0
  98. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/node_check.py +0 -0
  99. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/orphans.py +0 -0
  100. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/poller.py +0 -0
  101. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  102. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  103. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/publish.py +0 -0
  104. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/question_expiry.py +0 -0
  105. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/redact.py +0 -0
  106. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/reopens.py +0 -0
  107. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/scheduler.py +0 -0
  108. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/self_update.py +0 -0
  109. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/slots.py +0 -0
  110. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/stuck.py +0 -0
  111. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/usage.py +0 -0
  112. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/core/weekly_summary.py +0 -0
  113. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  114. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/dashboard/client.py +0 -0
  115. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/gh_protocol.py +0 -0
  116. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/install.py +0 -0
  117. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/personalization/__init__.py +0 -0
  118. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/personalization/manager.py +0 -0
  119. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/personalization/merge.py +0 -0
  120. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/personalization/paths.py +0 -0
  121. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  122. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/pipelines/base.py +0 -0
  123. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  124. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/setup.py +0 -0
  125. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/__init__.py +0 -0
  126. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  127. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  128. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  129. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  130. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  131. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  132. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  133. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  134. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/transports/__init__.py +0 -0
  135. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/transports/base.py +0 -0
  136. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/transports/file_mock.py +0 -0
  137. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/transports/socket_client.py +0 -0
  138. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/__init__.py +0 -0
  139. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/drift.py +0 -0
  140. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/fleet.py +0 -0
  141. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/health.py +0 -0
  142. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/links.py +0 -0
  143. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/numbers.py +0 -0
  144. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/server.py +0 -0
  145. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/static/index.html +0 -0
  146. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/status.py +0 -0
  147. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/transcript.py +0 -0
  148. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/src/ctrlrelay/web/waiting.py +0 -0
  149. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/__init__.py +0 -0
  150. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/_poller_fakes.py +0 -0
  151. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/_publish_fakes.py +0 -0
  152. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/conftest.py +0 -0
  153. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/fixtures/gh_search_issues.json +0 -0
  154. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  155. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/fixtures/transcript_sample.jsonl +0 -0
  156. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_agent_backend_wiring.py +0 -0
  157. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_agent_sandbox.py +0 -0
  158. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_allowed_accounts.py +0 -0
  159. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_audit.py +0 -0
  160. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_binary_resolution.py +0 -0
  161. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_bridge_protocol.py +0 -0
  162. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_bridge_server.py +0 -0
  163. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_bridge_start_command.py +0 -0
  164. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_budget.py +0 -0
  165. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_chat_machines.py +0 -0
  166. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_checkpoint.py +0 -0
  167. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_claim_reconciliation.py +0 -0
  168. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_cli_ci_wait.py +0 -0
  169. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_cli_dev.py +0 -0
  170. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_cli_repos.py +0 -0
  171. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_cli_secops.py +0 -0
  172. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_cli_start.py +0 -0
  173. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_cli_version.py +0 -0
  174. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_code_review.py +0 -0
  175. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dashboard_client.py +0 -0
  176. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dev_integration.py +0 -0
  177. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dev_pipeline.py +0 -0
  178. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dev_plan_step.py +0 -0
  179. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dev_step_switches.py +0 -0
  180. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dev_stuck.py +0 -0
  181. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dev_work_steps.py +0 -0
  182. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dispatch_health.py +0 -0
  183. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dispatch_outcome_hook.py +0 -0
  184. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_dispatcher.py +0 -0
  185. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_docs_site.py +0 -0
  186. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_event_emission.py +0 -0
  187. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_events.py +0 -0
  188. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_fleet.py +0 -0
  189. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_gh_protocol.py +0 -0
  190. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_github.py +0 -0
  191. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_github_budget.py +0 -0
  192. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_install.py +0 -0
  193. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_intake_hold.py +0 -0
  194. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_intake_hold_poller.py +0 -0
  195. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_issue_blockers.py +0 -0
  196. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_issue_outcome_announcement.py +0 -0
  197. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_issue_questions.py +0 -0
  198. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_job_claims.py +0 -0
  199. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_mattermost_handler.py +0 -0
  200. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_max_parallel.py +0 -0
  201. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_network.py +0 -0
  202. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_no_change_run.py +0 -0
  203. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_node_check.py +0 -0
  204. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_node_claims.py +0 -0
  205. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_notify_transport.py +0 -0
  206. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_obs.py +0 -0
  207. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_orphans.py +0 -0
  208. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_personalization.py +0 -0
  209. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_personalization_merge.py +0 -0
  210. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_pipeline_base.py +0 -0
  211. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_poller.py +0 -0
  212. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_poller_priority_order.py +0 -0
  213. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_poller_recheck.py +0 -0
  214. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_poller_search.py +0 -0
  215. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_post_merge.py +0 -0
  216. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_pr_verifier.py +0 -0
  217. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_pr_watcher.py +0 -0
  218. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_pre_dispatch_retry.py +0 -0
  219. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_proof.py +0 -0
  220. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_publish_failure_retry.py +0 -0
  221. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_question_backoff.py +0 -0
  222. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_question_expiry.py +0 -0
  223. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_redact.py +0 -0
  224. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_relay_alert_off_lock.py +0 -0
  225. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_relay_git_263.py +0 -0
  226. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_reopens.py +0 -0
  227. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_resume_prompt_contract.py +0 -0
  228. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_review_copy.py +0 -0
  229. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_review_gate.py +0 -0
  230. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_sandbox_visibility.py +0 -0
  231. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_scheduler.py +0 -0
  232. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_secops_integration.py +0 -0
  233. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_secops_operator_prs.py +0 -0
  234. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_self_update.py +0 -0
  235. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_setup.py +0 -0
  236. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_stuck.py +0 -0
  237. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_task_pipeline.py +0 -0
  238. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_telegram_handler.py +0 -0
  239. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_timeout_save.py +0 -0
  240. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_transport.py +0 -0
  241. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_transport_resolution.py +0 -0
  242. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_unreviewed_pr_announcement.py +0 -0
  243. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_usage.py +0 -0
  244. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_drift.py +0 -0
  245. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_health.py +0 -0
  246. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_links.py +0 -0
  247. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_numbers.py +0 -0
  248. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_peer_links.py +0 -0
  249. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_server.py +0 -0
  250. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_status.py +0 -0
  251. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_transcript.py +0 -0
  252. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_web_waiting.py +0 -0
  253. {ctrlrelay-0.27.0 → ctrlrelay-0.28.0}/tests/test_weekly_summary.py +0 -0
@@ -7,6 +7,175 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.28.0] - 2026-11-11
11
+
12
+ ### Added
13
+
14
+ - `ctrlrelay agent check <agent_type>` proves one backend writes its
15
+ checkpoint, without starting a dev run (#381). It spawns a single session
16
+ in a throwaway git repository in a temporary directory, with GitHub
17
+ credentials stripped and a prompt whose whole content is the `DONE`
18
+ checkpoint snippet, then reports the exit code, the checkpoint and its
19
+ status, the native session id the adapter captured, the selected identity
20
+ and the elapsed time — one line each, because they fail separately. Exits
21
+ 0 only on a `DONE` checkpoint from a session that exited on its own: a
22
+ CLI that exits zero having written nothing is the failure the command
23
+ exists to find, and one that writes `DONE` and then has to be stopped
24
+ would hang every dev run. The throwaway repository is a working
25
+ directory, not a sandbox. `--timeout` defaults to 300 seconds and must
26
+ be at least 1, `--json` prints the same report as one object including on
27
+ the failure paths — with the structured log moved to stderr so stdout is
28
+ exactly that object — and an unknown type exits non-zero naming every type
29
+ enrolled on the node. As an operator tool it may select any enrolled
30
+ backend and does not consult `agent.assignable_types`, which governs what
31
+ an assignment arriving from outside the machine may ask for.
32
+ - `ctrlrelay agent check <agent_type> --resume` proves the other half of the
33
+ contract: that the backend comes back knowing what it was doing (#382). A
34
+ backend that cannot resume fails every run that has to ask a question, and
35
+ the only way to exercise it before this was a real dev run that blocked and
36
+ waited for a chat answer. Round 1 is told a freshly generated 8-character
37
+ token and must write `BLOCKED_NEEDS_INPUT`; round 2 spawns with
38
+ `resume_session_id` set to the native id round 1 reported — the same argv a
39
+ pipeline resume builds — and must write `DONE` whose summary is that token.
40
+ The token is not in the second prompt, so a round that answered from its
41
+ own prompt could not pass, and a decoy fresh session with a different token
42
+ runs between the rounds, so a manifest that resumes the most recent session
43
+ instead of the id it is given fails rather than passing by luck. Round 1 must report both the blocked checkpoint
44
+ and a native session id or the command stops and says which was missing,
45
+ rather than spending a second fresh session that could only report a pass
46
+ for a backend that never resumed. A backend reporting no `resume`
47
+ capability fails with `resume not configured for <type>` without spawning.
48
+ A native session id that changes between rounds is a warning, not a
49
+ failure. Each round gets its own report line, each failure its own, and
50
+ `--json` carries both rounds plus `resume: true`.
51
+ - A plugin identity and capability contract for coding-agent CLIs (#379).
52
+ Claude and OpenCode now expose the same identity metadata, and local
53
+ `agent.plugins` manifests can enroll simple non-interactive CLIs with fixed
54
+ argv, prompt-over-argument or stdin, checkpoint handling, and optional
55
+ native-session mapping. `agent.assignable_types` defines the smaller set a
56
+ future control-plane assignment may select; assignments cannot supply a
57
+ binary or arbitrary arguments. The selected plugin, model, and canonical
58
+ invocation fingerprint are persisted before dispatch and pinned across CI
59
+ fixes, retries, and pending resumes. Generic executables must resolve to an
60
+ absolute path, native session IDs are validated before argv substitution,
61
+ and a pre-#379 native session with unknown identity is refused rather than
62
+ resumed through the current default backend.
63
+ - Repo context is linked under each backend's own instruction filename, so
64
+ Codex and Copilot see `AGENTS.md` or `.github/copilot-instructions.md`
65
+ instead of only Claude's `CLAUDE.md` (#379). A name the repository already
66
+ has is left alone: the repo's own file wins under that name, and the relay
67
+ context goes only under names that are free.
68
+ - Built-in identity pins use the stable launcher path, so Claude's normal
69
+ self-update does not invalidate a pending resume. Generic manifests still
70
+ pin their full invocation mapping (#379).
71
+ - A checked GitHub Copilot CLI manifest in `docs/configuration.md` and the
72
+ example config, with resume through its `sessionId`. It passes
73
+ `--disable-builtin-mcps`, because Copilot's built-in GitHub MCP server
74
+ otherwise gives the agent GitHub access under Copilot's own login,
75
+ bypassing credential stripping (#379).
76
+
77
+ - Copilot and Codex as built-in agent presets (#384, #385). Set
78
+ `repos[].agent_type: copilot` or `codex` (or `agent.type`) with no
79
+ `agent.plugins` entry. Their arguments are fixed in code, because some of them
80
+ are what make the backend safe unattended: Copilot's `--disable-builtin-mcps`
81
+ keeps its own GitHub login away from the agent. `agent.presets.<name>` may
82
+ change only `binary`, `model` and `default_timeout_seconds`. Both presets
83
+ run unconfined and refuse to start on a `sandbox: "bwrap"` node.
84
+ - Generic plugins can have their token usage measured, so the token budget
85
+ applies to them (#387). A manifest's `usage` mapping says which JSON-lines
86
+ event carries usage and where each count is. The reading is taken when the
87
+ CLI exits, a resumed round is charged only the difference from the
88
+ session's last total, and anything unreadable is recorded as unknown, never
89
+ zero. Both presets carry mappings measured against real output.
90
+ - Each agent backend receives only its own model-provider keys (#390). A
91
+ well-known provider variable such as `OPENAI_API_KEY` or
92
+ `ANTHROPIC_API_KEY` reaches an agent only if its backend allows it:
93
+ Claude gets `ANTHROPIC_*` and `CLAUDE_*`, the presets their own, and
94
+ `opencode` and plugins what `env_allow` lists (default none). The security
95
+ sweep keeps GitHub access but loses the relay's secrets and other backends'
96
+ keys.
97
+ - One agent backends table in the docs, with resume, usage, stuck detection
98
+ and sandbox for each backend, and how to log the Copilot and Codex CLIs in
99
+ for relay use (#386).
100
+
101
+ ### Fixed
102
+
103
+ - An agent can no longer outlive its session or stall the relay through
104
+ what it leaves behind (#394). Agents are spawned in their own session, so
105
+ each leads its own process group, and every exit path signals that group
106
+ rather than the direct child alone: after a normal exit and on a timeout
107
+ with SIGTERM, a 3-second grace, then SIGKILL; on cancellation, a stuck or
108
+ over-budget stop and a pipe error with an immediate SIGKILL, as the child
109
+ alone was before. A job the agent put in the background used
110
+ to keep running as the relay user after the run was reported done; and
111
+ because agents shared the relay's own process group, an agent that
112
+ signalled its group stopped the relay with it. The relay never signals a
113
+ group that is not provably the agent's own. The checkpoint read now opens
114
+ only a small regular file (`O_NOFOLLOW | O_NONBLOCK`, `fstat` on the same
115
+ descriptor, a 256 KiB cap, and the `.ctrlrelay` directory itself opened
116
+ without following a symlink, so an agent cannot point it at another run's
117
+ and have the relay read — and delete — that run's checkpoint): a FIFO at the state path blocked the read
118
+ forever, past the session timeout and with every other run on the event
119
+ loop stalled behind it, and a huge file or a symlink to `/dev/zero` was
120
+ read whole. A checkpoint that breaks the JSON parser (an integer past the
121
+ int-string limit, deep nesting, bad UTF-8) is now an unreadable checkpoint
122
+ rather than an exception. Known limit: a process group does not contain a
123
+ process that leaves it on purpose — one that calls `setsid` or
124
+ `setpgid` survives without a sandbox. Under `sandbox: "bwrap"` the PID
125
+ namespace ends it with the sandbox; full containment for the other
126
+ backends is #352. A narrow race also remains: if the agent's group id is
127
+ recycled for another group during a session and that group's leader has
128
+ exited too, the relay cannot tell the two apart and may signal it.
129
+ - An agent no longer inherits the daemon's stdin; it gets `/dev/null`
130
+ unless its prompt is sent on stdin. `codex exec` appends piped stdin to
131
+ its prompt and waits for end-of-file, so with the daemon's stdin an open
132
+ pipe every Codex run sat until the timeout without starting (#379).
133
+ - A repository's own instruction link that points outside the checkout,
134
+ such as `AGENTS.md -> /opt/shared/AGENTS.md`, was taken for the relay's
135
+ context link: it was replaced while the agent ran and deleted at cleanup.
136
+ Only links to the repo's context file are now replaced or removed (#379).
137
+ - When the daemon's `PATH` held a relative entry (`.`, an empty entry,
138
+ `bin`), a file committed to the repository could run in place of a bare
139
+ agent `binary`, of an `#!/usr/bin/env` interpreter, or of a command the
140
+ agent ran, because the agent starts inside the worktree. Agents now start
141
+ with relative `PATH` entries removed, sandboxed or not. This predates the
142
+ plugin runtime.
143
+ - Context-link cleanup walks the worktree without following symlinks, so a
144
+ directory swapped for a link during a run cannot redirect it outside the
145
+ checkout (#379).
146
+ - The relay no longer replaces a repository's own `CLAUDE.md` with its repo
147
+ context link. The tracked file became a type change while the agent ran
148
+ and a deletion after cleanup, so an agent that staged everything could put
149
+ either into its pull request. Cleanup now removes only links the relay
150
+ made (#379).
151
+ - A plugin listed under `agent.plugins` but not routed to by any repo or by
152
+ `agent.assignable_types` is built only when a pinned run selects it, so
153
+ one that cannot run under `agent.sandbox: bwrap` no longer stops the
154
+ daemon at startup. A routed one still does, naming where it was routed
155
+ from. A backend that fails to build on demand is refused for that run
156
+ instead of raising out of the pipeline (#379).
157
+
158
+ ### Upgrade notes
159
+
160
+ - `agent.plugins.copilot` and `agent.plugins.codex` are now refused at load,
161
+ because those names are built-in presets (#384, #385). Remove the entry and
162
+ set `agent.presets.<name>` if you changed its binary, model or timeout.
163
+ - An `opencode` setup, or a plugin, that reads a model-provider key from the
164
+ environment now needs that key listed in its `env_allow` (#390).
165
+ - On Linux, log the Copilot CLI in with the keyring out of reach
166
+ (`DBUS_SESSION_BUS_ADDRESS=disabled: copilot`, then `/login`) or set
167
+ `COPILOT_GITHUB_TOKEN`: the relay blocks the desktop keyring, so a
168
+ keyring-stored login cannot be read.
169
+ - A run waiting on a question from before this release has no recorded
170
+ agent identity. If its agent session had already started, it is refused
171
+ with `agent_identity_unknown` instead of being resumed on whatever backend
172
+ is configured now. Answer or close those questions before upgrading, or
173
+ re-run the issue afterwards (#379).
174
+ - A repo that commits its own `CLAUDE.md` now runs with that file instead of
175
+ the relay's context under that name. Move anything the agent must see into
176
+ the repo's file, or into the contexts entry for a name the repo does not
177
+ use (#379).
178
+
10
179
  ## [0.27.0] - 2026-11-06
11
180
 
12
181
  ### Added
@@ -2650,7 +2819,8 @@ pipeline).
2650
2819
  per-phase implementation plans (Phase 0 through Phase 4).
2651
2820
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2652
2821
 
2653
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.27.0...HEAD
2822
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.28.0...HEAD
2823
+ [0.28.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.27.0...v0.28.0
2654
2824
  [0.27.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.26.0...v0.27.0
2655
2825
  [0.26.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.25.0...v0.26.0
2656
2826
  [0.25.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.24.0...v0.25.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrlrelay
3
- Version: 0.27.0
3
+ Version: 0.28.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/
@@ -66,7 +66,8 @@ Coding agents are great when you sit and watch them. ctrlrelay is for the
66
66
  work you don't want to sit and watch.
67
67
 
68
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
69
+ (or add a label) and the work starts. Teammates you allow can hand it work
70
+ the same way. The result is a normal pull request
70
71
  your team already knows how to review.
71
72
  - **Runs where your code already lives.** ctrlrelay is a small program on your
72
73
  laptop or server — not a cloud service. Nothing leaves your machine except
@@ -81,8 +82,11 @@ work you don't want to sit and watch.
81
82
  - **Done means done.** It waits for CI, fixes failing checks, and posts an
82
83
  independent review on the pull request — so what reaches you is ready to
83
84
  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.
85
+ - **Pick your agent per repo.** Claude Code, OpenCode, GitHub Copilot CLI or
86
+ Codex, plus simple non-interactive CLIs enrolled through a local plugin
87
+ manifest ([what each one supports](https://ainvirion.github.io/ctrlrelay/configuration/#agent-backends)).
88
+ Choose the default per repo; a future control plane can select from the
89
+ agents you allow.
86
90
  - **Grows with you.** Start on one laptop. Add more machines later; they share
87
91
  the work without stepping on each other.
88
92
  - **Open source.** Apache 2.0. Read every line of what runs on your machine.
@@ -116,7 +120,8 @@ flowchart LR
116
120
  PR -->|CI + review| YOU(["You merge"])
117
121
  ```
118
122
 
119
- 1. **You assign an issue** to yourself, or add a label you pick.
123
+ 1. **You assign an issue** to yourself, or add a label you pick. Teammates
124
+ on your `allowed_accounts` list can do the same.
120
125
  2. **ctrlrelay starts the agent** in a fresh, isolated copy of the repo.
121
126
  3. **If the agent has a question**, it lands in your chat. Your reply goes
122
127
  straight back to the same session.
@@ -25,7 +25,8 @@ Coding agents are great when you sit and watch them. ctrlrelay is for the
25
25
  work you don't want to sit and watch.
26
26
 
27
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
28
+ (or add a label) and the work starts. Teammates you allow can hand it work
29
+ the same way. The result is a normal pull request
29
30
  your team already knows how to review.
30
31
  - **Runs where your code already lives.** ctrlrelay is a small program on your
31
32
  laptop or server — not a cloud service. Nothing leaves your machine except
@@ -40,8 +41,11 @@ work you don't want to sit and watch.
40
41
  - **Done means done.** It waits for CI, fixes failing checks, and posts an
41
42
  independent review on the pull request — so what reaches you is ready to
42
43
  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.
44
+ - **Pick your agent per repo.** Claude Code, OpenCode, GitHub Copilot CLI or
45
+ Codex, plus simple non-interactive CLIs enrolled through a local plugin
46
+ manifest ([what each one supports](https://ainvirion.github.io/ctrlrelay/configuration/#agent-backends)).
47
+ Choose the default per repo; a future control plane can select from the
48
+ agents you allow.
45
49
  - **Grows with you.** Start on one laptop. Add more machines later; they share
46
50
  the work without stepping on each other.
47
51
  - **Open source.** Apache 2.0. Read every line of what runs on your machine.
@@ -75,7 +79,8 @@ flowchart LR
75
79
  PR -->|CI + review| YOU(["You merge"])
76
80
  ```
77
81
 
78
- 1. **You assign an issue** to yourself, or add a label you pick.
82
+ 1. **You assign an issue** to yourself, or add a label you pick. Teammates
83
+ on your `allowed_accounts` list can do the same.
79
84
  2. **ctrlrelay starts the agent** in a fresh, isolated copy of the repo.
80
85
  3. **If the agent has a question**, it lands in your chat. Your reply goes
81
86
  straight back to the same session.
@@ -39,6 +39,9 @@ agent:
39
39
  binary: "claude"
40
40
  default_timeout_seconds: 1800
41
41
  output_format: "json"
42
+ # Enrolled types a future control-plane assignment may explicitly select.
43
+ # Empty keeps selection entirely local. Standalone repo defaults are unaffected.
44
+ assignable_types: []
42
45
 
43
46
  # Read only when a backend of type "opencode" is built, so this block
44
47
  # is inert on a claude-only deployment and harmless to leave in place.
@@ -67,6 +70,34 @@ agent:
67
70
  # # model is usually slower per token than a hosted one.
68
71
  # default_timeout_seconds: 3600
69
72
 
73
+ # Optional generic CLI manifests (#379). Each key becomes an agent_type you
74
+ # can use as agent.type, repos[].agent_type, or an allowlisted assignment.
75
+ # Arguments run directly (no shell). Supported placeholders: {prompt},
76
+ # {session_id}, {state_file}, {resume_session_id}, {model}, {provider}.
77
+ # Generic plugins have no live transcript, usage reading, or bwrap support.
78
+ # plugins:
79
+ # example:
80
+ # binary: "/opt/example/bin/agent"
81
+ # args: ["run", "{prompt}"]
82
+ # prompt_mode: "arg" # or "stdin"
83
+ # resume_args: ["resume", "{resume_session_id}", "{prompt}"]
84
+ # session_id_path: "info.session.id" # required with resume_args
85
+ # model: "model-x"
86
+ # provider: "provider-y"
87
+ # plugin_version: "1"
88
+ # instructions_files: ["AGENTS.md"] # names the CLI reads for context
89
+ # default_timeout_seconds: 1800
90
+ # Copilot and Codex are built-in presets: set repos[].agent_type to
91
+ # "copilot" or "codex" with no entry here. Only these three keys can be
92
+ # changed; the arguments are fixed. See docs/configuration.md.
93
+ # presets:
94
+ # copilot:
95
+ # binary: "copilot"
96
+ # model: "claude-sonnet-4.5"
97
+ # default_timeout_seconds: 3600
98
+ # codex:
99
+ # model: "gpt-5.1-codex"
100
+
70
101
  # EXACTLY ONE transport is active, and it is the one named by `type`.
71
102
  # The other blocks below are inert: they may stay in the file, but nothing
72
103
  # reads them. Running Telegram and Mattermost at the same time is NOT
@@ -40,11 +40,15 @@ pipeline work.
40
40
  ## Dispatcher ↔ agent interaction
41
41
 
42
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.
43
+ owns the shared subprocess lifecycle. Thin adapters provide CLI-specific argv
44
+ and native-session parsing: `ClaudeDispatcher`, `OpenCodeDispatcher`, and
45
+ `GenericCliDispatcher` for local manifests. All expose the same async
46
+ `spawn_session` shape, so pipelines stay agent-agnostic.
47
+ `make_repo_agent_dispatcher` enrolls the configured plugins and selects the
48
+ standalone default per repo: `agent.type` is the global default and
49
+ `repos[].agent_type` overrides it. `select_agent` additionally resolves an
50
+ allowlisted assignment override or a previously pinned identity to one concrete
51
+ adapter at the run boundary.
48
52
 
49
53
  - **Spawn shape:** Claude Code runs
50
54
  `claude -p <prompt> --output-format stream-json --verbose --dangerously-skip-permissions [--resume <id>]`;
@@ -70,11 +74,20 @@ with the same async `spawn_session` shape, so pipelines stay agent-agnostic:
70
74
  exposes `.success`, `.blocked`, `.failed` properties for pattern-matching
71
75
  pipelines.
72
76
  - **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.
77
+ Code), `--session <id>` (OpenCode), or a generic manifest's `resume_args`.
78
+ Pipelines look up the agent's own id (persisted as
79
+ `sessions.agent_session_id` by the prior spawn) and pass it through. A
80
+ generic plugin without a resume mapping refuses the request instead of
81
+ silently starting fresh. Claude retains its existing fresh-session fallback
82
+ when no native id was captured.
83
+ - **Identity and capabilities (#379):** every concrete adapter reports an
84
+ `AgentIdentity` (`agent_type`, plugin id/version, model, provider and an
85
+ invocation fingerprint) and conservative `AgentCapabilities` (resume,
86
+ structured events, usage and sandbox). The selected identity is written to
87
+ `sessions` before dispatch and
88
+ checked again before storing a native session id. A conflicting identity is
89
+ an error, never a silent backend switch. Capabilities default to false;
90
+ missing usage or structured-event support is not guessed.
78
91
 
79
92
  The dispatcher does NOT proxy or wrap the agent session — it shells out the
80
93
  same way you'd shell out to `gh` or `git`. This is deliberate.
@@ -87,7 +100,7 @@ SQLite, single file at `paths.state_db`.
87
100
 
88
101
  | Table | Columns | Notes |
89
102
  |---|---|---|
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). |
103
+ | `sessions` | `id`, `pipeline`, `repo`, `issue_number`, `worktree_path`, `status`, `blocked_question`, `started_at`, `ended_at`, `claude_exit_code`, `summary`, `agent_session_id`, `agent_type`, `agent_plugin_id`, `agent_plugin_version`, `agent_model`, `agent_provider`, `agent_fingerprint`, `output_tail` | Indexed on `repo` and `status`. One row per pipeline run. `agent_session_id` stores the agent's own session id for resume; the `agent_*` columns pin the selected plugin identity and invocation fingerprint (#379); `claude_exit_code` holds the worker's exit code whichever backend ran; `output_tail` is a scrubbed, capped tail of its output (#204). |
91
104
  | `repo_locks` | `repo` (PK), `session_id`, `acquired_at` | Acquired with `acquire_lock`, released in the pipeline's `finally`. |
92
105
  | `github_cursor` | `repo`, `last_checked_at`, `last_seen_issue_update` | Bounds GitHub API calls in the poller. |
93
106
  | `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. |
@@ -99,8 +112,9 @@ SQLite, single file at `paths.state_db`.
99
112
  | `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
113
 
101
114
  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`
115
+ `list_locks`, `set_agent_session_id`, `get_agent_session_id`,
116
+ `pin_agent_identity`, `get_agent_identity`, plus raw `execute` / `commit`.
117
+ The pipeline code uses raw SQL for the `sessions`
104
118
  table on purpose — kept thin and grep-able.
105
119
 
106
120
  ## Worktree lifecycle
@@ -203,7 +203,8 @@ ctrlrelay run dev -i ISSUE [-r REPO] [-c PATH] [--from-step STEP]
203
203
  | `--from-step` | no | — | Start again from `plan`, `code` or `publish`, taking the earlier steps from the issue's newest run that saved them and moving the branch back. See [A run whose process died](operations.md#a-run-whose-process-died). |
204
204
 
205
205
  Acquires the per-repo lock, creates the worktree, spawns the repo's agent
206
- (Claude Code or OpenCode, per `agent.type` / `repos[].agent_type`) with the
206
+ (Claude Code, OpenCode, Copilot, Codex or a plugin, per `agent.type` /
207
+ `repos[].agent_type`) with the
207
208
  issue's title/body in the prompt, and reports the result. If the agent blocks
208
209
  and a chat transport (Telegram or Mattermost) is configured, posts the question
209
210
  to your chat and waits for a reply (up to `DEFAULT_MAX_BLOCKED_ROUNDS` rounds).
@@ -263,7 +264,9 @@ ctrlrelay poller start [-c PATH] [-F|--foreground] [-i SECONDS]
263
264
  | `--config` / `-c` | `config/orchestrator.yaml` | Config path. |
264
265
 
265
266
  Polls each configured repo for issues assigned to your GitHub user (resolved
266
- via `gh api user --jq .login`). On the first run, seeds the seen-issue set
267
+ via `gh api user --jq .login`), by you or by an account in
268
+ `allowed_accounts`, plus issues carrying a trigger label one of those
269
+ accounts added. On the first run, seeds the seen-issue set
267
270
  with current assignments so it does not replay your existing backlog.
268
271
 
269
272
  For each newly assigned issue, runs `run_dev_issue(...)` (i.e. the dev pipeline)