coderouter-cli 2.11.0__tar.gz → 2.11.2__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 (296) hide show
  1. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/.gitignore +5 -0
  2. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/CHANGELOG.md +279 -1
  3. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/PKG-INFO +1 -1
  4. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/anthropic_native.py +23 -5
  5. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/cli.py +49 -0
  6. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/loader.py +65 -0
  7. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/schemas.py +85 -3
  8. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/doctor.py +98 -19
  9. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/doctor_apply.py +72 -3
  10. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/gguf_introspect.py +132 -25
  11. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/drift_detection.py +13 -1
  12. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/launcher_routes.py +3 -3
  13. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/launcher_devices.py +21 -9
  14. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/fallback.py +14 -2
  15. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/anthropic.py +14 -1
  16. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/convert.py +7 -0
  17. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/tool_repair.py +193 -76
  18. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/vscode_init.py +172 -16
  19. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/launcher-multi-build.md +17 -2
  20. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/vscode.md +26 -0
  21. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.llamacpp-vllm.yaml +105 -10
  22. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/pyproject.toml +1 -1
  23. coderouter_cli-2.11.2/tests/test_config_cwd_warning.py +268 -0
  24. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_doctor.py +278 -0
  25. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_doctor_apply.py +281 -0
  26. coderouter_cli-2.11.2/tests/test_examples_yaml_roundtrip.py +150 -0
  27. coderouter_cli-2.11.2/tests/test_gguf_introspect.py +292 -0
  28. coderouter_cli-2.11.2/tests/test_import_hygiene.py +118 -0
  29. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_backend_variants.py +35 -9
  30. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_devices_routes.py +23 -0
  31. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_gui_devices.py +4 -2
  32. coderouter_cli-2.11.2/tests/test_launcher_gui_events.py +498 -0
  33. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_gui_readiness.py +17 -6
  34. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_gui.py +4 -2
  35. coderouter_cli-2.11.2/tests/test_stop_reason_forward_compat.py +179 -0
  36. coderouter_cli-2.11.2/tests/test_tool_repair_differential.py +326 -0
  37. coderouter_cli-2.11.2/tests/test_tool_repair_scanners.py +169 -0
  38. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_vscode_init.py +180 -0
  39. coderouter_cli-2.11.0/tests/test_gguf_introspect.py +0 -164
  40. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/LICENSE +0 -0
  41. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/README.en.md +0 -0
  42. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/README.md +0 -0
  43. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/__init__.py +0 -0
  44. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/__main__.py +0 -0
  45. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/__init__.py +0 -0
  46. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/base.py +0 -0
  47. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/openai_compat.py +0 -0
  48. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/adapters/registry.py +0 -0
  49. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/cli_stats.py +0 -0
  50. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/__init__.py +0 -0
  51. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/capability_registry.py +0 -0
  52. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/config/env_file.py +0 -0
  53. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/cost.py +0 -0
  54. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/data/__init__.py +0 -0
  55. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/data/model-capabilities.yaml +0 -0
  56. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/env_security.py +0 -0
  57. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/errors.py +0 -0
  58. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/__init__.py +0 -0
  59. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/_fingerprint.py +0 -0
  60. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/backend_health.py +0 -0
  61. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/context_budget.py +0 -0
  62. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/continuous_probe.py +0 -0
  63. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/drift_actions.py +0 -0
  64. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/memory_budget.py +0 -0
  65. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/memory_pressure.py +0 -0
  66. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/self_healing.py +0 -0
  67. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/guards/tool_loop.py +0 -0
  68. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/hardware.py +0 -0
  69. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/__init__.py +0 -0
  70. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/anthropic_routes.py +0 -0
  71. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/app.py +0 -0
  72. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/dashboard_routes.py +0 -0
  73. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/metrics_routes.py +0 -0
  74. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/ingress/openai_routes.py +0 -0
  75. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/language_tax.py +0 -0
  76. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/launcher_speculative.py +0 -0
  77. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/launcher_swap.py +0 -0
  78. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/logging.py +0 -0
  79. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/metrics/__init__.py +0 -0
  80. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/metrics/collector.py +0 -0
  81. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/metrics/prometheus.py +0 -0
  82. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/output_filters.py +0 -0
  83. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/__init__.py +0 -0
  84. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/base.py +0 -0
  85. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/loader.py +0 -0
  86. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/plugins/registry.py +0 -0
  87. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/__init__.py +0 -0
  88. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/adaptive.py +0 -0
  89. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/auto_router.py +0 -0
  90. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/budget.py +0 -0
  91. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/routing/capability.py +0 -0
  92. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/__init__.py +0 -0
  93. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/audit_log.py +0 -0
  94. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/replay.py +0 -0
  95. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/request_log.py +0 -0
  96. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/store.py +0 -0
  97. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/state/suggest_rules.py +0 -0
  98. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/token_estimation.py +0 -0
  99. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/token_estimation_accurate.py +0 -0
  100. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/coderouter/translation/__init__.py +0 -0
  101. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/README.en.md +0 -0
  102. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/README.md +0 -0
  103. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/assets/dashboard-demo.png +0 -0
  104. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  105. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  106. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/external-agents.en.md +0 -0
  107. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/external-agents.md +0 -0
  108. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/gguf_dl.en.md +0 -0
  109. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/gguf_dl.md +0 -0
  110. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/hf-ollama-models.en.md +0 -0
  111. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/hf-ollama-models.md +0 -0
  112. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/install-backends.en.md +0 -0
  113. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/install-backends.md +0 -0
  114. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher-quickstart.en.md +0 -0
  115. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher-quickstart.md +0 -0
  116. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher.en.md +0 -0
  117. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/launcher.md +0 -0
  118. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/llamacpp-direct.en.md +0 -0
  119. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/llamacpp-direct.md +0 -0
  120. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/lmstudio-direct.en.md +0 -0
  121. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/lmstudio-direct.md +0 -0
  122. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/backends/verify-ollama-0.23.1.md +0 -0
  123. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/architecture.en.md +0 -0
  124. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/architecture.md +0 -0
  125. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/context-budget.en.md +0 -0
  126. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/context-budget.md +0 -0
  127. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/continuous-probing.en.md +0 -0
  128. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/continuous-probing.md +0 -0
  129. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/drift-detection.en.md +0 -0
  130. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/drift-detection.md +0 -0
  131. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/partial-stitch.en.md +0 -0
  132. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/concepts/partial-stitch.md +0 -0
  133. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  134. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/external-agents-adapter.md +0 -0
  135. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/launcher-model-swap.md +0 -0
  136. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/orchestration-companion.md +0 -0
  137. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  138. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/v1.6-auto-router-verification.md +0 -0
  139. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/designs/v1.6-auto-router.md +0 -0
  140. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/free-tier-guide.en.md +0 -0
  141. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/free-tier-guide.md +0 -0
  142. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/language-tax.en.md +0 -0
  143. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/language-tax.md +0 -0
  144. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/remote-access.en.md +0 -0
  145. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/remote-access.md +0 -0
  146. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/security.en.md +0 -0
  147. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/security.md +0 -0
  148. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/subagent-routing.en.md +0 -0
  149. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/subagent-routing.md +0 -0
  150. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/troubleshooting.en.md +0 -0
  151. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/troubleshooting.md +0 -0
  152. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/usage-guide.en.md +0 -0
  153. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/guides/usage-guide.md +0 -0
  154. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/low-memory-integration.en.md +0 -0
  155. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/low-memory-integration.md +0 -0
  156. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/openrouter-roster/CHANGES.md +0 -0
  157. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/openrouter-roster/README.md +0 -0
  158. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/openrouter-roster/latest.json +0 -0
  159. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.4.md +0 -0
  160. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.5-verify.md +0 -0
  161. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.5.md +0 -0
  162. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.6.md +0 -0
  163. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v0.7.md +0 -0
  164. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v1.0-verify.md +0 -0
  165. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/retrospectives/v1.0.md +0 -0
  166. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/quickstart.en.md +0 -0
  167. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/quickstart.md +0 -0
  168. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  169. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/docs/start/when-do-i-need-coderouter.md +0 -0
  170. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/.env.example +0 -0
  171. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/README.md +0 -0
  172. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers-agent-cli.yaml +0 -0
  173. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers-multiagent.yaml +0 -0
  174. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.context-budget-test.yaml +0 -0
  175. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.nvidia-nim.yaml +0 -0
  176. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.ollama-auto-custom.yaml +0 -0
  177. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.ollama-auto.yaml +0 -0
  178. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.ollama-free-chain.yaml +0 -0
  179. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.raspberrypi.yaml +0 -0
  180. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.swap.yaml +0 -0
  181. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/examples/providers.yaml +0 -0
  182. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/demo_traffic.sh +0 -0
  183. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/openrouter_roster_diff.py +0 -0
  184. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/smoke_v2_2.sh +0 -0
  185. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify-providers.yaml +0 -0
  186. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify_ollama_0_23.py +0 -0
  187. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify_v0_5.sh +0 -0
  188. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/scripts/verify_v1_0.sh +0 -0
  189. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/__init__.py +0 -0
  190. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/conftest.py +0 -0
  191. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_adapter_anthropic.py +0 -0
  192. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_agent_cli.py +0 -0
  193. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_audit_log.py +0 -0
  194. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_auto_router.py +0 -0
  195. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_auto_router_cjk.py +0 -0
  196. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_backend_health.py +0 -0
  197. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_budget.py +0 -0
  198. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability.py +0 -0
  199. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_degraded_payload.py +0 -0
  200. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_registry.py +0 -0
  201. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_registry_cache_control.py +0 -0
  202. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_capability_tool_choice.py +0 -0
  203. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_claude_code_suitability.py +0 -0
  204. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_cli.py +0 -0
  205. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_cli_bind_warning.py +0 -0
  206. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_cli_stats.py +0 -0
  207. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_config.py +0 -0
  208. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_config_shim_actions.py +0 -0
  209. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_context_budget.py +0 -0
  210. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_continuous_probe.py +0 -0
  211. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_dashboard_endpoint.py +0 -0
  212. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_doctor_cache_probe.py +0 -0
  213. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_drift_actions.py +0 -0
  214. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_drift_detection.py +0 -0
  215. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_drift_detection_integration.py +0 -0
  216. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_env_file.py +0 -0
  217. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_env_security.py +0 -0
  218. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_errors.py +0 -0
  219. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_examples_yaml.py +0 -0
  220. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback.py +0 -0
  221. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_anthropic.py +0 -0
  222. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_cache_control.py +0 -0
  223. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_cache_observed.py +0 -0
  224. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_cache_strip.py +0 -0
  225. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_empty_response.py +0 -0
  226. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_misconfig_warn.py +0 -0
  227. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_paid_gate.py +0 -0
  228. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_thinking.py +0 -0
  229. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fallback_tool_choice_emulate.py +0 -0
  230. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h1_prometheus_drift.py +0 -0
  231. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h2_h6_translation.py +0 -0
  232. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h3_client_reuse.py +0 -0
  233. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h4_h5_guards.py +0 -0
  234. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h7_probe_restore.py +0 -0
  235. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_h8_launcher_auth.py +0 -0
  236. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mA_routing.py +0 -0
  237. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mB_adapters.py +0 -0
  238. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mC_convert.py +0 -0
  239. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mD_logging_metrics.py +0 -0
  240. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mE_config_validation.py +0 -0
  241. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_fix_mF_ingress.py +0 -0
  242. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_guards_tool_loop.py +0 -0
  243. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_hardware.py +0 -0
  244. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_ingress_anthropic.py +0 -0
  245. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_ingress_count_tokens.py +0 -0
  246. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_ingress_profile.py +0 -0
  247. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_language_tax.py +0 -0
  248. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_language_tax_integration.py +0 -0
  249. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_config_bench.py +0 -0
  250. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_devices.py +0 -0
  251. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_devices_detect.py +0 -0
  252. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_mtp.py +0 -0
  253. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_mtp_fallback.py +0 -0
  254. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_provider_sync.py +0 -0
  255. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_readiness_restart.py +0 -0
  256. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_swap.py +0 -0
  257. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_swap_review.py +0 -0
  258. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_config.py +0 -0
  259. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_routes.py +0 -0
  260. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_launcher_variant_sweep.py +0 -0
  261. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_memory_budget.py +0 -0
  262. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_memory_pressure.py +0 -0
  263. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_cache.py +0 -0
  264. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_collector.py +0 -0
  265. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_cost.py +0 -0
  266. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_endpoint.py +0 -0
  267. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_jsonl.py +0 -0
  268. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_prometheus.py +0 -0
  269. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_metrics_prometheus_cache.py +0 -0
  270. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_models_passthrough.py +0 -0
  271. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_openai_compat.py +0 -0
  272. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_openrouter_roster_diff.py +0 -0
  273. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_output_filters.py +0 -0
  274. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_output_filters_adapters.py +0 -0
  275. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_partial_stitch.py +0 -0
  276. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugin_adapter.py +0 -0
  277. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugins_integration.py +0 -0
  278. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugins_loader.py +0 -0
  279. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_plugins_registry.py +0 -0
  280. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_reasoning_strip.py +0 -0
  281. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_repair_byte_fallback.py +0 -0
  282. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_request_log.py +0 -0
  283. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_role_normalization.py +0 -0
  284. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_routing_adaptive.py +0 -0
  285. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_self_healing.py +0 -0
  286. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_setup_sh.py +0 -0
  287. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_state_store.py +0 -0
  288. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_token_estimation.py +0 -0
  289. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_token_estimation_accurate.py +0 -0
  290. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tokens_saved_metric.py +0 -0
  291. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tool_repair.py +0 -0
  292. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tool_repair_lenient.py +0 -0
  293. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_tool_repair_r4.py +0 -0
  294. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_toolrepair_bench.py +0 -0
  295. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_translation_anthropic.py +0 -0
  296. {coderouter_cli-2.11.0 → coderouter_cli-2.11.2}/tests/test_translation_reverse.py +0 -0
@@ -9,6 +9,11 @@ secrets.env
9
9
  ~/.coderouter/
10
10
  CLAUDE.md
11
11
 
12
+ # *.bak — pre-overwrite backups of the files above (e.g. `vscode-init --force`
13
+ # saves `.envrc.bak` before clobbering `.envrc`; `doctor --apply` saves
14
+ # `providers.yaml.bak`). Same secret-risk as the originals, so same rule.
15
+ *.bak
16
+
12
17
  # ============================================================
13
18
  # Python
14
19
  # ============================================================
@@ -9,6 +9,280 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.11.2] — 2026-08-04 (file-write safety and launcher event hygiene)
13
+
14
+ Two fixes from the same review round as v2.11.1, split out because they
15
+ rewrite `_atomic_write` and the launcher GUI's event plumbing and deserved
16
+ their own rollback unit. **No breaking changes**: no config-schema field
17
+ changed, no CLI flag changed meaning, `--force` still replaces `.envrc`.
18
+ Existing `.vscode/settings.json` merge semantics are unchanged.
19
+
20
+ ### Fixed
21
+
22
+ - **`vscode-init --force` destroyed `.envrc` with no way back.** The
23
+ command replaced the file outright — no merge, no backup — so a
24
+ hand-added `source_env_if_exists .envrc.local`, the very pattern
25
+ `docs/guides/vscode.md` recommends, vanished silently. direnv then
26
+ looked healthy while exporting nothing, which is a hard failure to
27
+ trace back to its cause. The conflict message said only "Re-run with
28
+ `--force` to overwrite" and never mentioned that the whole file goes.
29
+ The previous contents are now copied to `.envrc.bak` before the write,
30
+ the path is reported in the command's output, and the conflict message
31
+ spells out that this is a whole-file replacement rather than a merge.
32
+ `--dry-run` makes no backup. Only one generation is kept, so a second
33
+ `--force` overwrites the first `.bak` (`vscode_init.py`).
34
+ - **The launcher GUI could lose track of a running backend because of
35
+ that backend's own log output.** `_log_queue` carried control events
36
+ and raw child stdout in the same channel, with the drain loop
37
+ dispatching on `line.startswith("_ERR_:")` and friends. A child line
38
+ beginning `_ERR_:` ran `del self.processes[proc_id]`, so a live
39
+ llama-server dropped out of the process table — unstoppable from the
40
+ UI, missed by the shutdown handler, holding its port and VRAM until
41
+ killed by hand. A line beginning `_SPAWNED_:` with too few colons
42
+ raised `ValueError` inside the drain loop and discarded that tick's
43
+ log processing entirely. This is reachable in ordinary use: llama.cpp
44
+ echoes GGUF metadata such as the chat template verbatim at startup,
45
+ and `extra_args` wrappers put arbitrary text on the same stream.
46
+ Events are now `(kind, proc_id, payload)` with `kind` in
47
+ `log / spawned / ready / error / devices`; raw child output is
48
+ enqueued unconditionally as `log` and never inspected for markers.
49
+ `_cr_log_queue` got the same treatment, the `"_DEVICES_"` proc_id
50
+ sentinel is gone, and a malformed control payload now skips its own
51
+ event instead of killing the loop. While here, `_SPAWNED_` parsing
52
+ moved from `split(":", 2)` to `rpartition`, so model names containing
53
+ a colon no longer mangle the port (`launcher_gui.py`).
54
+
55
+ ### Security
56
+
57
+ - **`vscode-init` widened the permissions of the files it rewrote.**
58
+ `_atomic_write` created its temp file under the process umask and
59
+ `os.replace`d it over the target, so an `.envrc` hardened to `0600` —
60
+ a file that holds `ANTHROPIC_AUTH_TOKEN` — came back `0644`. The temp
61
+ path was also fully predictable (`.envrc.tmp`, `settings.json.tmp`),
62
+ so a pre-planted symlink at that path was followed and its target
63
+ overwritten (CWE-377), and two concurrent runs in one workspace raced
64
+ on the same name. Writes now go through `tempfile.mkstemp` with
65
+ `O_EXCL`, are `fsync`ed, restore the original mode before the rename,
66
+ and clean up the temp file on failure. A newly created `.envrc` is
67
+ written `0600`, matching the standard `coderouter doctor --check-env`
68
+ already enforces for credential-bearing files; an existing file keeps
69
+ whatever mode the operator set. Mode handling is POSIX-only —
70
+ `os.chmod` on Windows only toggles the read-only bit and is skipped.
71
+ `.vscode/settings.json` is backed up before any rewrite too, not just
72
+ under `--force`, since the merge path also edits it in place
73
+ (`vscode_init.py`).
74
+
75
+ ### Changed
76
+
77
+ - `.gitignore` now excludes `*.bak`, so the backups above and
78
+ `providers.yaml.bak` from `doctor --apply` stay out of commits. They
79
+ carry the same secrets as the files they shadow.
80
+ - Three GUI test modules documented their `pytest.importorskip("tkinter")`
81
+ as a CI workaround, stating that the uv-managed Python on CI has no
82
+ tkinter and that these tests are therefore skipped there. Measured on
83
+ 2026-08-04: `uv python install` ships Tk 8.6 on both 3.12.11 and
84
+ 3.13.7, and all 93 cases in those files run and pass on CI. The
85
+ docstrings now say so, and record that the guards stay for local
86
+ system pythons built without Tk — which do exist.
87
+
88
+ ### Tests
89
+
90
+ - 1 new file, 38 new cases (`test_launcher_gui_events.py`, plus
91
+ additions to `test_vscode_init.py`). Suite: 2345 passed, 1 skipped,
92
+ ruff clean.
93
+ - Each fix has a regression test confirmed to fail against the pre-fix
94
+ tree — 10 for the write path, 7 for the event path. The headline one
95
+ asserts a live process survives its own `_ERR_:` stdout line; on the
96
+ old code `app.processes` comes back empty. Another plants a symlink at
97
+ the predictable temp path and asserts its target is untouched.
98
+
99
+ ---
100
+
101
+ ## [v2.11.1] — 2026-08-04 (second-round review: correctness & hardening)
102
+
103
+ Seven fixes from a full-source review of the 36k-line package, plus two
104
+ deprecation warnings that pre-announce the behaviour changes queued for
105
+ v2.12.0. **No breaking changes**: no config-schema field was removed or
106
+ narrowed, no CLI flag changed meaning, and every default is unchanged.
107
+ The two new warnings do not alter behaviour — they only tell you which
108
+ of your settings will need editing before v2.12.0.
109
+
110
+ ### Fixed
111
+
112
+ - **Brace scanning in `tool_repair` was quadratic and blocked the event
113
+ loop for minutes.** `_find_balanced_json_objects` and
114
+ `_find_candidate_object_spans` restarted a full scan to end-of-text at
115
+ every `{`, so an assistant reply carrying unclosed braces — the
116
+ ordinary shape of a deep nested JSON truncated by `max_tokens` — cost
117
+ O(k·n). Measured on the old code: 3.997 s for 8 KiB of `{`, 22.199 s
118
+ for a 48 KiB reply, 139.019 s for 48 KiB of bare `{`. Both scanners
119
+ are called synchronously from `to_anthropic_response`, which
120
+ `FallbackEngine._generate_anthropic_impl` / `._stream_anthropic_impl`
121
+ invoke from inside `async` handlers, so the whole server stalled for
122
+ the duration — every concurrent request, not just the offending one.
123
+ Replaced with a single right-to-left sweep that builds a depth-0
124
+ closing-brace table (`table[j]` = the first depth-0 `}` reached when
125
+ scanning from offset `j` outside any string); the answer for an
126
+ opening brace at `q` is `table[q+1]`, which is by definition the
127
+ fresh-restart semantics the old code had. Same inputs now take
128
+ 0.004 s / 0.016 s / 0.024 s. The strict (`"` only) and lenient (`'`
129
+ too) quoting rules live in separate tables and cannot bleed into each
130
+ other. Scan output is unchanged — see Tests. Added
131
+ `_MAX_BARE_SCAN_CHARS` (256 KiB); above it the bare-JSON pass is
132
+ skipped with a `tool-repair-input-too-large` warning while the fenced
133
+ / XML / R4c passes still run. The two `to_anthropic_response` call
134
+ sites moved to `asyncio.to_thread`; the function itself stays
135
+ synchronous (`tool_repair.py`, `routing/fallback.py`).
136
+ - **`doctor --apply` dropped `output_filters` a user had configured.**
137
+ The probe emitted only the filters that were *missing*, but
138
+ `deep_merge_dicts` replaces lists wholesale, so applying
139
+ `output_filters: [strip_thinking]` to a provider already carrying
140
+ `[strip_stop_markers, my_filter]` left it with `[strip_thinking]`
141
+ alone. `strip_tool_call_xml` and `repair_byte_fallback` were dropped
142
+ 100% of the time, since no probe ever recommends them. The patch now
143
+ carries the union — existing chain first, missing filters appended.
144
+ Order is load-bearing (`OutputFilterChain.feed` applies left to
145
+ right), so the union never sorts; `deep_merge_dicts` semantics are
146
+ untouched (`doctor.py`).
147
+ - **`doctor --check-model` could write `capabilities: tools: false` for
148
+ a model that supports tools.** The tool-call probe judged on
149
+ `content[:200]` — a display-oriented truncation — while the probe
150
+ budget is 256 tokens, or 1024 for thinking models (roughly 3–4 k
151
+ chars). A model that emits a preamble before its tool JSON, which is
152
+ exactly what thinking models do, fell through to the "nothing
153
+ tool-shaped at all" branch and got a `tools: false` patch that
154
+ `--apply` then wrote. This re-created, by a different route, the
155
+ false positive the v1.8.3 budget increase was meant to remove.
156
+ Detection now reads the full body up to `_TOOL_PROBE_SCAN_CHARS`
157
+ (16 KiB) and the 200-char slice is display-only; the detail line
158
+ reports how many chars were actually scanned (`doctor.py`).
159
+ - **Stray `stop_reason` values from Anthropic were treated as malformed
160
+ responses.** `AnthropicResponse.stop_reason` was a closed `Literal`,
161
+ and `ConfigDict(extra="allow")` does not widen a declared field, so
162
+ `pause_turn` (server-tool turns), `refusal` and
163
+ `model_context_window_exceeded` raised `ValidationError`.
164
+ `AnthropicAdapter` converts that to a retryable `AdapterError`, so a
165
+ perfectly good — and already billed — reply was discarded and the
166
+ chain fell through to the next provider, or to
167
+ `NoProvidersAvailableError` if it was the last. Streaming was
168
+ unaffected (`AnthropicStreamEvent` keeps its payload as a plain
169
+ dict), so only non-streaming requests were hit. The field is now
170
+ `str | None`, deliberately open: Anthropic has added stop reasons
171
+ three times and a closed `Literal` reproduces this bug on the next
172
+ one. `_REVERSE_FINISH_REASON_MAP` gained `pause_turn` → `stop`,
173
+ `refusal` → `content_filter`, `model_context_window_exceeded` →
174
+ `length`. While here, `drift_detection._EXPECTED_STOP` gained
175
+ `stop_sequence` — it was counting a documented, ordinary stop reason
176
+ as an anomaly (`translation/anthropic.py`, `translation/convert.py`,
177
+ `guards/drift_detection.py`).
178
+ - **`import coderouter.translation` failed on its own.** The package
179
+ reached `adapters` → `registry` → `anthropic_native`, which imported
180
+ `translation.convert` back at module scope. `pytest tests/` passed
181
+ only because alphabetical collection imports `test_adapter_anthropic`
182
+ first and primes `coderouter.adapters`; running
183
+ `pytest tests/test_tool_repair.py` on its own was a collection error.
184
+ The three `convert` symbols moved into the `generate()` / `stream()`
185
+ bodies that use them (`adapters/anthropic_native.py`).
186
+
187
+ ### Security
188
+
189
+ - **`doctor --apply` silently rewrote `providers.yaml` while reporting
190
+ "already up to date", then destroyed the original in `.bak`.** The
191
+ write was gated on `diff_text` — "does a ruamel re-dump differ from
192
+ what is on disk" — not on whether a patch changed anything. ruamel
193
+ re-flows quoted scalars at 80 columns and renders an explicit `null`
194
+ as an empty scalar, so a fully idempotent re-run still produced a
195
+ diff and still wrote. The CLI meanwhile took the `is_no_op` early
196
+ return and printed `All N patch(es) already applied`, showing neither
197
+ the diff nor a backup line. The *next* `--apply` then copied that
198
+ already-reformatted text over `providers.yaml.bak`, which is
199
+ deliberately un-timestamped and single-slot — so the only copy of the
200
+ operator's original was gone. The shipped `examples/providers.yaml`
201
+ is affected (five `api_key_env: null` lines), as is
202
+ `examples/providers.llamacpp-vllm.yaml` (two `binary: null`). The
203
+ write is now gated on the merge result per target file; a target
204
+ whose merges were all no-ops reports an empty diff and parks the
205
+ cosmetic delta in `ApplyResult.reformat_only`, surfaced under
206
+ `--dry-run` only. The dumper also sets `width = 4096`, as defence in
207
+ depth. Every exit path of the apply command — including both
208
+ exception returns — now states how many files were written.
209
+ **If you ran `doctor --apply` on v2.11.0 or earlier, compare
210
+ `providers.yaml` against `providers.yaml.bak` once.** The
211
+ reformatting is semantically equivalent YAML, but the first `--apply`
212
+ after upgrading will show the un-wrapping alongside your intended
213
+ change; that is expected and happens once (`doctor_apply.py`,
214
+ `cli.py`).
215
+ - **A malformed GGUF file could hang or crash the launcher, including
216
+ from the HTTP route.** `_skip_value` recursed once per array nesting
217
+ level at 12 bytes per level, so a ~12 KB file raised `RecursionError`
218
+ — which is not `GGUFParseError`, so it went straight through
219
+ `try_read_gguf_metadata`'s handler. Separately, array elements were
220
+ skipped with `fh.seek(size, 1)` per element with no end-of-file
221
+ check: a 49-byte file declaring `count = 1<<24` spun 16.7 M times,
222
+ took 7.94 s, and returned a `GGUFInfo` rather than raising. Both are
223
+ reachable from `POST /api/launcher/start`, and
224
+ `launcher_speculative.find_draft_companion` reads *every* `.gguf` in
225
+ the configured model directories, so one bad file poisons the scan.
226
+ `_skip_value` is now iterative over an explicit work stack —
227
+ `RecursionError` is structurally unreachable — with
228
+ `_MAX_ARRAY_DEPTH = 8` retained as an independent bound (real GGUF
229
+ arrays are depth 1; llama.cpp's writer emits nothing deeper). Every
230
+ skip validates against the file size before seeking, and fixed-width
231
+ scalar arrays are skipped in one bulk seek: a 150 k-element `u32`
232
+ array went from 0.0624 s to 0.000102 s, and the 49-byte case from
233
+ 7.94 s to 0.000043 s with a `GGUFParseError`. `p.open()` is now
234
+ wrapped for the stat/open TOCTOU, and `try_read_gguf_metadata`
235
+ catches `OSError` / `RecursionError` / `MemoryError` as well
236
+ (`gguf_introspect.py`).
237
+
238
+ ### Changed
239
+
240
+ - **Deprecation warning: `providers.yaml` discovered in the current
241
+ directory.** When neither `--config` nor `CODEROUTER_CONFIG` resolves
242
+ and the file actually loaded came from `./providers.yaml`, a
243
+ `cwd-config-loaded` warning is logged once per process naming the
244
+ path. In v2.12.0 this search step becomes opt-in behind
245
+ `CODEROUTER_ALLOW_CWD_CONFIG`, because starting the server in an
246
+ untrusted directory currently means loading that directory's
247
+ execution-bearing settings. Behaviour is unchanged in this release
248
+ (`config/loader.py`).
249
+ - **Deprecation warning: shell syntax in `restart_command`.** A value
250
+ containing `&&`, `||`, `|`, `;`, `>`, `<`, `$(` or a backtick, or
251
+ starting with `~/` or `FOO=bar`, now logs
252
+ `restart-command-shell-syntax` at config load. In v2.12.0 the command
253
+ will be `shlex.split` and run with `shell=False`, so those forms
254
+ break — and `pkill x && x` breaks *silently*, with `&&` passed to
255
+ `pkill` as an argument. Wrap such commands in a script, or use an
256
+ explicit `/bin/sh -c "..."`. This is a warning only; nothing is
257
+ rejected and execution is unchanged (`config/schemas.py`).
258
+
259
+ ### Tests
260
+
261
+ - 6 new files, 184 new cases (`test_tool_repair_differential.py`,
262
+ `test_tool_repair_scanners.py`, `test_examples_yaml_roundtrip.py`,
263
+ `test_import_hygiene.py`, `test_stop_reason_forward_compat.py`,
264
+ `test_config_cwd_warning.py`, plus additions to `test_doctor.py`,
265
+ `test_doctor_apply.py` and `test_gguf_introspect.py`). Suite: 2307
266
+ passed, 1 skipped, ruff clean.
267
+ - The scanner rewrite is pinned by a differential test that carries the
268
+ old quadratic algorithm as a reference implementation and compares
269
+ both scanners across five random alphabets × 2500 inputs each, plus
270
+ the full `benchmarks/tool-repair` corpus end to end. A naive global
271
+ single-pass rewrite — the obvious way to linearise this, and the way
272
+ that looks correct — diverges on 6870 of 12500 strict cases and 6977
273
+ lenient, because an apostrophe in ordinary prose (`I'll`, `don't`)
274
+ opens a single-quoted string that swallows every following brace. The
275
+ differential test was verified to catch exactly that.
276
+ - Each fix above has a regression test confirmed to fail against the
277
+ pre-fix tree, including
278
+ `test_cli_no_op_message_states_nothing_written` (the "up to date"
279
+ message printed while writing),
280
+ `test_no_op_apply_leaves_example_byte_identical` (the shipped
281
+ examples), `test_array_count_past_eof_raises_immediately` (raises,
282
+ and inside 0.5 s) and `test_lenient_survives_prose_apostrophe`.
283
+
284
+ ---
285
+
12
286
  ## [v2.11.0] — 2026-07-28 (launcher backend variants)
13
287
 
14
288
  ### Added
@@ -52,7 +326,11 @@ are kept verbatim where the Japanese text itself is the subject).
52
326
  exist in the chosen build with a 400 instead of letting
53
327
  `--device CUDA0` reach a Vulkan build and fail at startup. Skipped
54
328
  when `--list-devices` itself fails (best-effort, per the
55
- `hardware.py` invariant). New `launcher_devices.unknown_device_ids`.
329
+ `hardware.py` invariant). New `launcher_devices.foreign_device_ids`,
330
+ which compares the backend *prefix* (`backend_of`) rather than the
331
+ exact id — the mismatch worth catching is the wrong build, not a
332
+ number gap, and prefix matching stays correct if a future
333
+ `--list-devices` format change makes `parse_list_devices` miss a line.
56
334
 
57
335
  ### Changed
58
336
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.11.0
3
+ Version: 2.11.2
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -53,11 +53,16 @@ from coderouter.translation.anthropic import (
53
53
  AnthropicResponse,
54
54
  AnthropicStreamEvent,
55
55
  )
56
- from coderouter.translation.convert import (
57
- stream_anthropic_to_chat_chunks,
58
- to_anthropic_request,
59
- to_chat_response,
60
- )
56
+
57
+ # NOTE: `coderouter.translation.convert` is intentionally NOT imported here
58
+ # at module scope (see H-7). The import chain is:
59
+ # translation/__init__.py -> convert.py -> adapters/__init__.py
60
+ # -> registry.py -> anthropic_native.py -> translation.convert (again)
61
+ # A top-level import here closes that cycle and makes standalone imports of
62
+ # `coderouter.translation.*` fail with a partial-initialization ImportError
63
+ # unless `coderouter.adapters` happened to be imported first. The three
64
+ # symbols used from `convert` are imported lazily inside `generate()` /
65
+ # `stream()` instead, which breaks the cycle without changing behavior.
61
66
 
62
67
  logger = get_logger(__name__)
63
68
 
@@ -210,6 +215,12 @@ class AnthropicAdapter(BaseAdapter):
210
215
  ``to_chat_response``. The ``coderouter_provider`` tag is preserved
211
216
  on both sides.
212
217
  """
218
+ # Local import to avoid a circular import at module load time (H-7):
219
+ # convert.py transitively imports coderouter.adapters, which imports
220
+ # this module. Importing convert.py here (call time, not load time)
221
+ # keeps `coderouter.translation.*` importable standalone.
222
+ from coderouter.translation.convert import to_anthropic_request, to_chat_response
223
+
213
224
  anth_req = to_anthropic_request(request)
214
225
  anth_resp = await self.generate_anthropic(anth_req, overrides=overrides)
215
226
  chat_resp = to_chat_response(anth_resp)
@@ -241,6 +252,13 @@ class AnthropicAdapter(BaseAdapter):
241
252
  - Anthropic ``event: error`` → translator raises
242
253
  AdapterError(retryable=False), same treatment as above.
243
254
  """
255
+ # Local import to avoid a circular import at module load time (H-7);
256
+ # see the matching comment in generate() above.
257
+ from coderouter.translation.convert import (
258
+ stream_anthropic_to_chat_chunks,
259
+ to_anthropic_request,
260
+ )
261
+
244
262
  anth_req = to_anthropic_request(request)
245
263
  events = self.stream_anthropic(anth_req, overrides=overrides)
246
264
  async for chunk in stream_anthropic_to_chat_chunks(events, provider_name=self.name):
@@ -624,6 +624,24 @@ def _resolve_config_path(explicit: str | None) -> Path:
624
624
  return candidates[-1]
625
625
 
626
626
 
627
+ def _write_verdict_line(*, write: bool, files_written: int) -> None:
628
+ """Print the one-line "did we touch the disk?" verdict.
629
+
630
+ Called on **every** exit path of :func:`_run_apply_or_dry_run`.
631
+ The v1.x regression this guards against: an ``--apply`` run that
632
+ silently rewrote providers.yaml while the summary said "already
633
+ applied" and printed neither a diff nor a ``Backup:`` line. Making
634
+ the statement unconditional means no future early-return can go
635
+ quiet about a write again.
636
+ """
637
+ if not write:
638
+ print(" 0 files written (dry-run).")
639
+ elif files_written == 0:
640
+ print(" 0 files written — no file contents changed.")
641
+ else:
642
+ print(f" {files_written} file(s) written.")
643
+
644
+
627
645
  def _run_apply_or_dry_run(
628
646
  *,
629
647
  report: object,
@@ -655,9 +673,13 @@ def _run_apply_or_dry_run(
655
673
  )
656
674
  except MissingDependencyError as exc:
657
675
  print(f"doctor --apply: {exc}", file=sys.stderr)
676
+ _write_verdict_line(write=write, files_written=0)
658
677
  return 1
659
678
  except DoctorApplyError as exc:
679
+ # Every merge happens before the first byte is written, so an
680
+ # abort here always leaves the disk untouched.
660
681
  print(f"doctor --apply: {exc}", file=sys.stderr)
682
+ _write_verdict_line(write=write, files_written=0)
661
683
  return 1
662
684
 
663
685
  label = "Apply" if write else "Dry-run"
@@ -680,6 +702,8 @@ def _run_apply_or_dry_run(
680
702
  f" All {result.no_op_patches} patch(es) already applied "
681
703
  f"— providers.yaml is up to date."
682
704
  )
705
+ _write_verdict_line(write=write, files_written=len(result.written_paths))
706
+ _print_reformat_notice(result, write=write)
683
707
  return 0
684
708
 
685
709
  print(
@@ -694,16 +718,41 @@ def _run_apply_or_dry_run(
694
718
  print()
695
719
  print(diff, end="" if diff.endswith("\n") else "\n")
696
720
 
721
+ _write_verdict_line(write=write, files_written=len(result.written_paths))
697
722
  if write:
698
723
  for orig, bak in result.backups.items():
699
724
  print(f" Backup: {orig} → {bak}")
700
725
  else:
726
+ _print_reformat_notice(result, write=write)
701
727
  print()
702
728
  print(" (dry-run — no files were modified. Re-run with --apply to write.)")
703
729
 
704
730
  return 0
705
731
 
706
732
 
733
+ def _print_reformat_notice(result: object, *, write: bool) -> None:
734
+ """Dry-run-only heads-up about cosmetic re-serialization deltas.
735
+
736
+ ``reformat_only`` holds targets whose re-dump differs from disk
737
+ without any patch having changed a value — today that is explicit
738
+ ``key: null`` scalars, which ruamel re-emits as empty scalars.
739
+ These bytes are never written, so the note is purely so an operator
740
+ comparing "the diff I expected" with "the file I have" is not
741
+ surprised. Suppressed under ``--apply``, where nothing about them
742
+ is actionable.
743
+ """
744
+ reformat = getattr(result, "reformat_only", None) or {}
745
+ if write or not reformat:
746
+ return
747
+ print(
748
+ f" note: {len(reformat)} file(s) would re-serialize with cosmetic "
749
+ "differences (e.g. explicit `null` → empty scalar). These are NOT "
750
+ "written — --apply only rewrites files whose values actually changed."
751
+ )
752
+ for path in sorted(reformat):
753
+ print(f" - {path}")
754
+
755
+
707
756
 
708
757
 
709
758
  def _run_audit(args: argparse.Namespace) -> int:
@@ -7,6 +7,17 @@ Search order (first hit wins):
7
7
  4. ~/.coderouter/providers.yaml
8
8
 
9
9
  Secrets are resolved by reading the env var named by `api_key_env`.
10
+
11
+ .. note::
12
+ **[Unreleased] deprecation notice (planned v2.12.0):** step 3 above
13
+ (implicit CWD discovery) is slated to become opt-in, gated behind
14
+ ``CODEROUTER_ALLOW_CWD_CONFIG``. Landing that change without warning
15
+ first would silently break anyone currently relying on "run from the
16
+ directory with providers.yaml in it" with no diagnostic. Until that
17
+ lands, :func:`load_config` emits a one-time ``cwd-config-loaded``
18
+ warning whenever step 3 is the one that actually resolved — never
19
+ when an explicit ``--config`` / ``CODEROUTER_CONFIG`` path was used,
20
+ even if it happens to point at the same file.
10
21
  """
11
22
 
12
23
  from __future__ import annotations
@@ -17,6 +28,14 @@ from pathlib import Path
17
28
  import yaml
18
29
 
19
30
  from coderouter.config.schemas import CodeRouterConfig
31
+ from coderouter.logging import get_logger
32
+
33
+ logger = get_logger(__name__)
34
+
35
+ # Guards the one-time [Unreleased]/v2.12.0 CWD-config deprecation warning
36
+ # (see module docstring) so a process that calls load_config() repeatedly
37
+ # (tests, hot-reload, multiple create_app() calls) only logs it once.
38
+ _cwd_config_warning_emitted = False
20
39
 
21
40
 
22
41
  def _candidate_paths(explicit: str | os.PathLike[str] | None) -> list[Path]:
@@ -30,6 +49,51 @@ def _candidate_paths(explicit: str | os.PathLike[str] | None) -> list[Path]:
30
49
  return paths
31
50
 
32
51
 
52
+ def _warn_if_cwd_config(
53
+ chosen: Path, *, explicit: str | os.PathLike[str] | None
54
+ ) -> None:
55
+ """Emit the one-time [Unreleased]/v2.12.0 CWD-config deprecation warning.
56
+
57
+ Only fires when ``chosen`` was resolved via the *implicit* CWD-search
58
+ step. Compares ``chosen`` against the *literal* explicit / env
59
+ candidate paths (not merely whether those inputs were set) so that:
60
+
61
+ * an explicit ``--config`` (or ``CODEROUTER_CONFIG``) path that
62
+ happens to coincide with ``./providers.yaml`` stays quiet — an
63
+ explicit choice, however coincidental, is not the thing being
64
+ deprecated; but
65
+ * an explicit path/env var that was set but did NOT resolve (file
66
+ missing, so the search fell through to CWD) still warns, because
67
+ the CWD step is what actually served the config in that case.
68
+ """
69
+ global _cwd_config_warning_emitted
70
+ if _cwd_config_warning_emitted:
71
+ return
72
+ cwd_path = Path.cwd() / "providers.yaml"
73
+ if chosen != cwd_path:
74
+ return
75
+ if explicit and Path(explicit) == chosen:
76
+ return
77
+ if (env_path := os.environ.get("CODEROUTER_CONFIG")) and Path(env_path) == chosen:
78
+ return
79
+
80
+ _cwd_config_warning_emitted = True
81
+ logger.warning(
82
+ "cwd-config-loaded",
83
+ extra={
84
+ "path": str(chosen),
85
+ "hint": (
86
+ f"loaded providers.yaml from the current working "
87
+ f"directory ({chosen}). Starting in v2.12.0, this "
88
+ "implicit CWD discovery becomes opt-in (gated behind "
89
+ "CODEROUTER_ALLOW_CWD_CONFIG) — pass --config or set "
90
+ "CODEROUTER_CONFIG to pin the file explicitly and avoid "
91
+ "a startup break when that lands."
92
+ ),
93
+ },
94
+ )
95
+
96
+
33
97
  def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
34
98
  """Load providers.yaml + apply ALLOW_PAID env override."""
35
99
  candidates = _candidate_paths(path)
@@ -40,6 +104,7 @@ def load_config(path: str | os.PathLike[str] | None = None) -> CodeRouterConfig:
40
104
  f"providers.yaml not found. Searched:\n {searched}\n"
41
105
  f"Hint: copy examples/providers.yaml to ~/.coderouter/providers.yaml"
42
106
  )
107
+ _warn_if_cwd_config(chosen, explicit=path)
43
108
 
44
109
  with chosen.open("r", encoding="utf-8") as f:
45
110
  raw = yaml.safe_load(f) or {}
@@ -26,6 +26,10 @@ from pydantic import (
26
26
  model_validator,
27
27
  )
28
28
 
29
+ from coderouter.logging import get_logger
30
+
31
+ logger = get_logger(__name__)
32
+
29
33
 
30
34
  class Capabilities(BaseModel):
31
35
  """Capability flags per provider (plan.md §2.5)."""
@@ -350,6 +354,17 @@ class AgentCliConfig(BaseModel):
350
354
  return self
351
355
 
352
356
 
357
+ # [Unreleased]/planned v2.12.0: patterns used by ProviderConfig's
358
+ # ``_warn_restart_command_shell_syntax`` validator to flag
359
+ # ``restart_command`` values that rely on shell syntax not honored by
360
+ # the planned ``shlex.split`` + ``shell=False`` dispatch (see that
361
+ # validator's docstring and the field description below for the full
362
+ # rationale). Compiled once at module scope since every ProviderConfig
363
+ # validation reuses them.
364
+ _RESTART_COMMAND_SHELL_META_RE = re.compile(r"(&&|\|\||\||;|>|<|\$\(|`)")
365
+ _RESTART_COMMAND_ENV_PREFIX_RE = re.compile(r"^\w+=")
366
+
367
+
353
368
  class ProviderConfig(BaseModel):
354
369
  """A single provider entry from providers.yaml.
355
370
 
@@ -497,15 +512,82 @@ class ProviderConfig(BaseModel):
497
512
  restart_command: str | None = Field(
498
513
  default=None,
499
514
  description=(
500
- "v2.0-J (Self-healing): shell command to restart this "
501
- "provider's backend process. Examples: 'ollama serve', "
515
+ "v2.0-J (Self-healing): command to restart this provider's "
516
+ "backend process. Examples: 'ollama serve', "
502
517
  "'open -a LM\\ Studio'. Only executed when the profile's "
503
518
  "backend_health_action is 'exclude' and the provider "
504
519
  "transitions to UNHEALTHY. Unset = no automatic restart "
505
- "(recovery probe still runs, waiting for manual restart)."
520
+ "(recovery probe still runs, waiting for manual restart). "
521
+ "\n\n"
522
+ "[Unreleased]/planned v2.12.0: currently run as "
523
+ "``subprocess.run(command, shell=True)`` (coderouter/guards/"
524
+ "self_healing.py), so shell syntax works today — pipelines "
525
+ "(``pkill ollama && ollama serve``), ``~`` expansion, and "
526
+ "leading env-var assignments (``OLLAMA_HOST=0.0.0.0 ollama "
527
+ "serve``) are all honored. A future minor switches this to "
528
+ "``shlex.split(command)`` + ``shell=False`` (argv dispatch, "
529
+ "no shell in between), which does NOT support any of the "
530
+ "above — ``&&`` becomes a literal argument to the first "
531
+ "command instead of a separator, ``~/...`` paths are not "
532
+ "expanded, and ``FOO=bar cmd`` fails to exec. If your "
533
+ "command needs shell features, wrap it in a small script "
534
+ "and point restart_command at that script instead, e.g. "
535
+ "``restart_command: /path/to/restart-ollama.sh``."
506
536
  ),
507
537
  )
508
538
 
539
+ @model_validator(mode="after")
540
+ def _warn_restart_command_shell_syntax(self) -> ProviderConfig:
541
+ """[Unreleased]/planned v2.12.0 breaking-change heads-up (warning only).
542
+
543
+ ``coderouter/guards/self_healing.py`` currently runs
544
+ ``restart_command`` via ``subprocess.run(command, shell=True)``.
545
+ The planned v2.12.0 change switches that to
546
+ ``shlex.split(command)`` + ``shell=False`` — a shell-syntax
547
+ ``restart_command`` that works fine today would then either
548
+ crash (``FileNotFoundError`` for ``~/...`` paths or an
549
+ ``ENV=val cmd`` prefix) or, worse, silently misbehave (``&&`` /
550
+ ``||`` / ``|`` / ``;`` / redirects becoming literal argv tokens
551
+ instead of shell operators — a provider "restart" that quietly
552
+ does the wrong thing is worse than one that errors loudly).
553
+
554
+ Deliberately never raises: this validator does not enforce
555
+ anything, it only warns ahead of the wire change so operators
556
+ can migrate ``restart_command`` to a plain argv (or a wrapper
557
+ script) before v2.12.0 lands, instead of discovering the break
558
+ the next time a provider actually goes UNHEALTHY.
559
+ """
560
+ cmd = self.restart_command
561
+ if not cmd:
562
+ return self
563
+ stripped = cmd.strip()
564
+ reasons: list[str] = []
565
+ if _RESTART_COMMAND_SHELL_META_RE.search(cmd):
566
+ reasons.append("shell metacharacter (&&, ||, |, ;, >, <, $(...), or `...`)")
567
+ if stripped.startswith("~/"):
568
+ reasons.append("leading '~/' path (no shell expansion under shell=False)")
569
+ if _RESTART_COMMAND_ENV_PREFIX_RE.match(stripped):
570
+ reasons.append("leading environment-variable assignment (FOO=bar ...)")
571
+ if reasons:
572
+ logger.warning(
573
+ "restart-command-shell-syntax",
574
+ extra={
575
+ "provider": self.name,
576
+ "restart_command": cmd,
577
+ "reasons": reasons,
578
+ "hint": (
579
+ "restart_command currently runs via "
580
+ "subprocess.run(..., shell=True); a planned "
581
+ "v2.12.0 change switches to shlex.split(...) + "
582
+ "shell=False, which does not support this "
583
+ "command's shell syntax. Rewrite it as plain "
584
+ "argv, or wrap it in a script and point "
585
+ "restart_command at that script."
586
+ ),
587
+ },
588
+ )
589
+ return self
590
+
509
591
  @model_validator(mode="after")
510
592
  def _check_output_filters_known(self) -> ProviderConfig:
511
593
  """v1.0-A: fail at config-load on a typo'd filter name.