ctrlrelay 0.27.0__tar.gz → 0.28.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/CHANGELOG.md +217 -1
  2. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/PKG-INFO +10 -5
  3. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/README.md +9 -4
  4. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/config/orchestrator.yaml.example +31 -0
  5. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/architecture.md +27 -13
  6. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/cli.md +5 -2
  7. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/configuration.md +478 -24
  8. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/feedback-loop.md +35 -9
  9. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/getting-started.md +22 -0
  10. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/index.md +2 -1
  11. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/llms.txt +1 -0
  12. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/operations.md +1 -1
  13. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/registered-mode.md +32 -1
  14. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/pyproject.toml +1 -1
  15. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/cli.py +174 -0
  16. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/__init__.py +14 -0
  17. ctrlrelay-0.28.1/src/ctrlrelay/core/agent_check.py +1025 -0
  18. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/agent_env.py +108 -1
  19. ctrlrelay-0.28.1/src/ctrlrelay/core/agent_plugins.py +100 -0
  20. ctrlrelay-0.28.1/src/ctrlrelay/core/agent_presets.py +179 -0
  21. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/budget.py +30 -1
  22. ctrlrelay-0.28.1/src/ctrlrelay/core/checkpoint.py +303 -0
  23. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/config.py +263 -3
  24. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/dispatcher.py +797 -74
  25. ctrlrelay-0.28.1/src/ctrlrelay/core/generic_cli.py +582 -0
  26. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/github.py +12 -0
  27. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/issue_questions.py +59 -10
  28. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/obs.py +32 -0
  29. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/proof.py +4 -0
  30. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/question_expiry.py +76 -13
  31. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/state.py +79 -4
  32. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/worktree.py +240 -20
  33. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/pipelines/dev.py +121 -21
  34. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/pipelines/secops.py +113 -30
  35. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/pipelines/task.py +92 -19
  36. ctrlrelay-0.28.1/tests/test_agent_check.py +1362 -0
  37. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_agent_no_keys.py +11 -3
  38. ctrlrelay-0.28.1/tests/test_agent_plugins.py +514 -0
  39. ctrlrelay-0.28.1/tests/test_agent_presets.py +493 -0
  40. ctrlrelay-0.28.1/tests/test_agent_process_group.py +392 -0
  41. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_agent_routing.py +2 -0
  42. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_archived.py +1 -0
  43. ctrlrelay-0.28.1/tests/test_checkpoint_snippet.py +176 -0
  44. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_config.py +2 -2
  45. ctrlrelay-0.28.1/tests/test_generic_cli_adapter.py +669 -0
  46. ctrlrelay-0.28.1/tests/test_generic_cli_usage.py +916 -0
  47. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_issue_questions.py +59 -0
  48. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_opencode_adapter.py +33 -1
  49. ctrlrelay-0.28.1/tests/test_provider_credentials.py +252 -0
  50. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_question_expiry.py +171 -0
  51. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_question_survives_restart.py +73 -1
  52. ctrlrelay-0.28.1/tests/test_relay_git_credential.py +218 -0
  53. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_resume_prompt_contract.py +6 -4
  54. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_run_resume.py +29 -0
  55. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_secops_pipeline.py +1 -0
  56. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_state.py +84 -0
  57. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_worktree.py +343 -0
  58. ctrlrelay-0.27.0/src/ctrlrelay/core/checkpoint.py +0 -155
  59. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  60. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  61. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  62. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/dependabot.yml +0 -0
  63. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/workflows/build.yml +0 -0
  64. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/workflows/cla.yml +0 -0
  65. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/workflows/pages.yml +0 -0
  66. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/workflows/publish.yml +0 -0
  67. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.github/workflows/test.yml +0 -0
  68. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/.gitignore +0 -0
  69. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/CODE_OF_CONDUCT.md +0 -0
  70. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/CONTRIBUTING.md +0 -0
  71. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/LICENSE +0 -0
  72. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/SECURITY.md +0 -0
  73. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/Gemfile +0 -0
  74. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/_config.yml +0 -0
  75. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/adding-a-machine.md +0 -0
  76. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/bridge.md +0 -0
  77. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/control-plane-brain.md +0 -0
  78. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/development.md +0 -0
  79. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/personalization.md +0 -0
  80. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/superpowers/plans/2026-10-30-status-page.md +0 -0
  81. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/docs/superpowers/specs/2026-10-30-status-page-design.md +0 -0
  82. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/__init__.py +0 -0
  83. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/__init__.py +0 -0
  84. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/__main__.py +0 -0
  85. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/factory.py +0 -0
  86. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/handler.py +0 -0
  87. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/mattermost_handler.py +0 -0
  88. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/node_tag.py +0 -0
  89. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/protocol.py +0 -0
  90. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/server.py +0 -0
  91. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  92. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/archived.py +0 -0
  93. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/audit.py +0 -0
  94. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/binaries.py +0 -0
  95. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/blockers.py +0 -0
  96. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/claims.py +0 -0
  97. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/code_review.py +0 -0
  98. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/dispatch_health.py +0 -0
  99. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/events.py +0 -0
  100. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/github_budget.py +0 -0
  101. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/heartbeat.py +0 -0
  102. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/intake_hold.py +0 -0
  103. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/job_claims.py +0 -0
  104. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/network.py +0 -0
  105. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/node_check.py +0 -0
  106. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/orphans.py +0 -0
  107. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/poller.py +0 -0
  108. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/pr_verifier.py +0 -0
  109. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/pr_watcher.py +0 -0
  110. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/publish.py +0 -0
  111. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/redact.py +0 -0
  112. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/reopens.py +0 -0
  113. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/scheduler.py +0 -0
  114. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/self_update.py +0 -0
  115. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/slots.py +0 -0
  116. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/stuck.py +0 -0
  117. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/usage.py +0 -0
  118. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/core/weekly_summary.py +0 -0
  119. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/dashboard/__init__.py +0 -0
  120. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/dashboard/client.py +0 -0
  121. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/gh_protocol.py +0 -0
  122. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/install.py +0 -0
  123. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/personalization/__init__.py +0 -0
  124. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/personalization/manager.py +0 -0
  125. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/personalization/merge.py +0 -0
  126. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/personalization/paths.py +0 -0
  127. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/pipelines/__init__.py +0 -0
  128. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/pipelines/base.py +0 -0
  129. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  130. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/setup.py +0 -0
  131. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/__init__.py +0 -0
  132. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
  133. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  134. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
  135. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
  136. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  137. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
  138. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
  139. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/templates/systemd/web.service.template +0 -0
  140. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/transports/__init__.py +0 -0
  141. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/transports/base.py +0 -0
  142. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/transports/file_mock.py +0 -0
  143. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/transports/socket_client.py +0 -0
  144. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/__init__.py +0 -0
  145. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/drift.py +0 -0
  146. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/fleet.py +0 -0
  147. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/health.py +0 -0
  148. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/links.py +0 -0
  149. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/numbers.py +0 -0
  150. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/server.py +0 -0
  151. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/static/index.html +0 -0
  152. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/status.py +0 -0
  153. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/transcript.py +0 -0
  154. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/src/ctrlrelay/web/waiting.py +0 -0
  155. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/__init__.py +0 -0
  156. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/_poller_fakes.py +0 -0
  157. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/_publish_fakes.py +0 -0
  158. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/conftest.py +0 -0
  159. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/fixtures/gh_search_issues.json +0 -0
  160. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/fixtures/stream_json_rate_limit.jsonl +0 -0
  161. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/fixtures/transcript_sample.jsonl +0 -0
  162. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_agent_backend_wiring.py +0 -0
  163. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_agent_sandbox.py +0 -0
  164. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_allowed_accounts.py +0 -0
  165. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_audit.py +0 -0
  166. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_binary_resolution.py +0 -0
  167. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_bridge_protocol.py +0 -0
  168. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_bridge_server.py +0 -0
  169. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_bridge_start_command.py +0 -0
  170. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_budget.py +0 -0
  171. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_chat_machines.py +0 -0
  172. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_checkpoint.py +0 -0
  173. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_claim_reconciliation.py +0 -0
  174. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_cli_ci_wait.py +0 -0
  175. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_cli_dev.py +0 -0
  176. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_cli_repos.py +0 -0
  177. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_cli_secops.py +0 -0
  178. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_cli_start.py +0 -0
  179. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_cli_version.py +0 -0
  180. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_code_review.py +0 -0
  181. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dashboard_client.py +0 -0
  182. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dev_integration.py +0 -0
  183. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dev_pipeline.py +0 -0
  184. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dev_plan_step.py +0 -0
  185. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dev_step_switches.py +0 -0
  186. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dev_stuck.py +0 -0
  187. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dev_work_steps.py +0 -0
  188. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dispatch_health.py +0 -0
  189. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dispatch_outcome_hook.py +0 -0
  190. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_dispatcher.py +0 -0
  191. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_docs_site.py +0 -0
  192. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_event_emission.py +0 -0
  193. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_events.py +0 -0
  194. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_fleet.py +0 -0
  195. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_gh_protocol.py +0 -0
  196. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_github.py +0 -0
  197. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_github_budget.py +0 -0
  198. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_install.py +0 -0
  199. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_intake_hold.py +0 -0
  200. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_intake_hold_poller.py +0 -0
  201. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_issue_blockers.py +0 -0
  202. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_issue_outcome_announcement.py +0 -0
  203. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_job_claims.py +0 -0
  204. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_mattermost_handler.py +0 -0
  205. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_max_parallel.py +0 -0
  206. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_network.py +0 -0
  207. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_no_change_run.py +0 -0
  208. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_node_check.py +0 -0
  209. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_node_claims.py +0 -0
  210. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_notify_transport.py +0 -0
  211. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_obs.py +0 -0
  212. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_orphans.py +0 -0
  213. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_personalization.py +0 -0
  214. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_personalization_merge.py +0 -0
  215. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_pipeline_base.py +0 -0
  216. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_poller.py +0 -0
  217. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_poller_priority_order.py +0 -0
  218. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_poller_recheck.py +0 -0
  219. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_poller_search.py +0 -0
  220. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_post_merge.py +0 -0
  221. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_pr_verifier.py +0 -0
  222. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_pr_watcher.py +0 -0
  223. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_pre_dispatch_retry.py +0 -0
  224. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_proof.py +0 -0
  225. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_publish_failure_retry.py +0 -0
  226. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_question_backoff.py +0 -0
  227. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_redact.py +0 -0
  228. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_relay_alert_off_lock.py +0 -0
  229. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_relay_git_263.py +0 -0
  230. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_reopens.py +0 -0
  231. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_review_copy.py +0 -0
  232. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_review_gate.py +0 -0
  233. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_sandbox_visibility.py +0 -0
  234. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_scheduler.py +0 -0
  235. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_secops_integration.py +0 -0
  236. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_secops_operator_prs.py +0 -0
  237. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_self_update.py +0 -0
  238. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_setup.py +0 -0
  239. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_stuck.py +0 -0
  240. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_task_pipeline.py +0 -0
  241. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_telegram_handler.py +0 -0
  242. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_timeout_save.py +0 -0
  243. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_transport.py +0 -0
  244. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_transport_resolution.py +0 -0
  245. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_unreviewed_pr_announcement.py +0 -0
  246. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_usage.py +0 -0
  247. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_drift.py +0 -0
  248. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_health.py +0 -0
  249. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_links.py +0 -0
  250. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_numbers.py +0 -0
  251. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_peer_links.py +0 -0
  252. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_server.py +0 -0
  253. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_status.py +0 -0
  254. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_transcript.py +0 -0
  255. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_web_waiting.py +0 -0
  256. {ctrlrelay-0.27.0 → ctrlrelay-0.28.1}/tests/test_weekly_summary.py +0 -0
@@ -7,6 +7,220 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.28.1] - 2026-11-11
11
+
12
+ ### Fixed
13
+
14
+ - The relay's own clone, fetch and `ls-remote` now use gh's login on a node
15
+ where git has no credential for github.com (#398). The first dev run on
16
+ such a node failed before the agent started with `could not read
17
+ Username for 'https://github.com'`. `gh auth git-credential` is appended
18
+ as a helper for `https://github.com` through `GIT_CONFIG_COUNT`, after
19
+ any entries already in the environment, so a unit's `insteadOf` rewrite
20
+ keeps working and an operator's own helper is still asked first. These
21
+ calls also set `GIT_TERMINAL_PROMPT=0`, so a missing credential fails at
22
+ once. Pushes already used gh's login; the agent's environment is
23
+ unchanged.
24
+ - The checkpoint snippets in the dev, task and secops prompts, and in
25
+ `ctrlrelay agent check --resume`, no longer give an agent anything to
26
+ re-quote (#397). The BLOCKED and FAILED snippets (and secops' DONE) were a
27
+ `printf` built from single-quoted pieces joined by backslash
28
+ continuations. Agents sometimes rewrote the quoting or dropped a
29
+ continuation, and the round ended with a malformed or missing checkpoint
30
+ after the work was done. They now write the file with `python3` from a
31
+ quoted heredoc, the shape the DONE snippets already used. The text goes on
32
+ its own lines in a raw string and `json.dump` escapes it, so a question or
33
+ error may hold double quotes, newlines and backslashes; the old `%s`
34
+ pasted those into the JSON unescaped. One function renders the snippet for
35
+ every pipeline and for `agent check`, so the check runs exactly the shell
36
+ a real run uses.
37
+ - A run paused on a question is no longer abandoned half an hour later
38
+ while its issue is still open (#400). The hourly sweep that retires a
39
+ question once everything it cites is closed read `oaf-kernel#118` as
40
+ this repository's `#118`, found an unrelated issue closed days before,
41
+ and retired a live question 33 minutes after it was asked. A reference
42
+ written `owner/repo#N`, or linked, is now looked up in that repository;
43
+ one whose repository cannot be told (`name#N`, `PR#N`) keeps the
44
+ question; and the issue the question was asked on must be closed too.
45
+ Closing that issue now retires its question even when the question
46
+ cites nothing else. The 48-hour
47
+ `question_ttl_seconds` was never the cause.
48
+ - A question asked on an issue that expires unanswered now says on the
49
+ issue that the run has stopped and how to go on, and labels the issue
50
+ `ctrlrelay:unanswered` so it can be found. Adding it creates the label
51
+ in the repository if it is missing; nothing removes it. If the label
52
+ cannot be added, the comment still posts and does not mention it. A question retired
53
+ because its issue was closed is not labelled.
54
+
55
+ ## [0.28.0] - 2026-11-11
56
+
57
+ ### Added
58
+
59
+ - `ctrlrelay agent check <agent_type>` proves one backend writes its
60
+ checkpoint, without starting a dev run (#381). It spawns a single session
61
+ in a throwaway git repository in a temporary directory, with GitHub
62
+ credentials stripped and a prompt whose whole content is the `DONE`
63
+ checkpoint snippet, then reports the exit code, the checkpoint and its
64
+ status, the native session id the adapter captured, the selected identity
65
+ and the elapsed time — one line each, because they fail separately. Exits
66
+ 0 only on a `DONE` checkpoint from a session that exited on its own: a
67
+ CLI that exits zero having written nothing is the failure the command
68
+ exists to find, and one that writes `DONE` and then has to be stopped
69
+ would hang every dev run. The throwaway repository is a working
70
+ directory, not a sandbox. `--timeout` defaults to 300 seconds and must
71
+ be at least 1, `--json` prints the same report as one object including on
72
+ the failure paths — with the structured log moved to stderr so stdout is
73
+ exactly that object — and an unknown type exits non-zero naming every type
74
+ enrolled on the node. As an operator tool it may select any enrolled
75
+ backend and does not consult `agent.assignable_types`, which governs what
76
+ an assignment arriving from outside the machine may ask for.
77
+ - `ctrlrelay agent check <agent_type> --resume` proves the other half of the
78
+ contract: that the backend comes back knowing what it was doing (#382). A
79
+ backend that cannot resume fails every run that has to ask a question, and
80
+ the only way to exercise it before this was a real dev run that blocked and
81
+ waited for a chat answer. Round 1 is told a freshly generated 8-character
82
+ token and must write `BLOCKED_NEEDS_INPUT`; round 2 spawns with
83
+ `resume_session_id` set to the native id round 1 reported — the same argv a
84
+ pipeline resume builds — and must write `DONE` whose summary is that token.
85
+ The token is not in the second prompt, so a round that answered from its
86
+ own prompt could not pass, and a decoy fresh session with a different token
87
+ runs between the rounds, so a manifest that resumes the most recent session
88
+ instead of the id it is given fails rather than passing by luck. Round 1 must report both the blocked checkpoint
89
+ and a native session id or the command stops and says which was missing,
90
+ rather than spending a second fresh session that could only report a pass
91
+ for a backend that never resumed. A backend reporting no `resume`
92
+ capability fails with `resume not configured for <type>` without spawning.
93
+ A native session id that changes between rounds is a warning, not a
94
+ failure. Each round gets its own report line, each failure its own, and
95
+ `--json` carries both rounds plus `resume: true`.
96
+ - A plugin identity and capability contract for coding-agent CLIs (#379).
97
+ Claude and OpenCode now expose the same identity metadata, and local
98
+ `agent.plugins` manifests can enroll simple non-interactive CLIs with fixed
99
+ argv, prompt-over-argument or stdin, checkpoint handling, and optional
100
+ native-session mapping. `agent.assignable_types` defines the smaller set a
101
+ future control-plane assignment may select; assignments cannot supply a
102
+ binary or arbitrary arguments. The selected plugin, model, and canonical
103
+ invocation fingerprint are persisted before dispatch and pinned across CI
104
+ fixes, retries, and pending resumes. Generic executables must resolve to an
105
+ absolute path, native session IDs are validated before argv substitution,
106
+ and a pre-#379 native session with unknown identity is refused rather than
107
+ resumed through the current default backend.
108
+ - Repo context is linked under each backend's own instruction filename, so
109
+ Codex and Copilot see `AGENTS.md` or `.github/copilot-instructions.md`
110
+ instead of only Claude's `CLAUDE.md` (#379). A name the repository already
111
+ has is left alone: the repo's own file wins under that name, and the relay
112
+ context goes only under names that are free.
113
+ - Built-in identity pins use the stable launcher path, so Claude's normal
114
+ self-update does not invalidate a pending resume. Generic manifests still
115
+ pin their full invocation mapping (#379).
116
+ - A checked GitHub Copilot CLI manifest in `docs/configuration.md` and the
117
+ example config, with resume through its `sessionId`. It passes
118
+ `--disable-builtin-mcps`, because Copilot's built-in GitHub MCP server
119
+ otherwise gives the agent GitHub access under Copilot's own login,
120
+ bypassing credential stripping (#379).
121
+
122
+ - Copilot and Codex as built-in agent presets (#384, #385). Set
123
+ `repos[].agent_type: copilot` or `codex` (or `agent.type`) with no
124
+ `agent.plugins` entry. Their arguments are fixed in code, because some of them
125
+ are what make the backend safe unattended: Copilot's `--disable-builtin-mcps`
126
+ keeps its own GitHub login away from the agent. `agent.presets.<name>` may
127
+ change only `binary`, `model` and `default_timeout_seconds`. Both presets
128
+ run unconfined and refuse to start on a `sandbox: "bwrap"` node.
129
+ - Generic plugins can have their token usage measured, so the token budget
130
+ applies to them (#387). A manifest's `usage` mapping says which JSON-lines
131
+ event carries usage and where each count is. The reading is taken when the
132
+ CLI exits, a resumed round is charged only the difference from the
133
+ session's last total, and anything unreadable is recorded as unknown, never
134
+ zero. Both presets carry mappings measured against real output.
135
+ - Each agent backend receives only its own model-provider keys (#390). A
136
+ well-known provider variable such as `OPENAI_API_KEY` or
137
+ `ANTHROPIC_API_KEY` reaches an agent only if its backend allows it:
138
+ Claude gets `ANTHROPIC_*` and `CLAUDE_*`, the presets their own, and
139
+ `opencode` and plugins what `env_allow` lists (default none). The security
140
+ sweep keeps GitHub access but loses the relay's secrets and other backends'
141
+ keys.
142
+ - One agent backends table in the docs, with resume, usage, stuck detection
143
+ and sandbox for each backend, and how to log the Copilot and Codex CLIs in
144
+ for relay use (#386).
145
+
146
+ ### Fixed
147
+
148
+ - An agent can no longer outlive its session or stall the relay through
149
+ what it leaves behind (#394). Agents are spawned in their own session, so
150
+ each leads its own process group, and every exit path signals that group
151
+ rather than the direct child alone: after a normal exit and on a timeout
152
+ with SIGTERM, a 3-second grace, then SIGKILL; on cancellation, a stuck or
153
+ over-budget stop and a pipe error with an immediate SIGKILL, as the child
154
+ alone was before. A job the agent put in the background used
155
+ to keep running as the relay user after the run was reported done; and
156
+ because agents shared the relay's own process group, an agent that
157
+ signalled its group stopped the relay with it. The relay never signals a
158
+ group that is not provably the agent's own. The checkpoint read now opens
159
+ only a small regular file (`O_NOFOLLOW | O_NONBLOCK`, `fstat` on the same
160
+ descriptor, a 256 KiB cap, and the `.ctrlrelay` directory itself opened
161
+ without following a symlink, so an agent cannot point it at another run's
162
+ and have the relay read — and delete — that run's checkpoint): a FIFO at the state path blocked the read
163
+ forever, past the session timeout and with every other run on the event
164
+ loop stalled behind it, and a huge file or a symlink to `/dev/zero` was
165
+ read whole. A checkpoint that breaks the JSON parser (an integer past the
166
+ int-string limit, deep nesting, bad UTF-8) is now an unreadable checkpoint
167
+ rather than an exception. Known limit: a process group does not contain a
168
+ process that leaves it on purpose — one that calls `setsid` or
169
+ `setpgid` survives without a sandbox. Under `sandbox: "bwrap"` the PID
170
+ namespace ends it with the sandbox; full containment for the other
171
+ backends is #352. A narrow race also remains: if the agent's group id is
172
+ recycled for another group during a session and that group's leader has
173
+ exited too, the relay cannot tell the two apart and may signal it.
174
+ - An agent no longer inherits the daemon's stdin; it gets `/dev/null`
175
+ unless its prompt is sent on stdin. `codex exec` appends piped stdin to
176
+ its prompt and waits for end-of-file, so with the daemon's stdin an open
177
+ pipe every Codex run sat until the timeout without starting (#379).
178
+ - A repository's own instruction link that points outside the checkout,
179
+ such as `AGENTS.md -> /opt/shared/AGENTS.md`, was taken for the relay's
180
+ context link: it was replaced while the agent ran and deleted at cleanup.
181
+ Only links to the repo's context file are now replaced or removed (#379).
182
+ - When the daemon's `PATH` held a relative entry (`.`, an empty entry,
183
+ `bin`), a file committed to the repository could run in place of a bare
184
+ agent `binary`, of an `#!/usr/bin/env` interpreter, or of a command the
185
+ agent ran, because the agent starts inside the worktree. Agents now start
186
+ with relative `PATH` entries removed, sandboxed or not. This predates the
187
+ plugin runtime.
188
+ - Context-link cleanup walks the worktree without following symlinks, so a
189
+ directory swapped for a link during a run cannot redirect it outside the
190
+ checkout (#379).
191
+ - The relay no longer replaces a repository's own `CLAUDE.md` with its repo
192
+ context link. The tracked file became a type change while the agent ran
193
+ and a deletion after cleanup, so an agent that staged everything could put
194
+ either into its pull request. Cleanup now removes only links the relay
195
+ made (#379).
196
+ - A plugin listed under `agent.plugins` but not routed to by any repo or by
197
+ `agent.assignable_types` is built only when a pinned run selects it, so
198
+ one that cannot run under `agent.sandbox: bwrap` no longer stops the
199
+ daemon at startup. A routed one still does, naming where it was routed
200
+ from. A backend that fails to build on demand is refused for that run
201
+ instead of raising out of the pipeline (#379).
202
+
203
+ ### Upgrade notes
204
+
205
+ - `agent.plugins.copilot` and `agent.plugins.codex` are now refused at load,
206
+ because those names are built-in presets (#384, #385). Remove the entry and
207
+ set `agent.presets.<name>` if you changed its binary, model or timeout.
208
+ - An `opencode` setup, or a plugin, that reads a model-provider key from the
209
+ environment now needs that key listed in its `env_allow` (#390).
210
+ - On Linux, log the Copilot CLI in with the keyring out of reach
211
+ (`DBUS_SESSION_BUS_ADDRESS=disabled: copilot`, then `/login`) or set
212
+ `COPILOT_GITHUB_TOKEN`: the relay blocks the desktop keyring, so a
213
+ keyring-stored login cannot be read.
214
+ - A run waiting on a question from before this release has no recorded
215
+ agent identity. If its agent session had already started, it is refused
216
+ with `agent_identity_unknown` instead of being resumed on whatever backend
217
+ is configured now. Answer or close those questions before upgrading, or
218
+ re-run the issue afterwards (#379).
219
+ - A repo that commits its own `CLAUDE.md` now runs with that file instead of
220
+ the relay's context under that name. Move anything the agent must see into
221
+ the repo's file, or into the contexts entry for a name the repo does not
222
+ use (#379).
223
+
10
224
  ## [0.27.0] - 2026-11-06
11
225
 
12
226
  ### Added
@@ -2650,7 +2864,9 @@ pipeline).
2650
2864
  per-phase implementation plans (Phase 0 through Phase 4).
2651
2865
  - `docs/Claude_Code_Project_Guide.md` — project development guide.
2652
2866
 
2653
- [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.27.0...HEAD
2867
+ [Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.28.1...HEAD
2868
+ [0.28.1]: https://github.com/AInvirion/ctrlrelay/compare/v0.28.0...v0.28.1
2869
+ [0.28.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.27.0...v0.28.0
2654
2870
  [0.27.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.26.0...v0.27.0
2655
2871
  [0.26.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.25.0...v0.26.0
2656
2872
  [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.1
4
4
  Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
5
  Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
6
  Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
@@ -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)