devops-bot-sdk 1.6.33__tar.gz → 1.6.37__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 (190) hide show
  1. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/SOURCES.txt +26 -3
  4. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/entry_points.txt +1 -0
  5. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/pyproject.toml +1 -0
  6. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/__init__.py +2 -2
  7. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agentd.py +21 -0
  8. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/cli.py +31 -1
  9. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/client.py +243 -55
  10. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/config.py +105 -1
  11. devops_bot_sdk-1.6.37/sdk/estimator.py +246 -0
  12. devops_bot_sdk-1.6.37/sdk/firewall.py +310 -0
  13. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/formats.py +28 -2
  14. devops_bot_sdk-1.6.37/sdk/github_ingest.py +573 -0
  15. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/graph_recon.py +137 -0
  16. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/graphify.py +309 -21
  17. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/intake.py +154 -9
  18. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ipc/handlers.py +422 -15
  19. devops_bot_sdk-1.6.37/sdk/journal.py +388 -0
  20. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/local_exec.py +259 -50
  21. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/plan_run.py +26 -4
  22. devops_bot_sdk-1.6.37/sdk/prd_progress.py +288 -0
  23. devops_bot_sdk-1.6.37/sdk/providers.py +631 -0
  24. devops_bot_sdk-1.6.37/sdk/resume.py +383 -0
  25. devops_bot_sdk-1.6.37/sdk/router.py +420 -0
  26. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/run_auto.py +17 -24
  27. devops_bot_sdk-1.6.37/sdk/swagger.py +356 -0
  28. devops_bot_sdk-1.6.37/sdk/task_types.py +199 -0
  29. devops_bot_sdk-1.6.37/sdk/tiers/__init__.py +17 -0
  30. devops_bot_sdk-1.6.37/sdk/tiers/mini_agent.py +336 -0
  31. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/api.py +84 -28
  32. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/ip_allowlist.py +43 -16
  33. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/models.py +15 -14
  34. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/server.py +125 -17
  35. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/app.js +87 -10
  36. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/chat.html +27 -8
  37. devops_bot_sdk-1.6.37/sdk/vps_health.py +264 -0
  38. devops_bot_sdk-1.6.37/sdk/write_hook.py +242 -0
  39. devops_bot_sdk-1.6.37/tests/test_cheap_tier.py +428 -0
  40. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_client_jira_projects.py +45 -4
  41. devops_bot_sdk-1.6.37/tests/test_estimator.py +285 -0
  42. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_formats.py +56 -5
  43. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_github_ingest.py +8 -4
  44. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_graphify_build.py +134 -4
  45. devops_bot_sdk-1.6.37/tests/test_journal.py +288 -0
  46. devops_bot_sdk-1.6.37/tests/test_local_exec_provider.py +295 -0
  47. devops_bot_sdk-1.6.37/tests/test_prd_artifact.py +473 -0
  48. devops_bot_sdk-1.6.37/tests/test_providers.py +378 -0
  49. devops_bot_sdk-1.6.37/tests/test_resume.py +495 -0
  50. devops_bot_sdk-1.6.37/tests/test_router.py +447 -0
  51. devops_bot_sdk-1.6.37/tests/test_swagger_command.py +260 -0
  52. devops_bot_sdk-1.6.37/tests/test_tier_flag_parity.py +364 -0
  53. devops_bot_sdk-1.6.37/tests/test_ui_gate.py +281 -0
  54. devops_bot_sdk-1.6.37/tests/test_ui_lifespan.py +89 -0
  55. devops_bot_sdk-1.6.37/tests/test_write_hook.py +289 -0
  56. devops_bot_sdk-1.6.33/sdk/github_ingest.py +0 -161
  57. devops_bot_sdk-1.6.33/sdk/ui/static/login.html +0 -111
  58. devops_bot_sdk-1.6.33/sdk/vps_health.py +0 -111
  59. devops_bot_sdk-1.6.33/tests/test_otp_login.py +0 -178
  60. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/README.md +0 -0
  61. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  62. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/requires.txt +0 -0
  63. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  64. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/__init__.py +0 -0
  65. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/activity.py +0 -0
  66. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/builder.py +0 -0
  67. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/chat.py +0 -0
  68. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/connectors.py +0 -0
  69. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/__init__.py +0 -0
  70. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/claude_code.py +0 -0
  71. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/http.py +0 -0
  72. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/mcp.py +0 -0
  73. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/n8n.py +0 -0
  74. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/shell.py +0 -0
  75. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/integrations.py +0 -0
  76. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/inventory.py +0 -0
  77. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/library/__init__.py +0 -0
  78. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/manifest.py +0 -0
  79. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_studio.py +0 -0
  80. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_template.py +0 -0
  81. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_vars.py +0 -0
  82. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/run_log.py +0 -0
  83. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/schedules.py +0 -0
  84. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/secrets.py +0 -0
  85. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/ticket_run.py +0 -0
  86. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/usage.py +0 -0
  87. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/varstore.py +0 -0
  88. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/browser_profile.py +0 -0
  89. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/__init__.py +0 -0
  90. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/files.py +0 -0
  91. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/process.py +0 -0
  92. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/screenshot.py +0 -0
  93. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/crucial.py +0 -0
  94. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/departments.py +0 -0
  95. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/__init__.py +0 -0
  96. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/assets.py +0 -0
  97. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/bootstrap.py +0 -0
  98. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/browser.py +0 -0
  99. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/compare.py +0 -0
  100. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/loop.py +0 -0
  101. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/exceptions.py +0 -0
  102. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/git_ops.py +0 -0
  103. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/hooks/__init__.py +0 -0
  104. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/hooks/crucial_guard.py +0 -0
  105. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ipc/__init__.py +0 -0
  106. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ipc/electron_bridge.py +0 -0
  107. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/__init__.py +0 -0
  108. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/envelope.py +0 -0
  109. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/requests.py +0 -0
  110. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/responses.py +0 -0
  111. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/snapshots.py +0 -0
  112. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/phase_timer.py +0 -0
  113. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/platform_compat.py +0 -0
  114. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/proof_readers.py +0 -0
  115. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/py.typed +0 -0
  116. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/scheduler.py +0 -0
  117. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/self_report.py +0 -0
  118. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/self_report_local.py +0 -0
  119. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/sse.py +0 -0
  120. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/task_file.py +0 -0
  121. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/test.py +0 -0
  122. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/test_pipeline.py +0 -0
  123. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ticket_fields.py +0 -0
  124. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/__init__.py +0 -0
  125. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/dashboard.py +0 -0
  126. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/health.py +0 -0
  127. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/jobs.py +0 -0
  128. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/runner.py +0 -0
  129. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/security.py +0 -0
  130. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/service.py +0 -0
  131. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/session.py +0 -0
  132. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/setup_manager.py +0 -0
  133. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/agents.html +0 -0
  134. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/custom-agents.html +0 -0
  135. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/default-agents.html +0 -0
  136. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/index.html +0 -0
  137. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/report.html +0 -0
  138. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/runs.html +0 -0
  139. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/schedules.html +0 -0
  140. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/setup.html +0 -0
  141. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/styles.css +0 -0
  142. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/tickets.html +0 -0
  143. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/updater.py +0 -0
  144. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/url_access.py +0 -0
  145. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/setup.cfg +0 -0
  146. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_activity.py +0 -0
  147. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_activity_tracking.py +0 -0
  148. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_chat_features.py +0 -0
  149. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_listing.py +0 -0
  150. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_async.py +0 -0
  151. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_models.py +0 -0
  152. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_run_ticket.py +0 -0
  153. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_schedules.py +0 -0
  154. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_workspace_settings.py +0 -0
  155. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agents_connectors_builder.py +0 -0
  156. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_allowed_tools_config.py +0 -0
  157. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_auto_agent_dispatch.py +0 -0
  158. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_browser_profile.py +0 -0
  159. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_config_token_diagnostics.py +0 -0
  160. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_configure_token_owner.py +0 -0
  161. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_console_encoding.py +0 -0
  162. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_departments.py +0 -0
  163. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_epic_decompose_run.py +0 -0
  164. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_epic_inherited_agent_id.py +0 -0
  165. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_epic_session_isolation.py +0 -0
  166. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_git_action.py +0 -0
  167. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_graph_recon.py +0 -0
  168. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_graphify_hook_install.py +0 -0
  169. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_health.py +0 -0
  170. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_intake_plan_review.py +0 -0
  171. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_phase_timer.py +0 -0
  172. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_plan_new_command.py +0 -0
  173. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_plan_run_activity.py +0 -0
  174. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_proof_readers.py +0 -0
  175. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_recon_session_isolation.py +0 -0
  176. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_run_auto_agent_only.py +0 -0
  177. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_run_outputs.py +0 -0
  178. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_schedule_api.py +0 -0
  179. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_scheduler.py +0 -0
  180. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_self_report.py +0 -0
  181. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_service_token_warning.py +0 -0
  182. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_task_file_and_plan_run.py +0 -0
  183. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_task_file_format_doc.py +0 -0
  184. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_ticket_fields.py +0 -0
  185. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_ticket_run_resolve_by_id.py +0 -0
  186. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_ticket_speed_fixes.py +0 -0
  187. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_update_timer.py +0 -0
  188. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_updater_extras.py +0 -0
  189. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_url_access.py +0 -0
  190. {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_vps_health.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.33
3
+ Version: 1.6.37
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.33
3
+ Version: 1.6.37
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -14,31 +14,41 @@ sdk/client.py
14
14
  sdk/config.py
15
15
  sdk/crucial.py
16
16
  sdk/departments.py
17
+ sdk/estimator.py
17
18
  sdk/exceptions.py
19
+ sdk/firewall.py
18
20
  sdk/formats.py
19
21
  sdk/git_ops.py
20
22
  sdk/github_ingest.py
21
23
  sdk/graph_recon.py
22
24
  sdk/graphify.py
23
25
  sdk/intake.py
26
+ sdk/journal.py
24
27
  sdk/local_exec.py
25
28
  sdk/phase_timer.py
26
29
  sdk/plan_run.py
27
30
  sdk/platform_compat.py
31
+ sdk/prd_progress.py
28
32
  sdk/proof_readers.py
33
+ sdk/providers.py
29
34
  sdk/py.typed
35
+ sdk/resume.py
36
+ sdk/router.py
30
37
  sdk/run_auto.py
31
38
  sdk/scheduler.py
32
39
  sdk/self_report.py
33
40
  sdk/self_report_local.py
34
41
  sdk/sse.py
42
+ sdk/swagger.py
35
43
  sdk/task_file.py
44
+ sdk/task_types.py
36
45
  sdk/test.py
37
46
  sdk/test_pipeline.py
38
47
  sdk/ticket_fields.py
39
48
  sdk/updater.py
40
49
  sdk/url_access.py
41
50
  sdk/vps_health.py
51
+ sdk/write_hook.py
42
52
  sdk/agents/__init__.py
43
53
  sdk/agents/activity.py
44
54
  sdk/agents/builder.py
@@ -83,6 +93,8 @@ sdk/models/envelope.py
83
93
  sdk/models/requests.py
84
94
  sdk/models/responses.py
85
95
  sdk/models/snapshots.py
96
+ sdk/tiers/__init__.py
97
+ sdk/tiers/mini_agent.py
86
98
  sdk/ui/__init__.py
87
99
  sdk/ui/api.py
88
100
  sdk/ui/dashboard.py
@@ -102,7 +114,6 @@ sdk/ui/static/chat.html
102
114
  sdk/ui/static/custom-agents.html
103
115
  sdk/ui/static/default-agents.html
104
116
  sdk/ui/static/index.html
105
- sdk/ui/static/login.html
106
117
  sdk/ui/static/report.html
107
118
  sdk/ui/static/runs.html
108
119
  sdk/ui/static/schedules.html
@@ -122,6 +133,7 @@ tests/test_agents_connectors_builder.py
122
133
  tests/test_allowed_tools_config.py
123
134
  tests/test_auto_agent_dispatch.py
124
135
  tests/test_browser_profile.py
136
+ tests/test_cheap_tier.py
125
137
  tests/test_client_jira_projects.py
126
138
  tests/test_config_token_diagnostics.py
127
139
  tests/test_configure_token_owner.py
@@ -130,6 +142,7 @@ tests/test_departments.py
130
142
  tests/test_epic_decompose_run.py
131
143
  tests/test_epic_inherited_agent_id.py
132
144
  tests/test_epic_session_isolation.py
145
+ tests/test_estimator.py
133
146
  tests/test_formats.py
134
147
  tests/test_git_action.py
135
148
  tests/test_github_ingest.py
@@ -138,24 +151,34 @@ tests/test_graphify_build.py
138
151
  tests/test_graphify_hook_install.py
139
152
  tests/test_health.py
140
153
  tests/test_intake_plan_review.py
141
- tests/test_otp_login.py
154
+ tests/test_journal.py
155
+ tests/test_local_exec_provider.py
142
156
  tests/test_phase_timer.py
143
157
  tests/test_plan_new_command.py
144
158
  tests/test_plan_run_activity.py
159
+ tests/test_prd_artifact.py
145
160
  tests/test_proof_readers.py
161
+ tests/test_providers.py
146
162
  tests/test_recon_session_isolation.py
163
+ tests/test_resume.py
164
+ tests/test_router.py
147
165
  tests/test_run_auto_agent_only.py
148
166
  tests/test_run_outputs.py
149
167
  tests/test_schedule_api.py
150
168
  tests/test_scheduler.py
151
169
  tests/test_self_report.py
152
170
  tests/test_service_token_warning.py
171
+ tests/test_swagger_command.py
153
172
  tests/test_task_file_and_plan_run.py
154
173
  tests/test_task_file_format_doc.py
155
174
  tests/test_ticket_fields.py
156
175
  tests/test_ticket_run_resolve_by_id.py
157
176
  tests/test_ticket_speed_fixes.py
177
+ tests/test_tier_flag_parity.py
178
+ tests/test_ui_gate.py
179
+ tests/test_ui_lifespan.py
158
180
  tests/test_update_timer.py
159
181
  tests/test_updater_extras.py
160
182
  tests/test_url_access.py
161
- tests/test_vps_health.py
183
+ tests/test_vps_health.py
184
+ tests/test_write_hook.py
@@ -2,4 +2,5 @@
2
2
  agentos = sdk.cli:main
3
3
  agentos-agentd = sdk.agentd:main
4
4
  agentos-auto = sdk.run_auto:main
5
+ agentos-swagger = sdk.swagger:main
5
6
  agentos-ui = sdk.ui.server:main
@@ -54,6 +54,7 @@ agentos = "sdk.cli:main" # unified CLI: configure/agent/integ
54
54
  agentos-auto = "sdk.run_auto:main" # run orchestrate-auto LOCALLY (co_ token from config)
55
55
  agentos-agentd = "sdk.agentd:main" # long-running custom-agent daemon
56
56
  agentos-ui = "sdk.ui.server:main" # local web control panel (needs the [ui] extra)
57
+ agentos-swagger = "sdk.swagger:main" # where /docs is + can you reach it (= `agentos swagger`)
57
58
 
58
59
  [project.urls]
59
60
  Homepage = "https://agentos.io"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.33
3
+ Version: 1.6.37
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -34,7 +34,7 @@ Rules:
34
34
  - All data egress through submit_webhook only
35
35
  """
36
36
 
37
- __version__ = "1.6.33" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.37" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
38
38
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
39
39
 
40
40
  from sdk.client import BackendClient
@@ -429,8 +429,25 @@ async def _dispatch_agent_run(
429
429
  )
430
430
  run_log.log_line(_log_path, f"task_id={task_id}")
431
431
 
432
+ # A custom-agent run writes real files, so it gets the SAME shared
433
+ # post-write hook the coding path uses: journal (synchronous,
434
+ # survives a kill) → requirement overlay → graph refresh. Without
435
+ # this, a custom-agent run produced no graph update and no journal
436
+ # entry at all, so a tier switch during one had no anchor to resume
437
+ # from — the run type was simply outside the resume triple.
438
+ from sdk import write_hook
439
+ from sdk.agents.engines.claude_code import ensure_agent_workspace
440
+
441
+ _workspace = str(ensure_agent_workspace(manifest.name))
442
+ _record_write = write_hook.make_recorder(
443
+ task_id=task_id or f"agent-{manifest.name}",
444
+ repo_path=_workspace,
445
+ )
446
+ _write_events = write_hook.make_event_hook(_record_write)
447
+
432
448
  async def _tee(ev: dict) -> None:
433
449
  run_log.log_event(_log_path, ev)
450
+ await _write_events(ev)
434
451
 
435
452
  _started = time.monotonic()
436
453
  local_result = await claude_code_engine.run(
@@ -438,6 +455,10 @@ async def _dispatch_agent_run(
438
455
  on_event=_tee,
439
456
  )
440
457
  run_log.finish_run(_log_path, local_result)
458
+ # Backstop for writes the stream did not surface (a resumed turn,
459
+ # an engine that tees events elsewhere). Ordering is lost, but an
460
+ # ordered-but-empty journal is worse than an unordered complete one.
461
+ await write_hook.record_result_files(_record_write, local_result)
441
462
  # Local ledger + platform `agent_activities` feed. EVERY trigger path
442
463
  # reaches this function (cron schedule, ticket label, on-demand
443
464
  # run-on-ticket from the UI / CLI / desktop IPC), so a VPS agent run is
@@ -58,6 +58,26 @@ Command surface:
58
58
  agentos integration rm <name>
59
59
  Remove a registered integration.
60
60
 
61
+ agentos firewall [--apply] [--revoke IP] [--port N]
62
+ Print the host-firewall rules that open the agentos-ui port to every IP in
63
+ the application allowlist (`sdk.config.get_allowed_ips`), for THIS OS — ufw
64
+ on Linux, `netsh advfirewall` on Windows, a pf note on macOS. Prints by
65
+ default; `--apply` executes them and needs root / Administrator. The host
66
+ firewall is a separate layer the allowlist cannot reach, and the two drifting
67
+ apart is what silently blinded every VPS when the backend's egress IP moved.
68
+ See sdk.firewall.
69
+
70
+ agentos swagger [--open] [--json [PATH]] [--port N]
71
+ Where the panel's Swagger UI is and whether it can actually be reached:
72
+ the URL derived from the bind address the service was installed with,
73
+ whether a co_ token is configured (masked), how many operations the spec
74
+ has, and a ufw/netsh diff against sdk.config.get_allowed_ips when the HOST
75
+ firewall would drop the port — a layer the SDK's own allowlist cannot see.
76
+ `--open` launches a browser; `--json` writes openapi.json from
77
+ create_app().openapi() with no server running (`--json -` pipes it).
78
+ Also flags routes registered twice, since the second copy is dead code.
79
+ Needs the [ui] extra for the route count and --json. See sdk.swagger.
80
+
61
81
  agentos report submit [--date YYYY-MM-DD] (--text "…" | --file PATH)
62
82
  [--project NAME] [--proof PATH|URL]… [--no-wait]
63
83
  Report work that never reached Jira or GitHub, attach proofs (local
@@ -139,6 +159,14 @@ def main() -> None:
139
159
  _github_command(rest)
140
160
  return
141
161
 
162
+ if noun == "firewall":
163
+ from sdk import firewall
164
+ sys.exit(firewall.cli(rest))
165
+
166
+ if noun == "swagger":
167
+ from sdk import swagger
168
+ sys.exit(swagger.cli(rest))
169
+
142
170
  if noun == "report":
143
171
  _report_command(rest)
144
172
  return
@@ -152,7 +180,7 @@ def main() -> None:
152
180
 
153
181
 
154
182
  def _print_usage() -> None:
155
- print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser> ...")
183
+ print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser|firewall|swagger> ...")
156
184
  print()
157
185
  print(" agentos configure [--rotate]")
158
186
  print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
@@ -164,6 +192,8 @@ def _print_usage() -> None:
164
192
  print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
165
193
  print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
166
194
  print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
195
+ print(" agentos firewall [--apply] [--revoke IP] [--port N] # open the UI port to the allowlisted IPs")
196
+ print(" agentos swagger [--open] [--json [PATH]] # where /docs is, and can you reach it")
167
197
 
168
198
 
169
199
  # ── agent ────────────────────────────────────────────────────────────────
@@ -45,6 +45,13 @@ logger = logging.getLogger(__name__)
45
45
  # agents without a lookup on every run.
46
46
  _AGENT_ID_CACHE: dict[str, str] = {}
47
47
 
48
+ # GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
49
+ # monotonic). Keyed per credential so two clients on one box never share a token.
50
+ # Without this the token is re-fetched on EVERY call, which is what
51
+ # tests/test_ticket_speed_fixes.py::TestGithubTokenCache guards against.
52
+ _GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
53
+ _GITHUB_TOKEN_TTL_S = float(os.getenv("AGENTOS_GITHUB_TOKEN_TTL_S", "") or 1800.0)
54
+
48
55
  # GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
49
56
  # monotonic). Keyed per credential so two clients on one box never share a token.
50
57
  _GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
@@ -197,6 +204,30 @@ _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS exec
197
204
  _AUTO_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — same budget for multi-task auto runs
198
205
 
199
206
 
207
+ def _jira_project_records(payload: dict) -> list[dict]:
208
+ """Normalise a jira-projects envelope to `list[dict]`.
209
+
210
+ The endpoint returns `data` as a FLAT LIST. Sibling endpoints on the same
211
+ gateway wrap theirs in `data.records` / `data.record`, and this shape is not
212
+ documented, so all three are tolerated rather than guessing one — a wrong
213
+ guess reads as "no projects", which is indistinguishable from a real empty
214
+ result and would silently disable the whole picker.
215
+ """
216
+ data = payload.get("data")
217
+ if isinstance(data, list):
218
+ return [r for r in data if isinstance(r, dict)]
219
+ if isinstance(data, dict):
220
+ records = data.get("records")
221
+ if isinstance(records, list):
222
+ return [r for r in records if isinstance(r, dict)]
223
+ record = data.get("record")
224
+ if isinstance(record, dict):
225
+ return [record]
226
+ if data.get("id") or data.get("jiraProjectId"):
227
+ return [dict(data)]
228
+ return []
229
+
230
+
200
231
  class BackendClient:
201
232
  """Thin HTTPS/SSE client — wraps all backend /api/v1/* endpoints.
202
233
 
@@ -928,51 +959,71 @@ class BackendClient:
928
959
  co_ token. Best-effort: returns None on any failure so a run without a
929
960
  connected GitHub account still proceeds (clone/PR steps just won't auth).
930
961
  """
962
+ import time as _time
963
+
964
+ now = _time.monotonic()
965
+ cached = _GITHUB_TOKEN_CACHE.get(self._token)
966
+ if cached and now < cached[1]:
967
+ return cached[0]
931
968
  record = await self.github_token_record()
932
- return (record or {}).get("accessToken") or None
969
+ token = (record or {}).get("accessToken") or None
970
+ if token:
971
+ _GITHUB_TOKEN_CACHE[self._token] = (token, now + _GITHUB_TOKEN_TTL_S)
972
+ return token
933
973
 
934
- async def ingest_github_prs(self, since: str | None = None) -> dict:
974
+ async def ingest_github_prs(self, since: str | None = None) -> dict | None:
935
975
  """POST {AI}/api/v1/github/prs — trigger this user's daily PR ingestion.
936
976
 
937
- The backend resolves the caller's GitHub link server-side from the co_
938
- token in ``Authorization`` (see ``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md``);
939
- ``githubUsername``/``githubAccessToken`` below are placeholders kept only
940
- for backends that predate that resolution and are otherwise ignored.
941
- ``since`` (ISO-8601 date) scopes the fetch — omit for "today".
942
-
943
- Returns the backend's ``{status, job_id, status_url, ...}`` envelope.
944
- Raises BackendUnreachable / HTTP errors — the github_ingest cron catches
945
- and logs these itself rather than treating them as success.
977
+ The daily trigger. The work runs on the AI backend (LLM summarise, the
978
+ downstream daily-summary forward, the Qdrant upsert); the SDK only decides
979
+ WHEN, because only this machine knows its own catch-up watermark.
980
+
981
+ The backend resolves the caller's GitHub link server-side from the co_ token
982
+ in ``Authorization`` and OVERWRITES whatever the body sends
983
+ (``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md`` §7/§9 — that endpoint used to
984
+ take a GitHub access token in an unauthenticated body). So this deliberately
985
+ sends NO credentials: a client that still supplied them would be shipping a
986
+ secret the server ignores.
987
+
988
+ ``since`` is an ISO date (``YYYY-MM-DD``) → the backend's ``date_from``.
989
+ Omitted means "today", which is what a never-missed run wants; the cron sends
990
+ the watermark instead so a machine that was off does not lose days.
991
+
992
+ Returns the response JSON (``{status, job_id, status_url, ...}``) or **None**
993
+ on any failure — never raises into the cron. WHY it failed lands in
994
+ ``self.last_ingest_error`` (``{status, body, retryable}``), which the
995
+ scheduler needs to tell a busy backend (retry in 30 min) from a rejected
996
+ request (wait for the next slot).
946
997
  """
947
- body = [{
998
+ payload = [{
948
999
  "userId": self._user_id_from_token(),
949
- "githubUsername": "",
950
- "githubAccessToken": "",
951
- "apiTokenHash": self._token,
952
- "date_from": since,
1000
+ "apiTokenHash": self._token, # downstream daily-summary Authorization
953
1001
  }]
954
- async with httpx.AsyncClient(timeout=30.0) as client:
955
- resp = await client.post(
956
- self._url("/api/v1/github/prs"), json=body, headers=self._headers,
957
- )
958
- _check_status(resp.status_code, self._url("/api/v1/github/prs"))
959
- return resp.json()
960
-
961
- async def report_vps_health(self, payload: dict) -> dict:
962
- """PATCH {AI}/api/v1/vps/health — heartbeat this machine's resource usage.
963
-
964
- Feeds the backend's ``contabo_allocation`` row (VPS health shown in chat /
965
- activity reports) — a live GET of the same path answers on-demand checks,
966
- this PATCH is what keeps that data from going stale. Raises on failure
967
- (BackendUnreachable / HTTP error); the vps_health cron logs and continues
968
- rather than crashing the loop.
969
- """
970
- async with httpx.AsyncClient(timeout=30.0) as client:
971
- resp = await client.request(
972
- "PATCH", self._url("/api/v1/vps/health"), json=payload, headers=self._headers,
973
- )
974
- _check_status(resp.status_code, self._url("/api/v1/vps/health"))
975
- return resp.json()
1002
+ if since:
1003
+ payload[0]["date_from"] = since
1004
+ self.last_ingest_error = {}
1005
+ try:
1006
+ async with httpx.AsyncClient(timeout=60.0) as client:
1007
+ resp = await client.post(
1008
+ self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
1009
+ )
1010
+ if resp.status_code >= 400:
1011
+ body = resp.text[:500]
1012
+ logger.warning("client.ingest_github_prs failed status=%s body=%s",
1013
+ resp.status_code, body[:200])
1014
+ self.last_ingest_error = {
1015
+ "status": resp.status_code,
1016
+ "body": body,
1017
+ "retryable": is_retryable_backend_error(resp.status_code, body),
1018
+ }
1019
+ return None
1020
+ return resp.json()
1021
+ except Exception as exc: # noqa: BLE001 — a cron must not die on a blip
1022
+ logger.warning("client.ingest_github_prs error err=%s", exc)
1023
+ # A transport failure (timeout, reset, DNS blip) is as retryable as an
1024
+ # exhausted pool — the backend may simply be busy.
1025
+ self.last_ingest_error = {"status": 0, "body": str(exc)[:500], "retryable": True}
1026
+ return None
976
1027
 
977
1028
  async def move_to_backlog(self, task_id: str) -> bool:
978
1029
  """PUT {BE}/tasks-ml/{id} status=backlog (best-effort).
@@ -1096,6 +1147,108 @@ class BackendClient:
1096
1147
  logger.warning("client.set_task_status error task=%s err=%s", task_id, exc)
1097
1148
  return False
1098
1149
 
1150
+ # ── Cheap tier (T0) — the backend holds the Azure/GCP credentials ───
1151
+
1152
+ async def llm_complete(
1153
+ self,
1154
+ messages: list[dict],
1155
+ *,
1156
+ tools: list[dict] | None = None,
1157
+ step_type: str = "small_task",
1158
+ task_id: str | None = None,
1159
+ budget_tokens: int | None = None,
1160
+ used_tokens: int = 0,
1161
+ max_tokens: int | None = None,
1162
+ timeout: float = 120.0,
1163
+ attempts: int = 3,
1164
+ ) -> dict:
1165
+ """POST /api/v1/llm/complete — one turn of the cheap-tier tool loop.
1166
+
1167
+ The credential asymmetry is deliberate (plan §6): Azure/GCP keys stay
1168
+ **server-side** because the backend is the client for T0, while the
1169
+ Bedrock/subscription credentials stay desktop-side because the local
1170
+ ``claude`` CLI is the client for T1/T2. Each tier's creds live where that
1171
+ tier's client runs, so the desktop never holds an Azure key.
1172
+
1173
+ The budget is enforced **server-side** — the desktop is untrusted for
1174
+ accounting — and a breach comes back as ``409 budget_exhausted``, which
1175
+ the router treats as "escalate to T1", not as an error.
1176
+
1177
+ Returns ``{"ok", "text", "tool_calls", "model", "tokens_in",
1178
+ "tokens_out", "status", "reason"}``. Never raises: an unreachable
1179
+ backend is a *routing signal* (the cheap tier is the LESS available one),
1180
+ so it comes back as ``ok=False`` with ``reason="backend_unreachable"``
1181
+ and the router escalates.
1182
+
1183
+ Retries only what is worth retrying, via the existing
1184
+ ``is_retryable_backend_error`` — so a saturated Postgres pool surfaced
1185
+ as a 5xx is retried rather than escalating the whole task off the cheap
1186
+ tier for a transient blip.
1187
+ """
1188
+ path = "/api/v1/llm/complete"
1189
+ payload: dict[str, Any] = {
1190
+ "messages": messages,
1191
+ "step_type": step_type,
1192
+ }
1193
+ if tools:
1194
+ payload["tools"] = tools
1195
+ if task_id:
1196
+ payload["task_id"] = task_id
1197
+ if budget_tokens is not None:
1198
+ payload["budget_tokens"] = budget_tokens
1199
+ if used_tokens:
1200
+ payload["used_tokens"] = used_tokens
1201
+ if max_tokens is not None:
1202
+ payload["max_tokens"] = max_tokens
1203
+
1204
+ def _fail(reason: str, status: int = 0, detail: str = "") -> dict:
1205
+ return {"ok": False, "text": "", "tool_calls": [], "model": None,
1206
+ "tokens_in": 0, "tokens_out": 0, "status": status,
1207
+ "reason": reason, "detail": detail[:300]}
1208
+
1209
+ last = _fail("backend_unreachable")
1210
+ for attempt in range(max(1, attempts)):
1211
+ try:
1212
+ async with httpx.AsyncClient(timeout=timeout) as client:
1213
+ resp = await client.post(
1214
+ self._url(path), json=payload, headers=self._headers,
1215
+ )
1216
+ except Exception as exc: # noqa: BLE001 — network/DNS/TLS
1217
+ last = _fail("backend_unreachable", detail=str(exc))
1218
+ if attempt + 1 < attempts:
1219
+ await asyncio.sleep(min(2.0 * (attempt + 1), 5.0))
1220
+ continue
1221
+ return last
1222
+ body = resp.text or ""
1223
+ if resp.status_code == 409:
1224
+ # The hard ceiling. One mechanism serves both accounting and the
1225
+ # cap, so there is nothing to reconcile between them.
1226
+ return _fail("budget_exhausted", resp.status_code, body)
1227
+ if resp.status_code >= 400:
1228
+ if is_retryable_backend_error(resp.status_code, body) and attempt + 1 < attempts:
1229
+ last = _fail("retryable", resp.status_code, body)
1230
+ await asyncio.sleep(min(2.0 * (attempt + 1), 5.0))
1231
+ continue
1232
+ reason = ("auth" if resp.status_code == 401
1233
+ else "unavailable" if resp.status_code >= 500
1234
+ else "rejected")
1235
+ return _fail(reason, resp.status_code, body)
1236
+ try:
1237
+ data = resp.json() or {}
1238
+ except Exception: # noqa: BLE001 — a 200 with a non-JSON body
1239
+ return _fail("bad_response", resp.status_code, body)
1240
+ return {
1241
+ "ok": True,
1242
+ "text": str(data.get("text") or ""),
1243
+ "tool_calls": list(data.get("tool_calls") or []),
1244
+ "model": data.get("model"),
1245
+ "tokens_in": int(data.get("tokens_in") or 0),
1246
+ "tokens_out": int(data.get("tokens_out") or 0),
1247
+ "status": resp.status_code,
1248
+ "reason": "",
1249
+ }
1250
+ return last
1251
+
1099
1252
  async def update_task_ml(
1100
1253
  self, task_id: str, fields: dict, model_used: list[str] | None = None,
1101
1254
  ) -> bool:
@@ -1214,15 +1367,32 @@ class BackendClient:
1214
1367
  return None
1215
1368
 
1216
1369
  async def list_jira_projects(
1217
- self, pv_id: str, limit: int = 100, offset: int = 0,
1370
+ self, pv_id: str = "", limit: int = 100, offset: int = 0,
1371
+ *, all_: bool = False,
1218
1372
  ) -> list[dict]:
1219
- """GET {BE}/ml-api/jira-projects?limit=&offset=&pvId= — project records.
1220
-
1221
- Scoped to one product version. Used to resolve an existing project (match
1222
- on `key`) before falling back to create_jira_project. Pagination is the
1223
- caller's: `total` in the envelope is ignored. Returns [] on any failure —
1224
- unlike list_todo_tasks_raw, a failure is logged rather than silent, so a
1225
- broken gateway can't read as "this version has no projects".
1373
+ """GET {BE}/ml-api/jira-projects — project records.
1374
+
1375
+ Two fixes over the original, both of which made an unfiltered listing
1376
+ impossible:
1377
+
1378
+ 1. `pvId` is now OMITTED when blank. It used to be sent unconditionally as
1379
+ `pvId=""`, which scopes the query to nothing, so there was no way to
1380
+ ask "every project I can see". That matters because `pvId` is null on
1381
+ the large majority of live rows (57 projects, 3 with a pvId at time of
1382
+ writing) — a pvId-only client can reach almost none of the boards a
1383
+ person would actually name.
1384
+ 2. The envelope is read TOLERANTLY. This endpoint returns `data` as a
1385
+ FLAT LIST; the original read `data.records` only, so an unfiltered call
1386
+ would have returned `[]` even once (1) was fixed. Both shapes are now
1387
+ accepted, plus a bare `data.record`.
1388
+
1389
+ Pass `all_=True` to send the gateway's `all=true` (no pagination) instead
1390
+ of `limit`/`offset`. Match on `key` to resolve an existing project before
1391
+ falling back to `create_jira_project` — `key` is the same string a ticket
1392
+ carries as `projectKey`.
1393
+
1394
+ Returns [] on any failure, logged rather than silent, so a broken gateway
1395
+ cannot read as "there are no projects".
1226
1396
  """
1227
1397
  from sdk.config import BE_DEFAULT_URL
1228
1398
 
@@ -1233,25 +1403,43 @@ class BackendClient:
1233
1403
  "developer-name": "null",
1234
1404
  "product": "",
1235
1405
  }
1406
+ params: dict[str, object] = {"offset": offset}
1407
+ if all_:
1408
+ params["all"] = "true"
1409
+ else:
1410
+ params["limit"] = limit
1411
+ # Only scope by product when there is one — see (1) above.
1412
+ if str(pv_id or "").strip():
1413
+ params["pvId"] = str(pv_id).strip()
1236
1414
  try:
1237
- # All three params are always sent — the wire URL keeps its documented
1238
- # shape even when the caller has no pvId to scope by.
1239
1415
  async with httpx.AsyncClient(timeout=30.0) as client:
1240
- resp = await client.get(
1241
- url,
1242
- params={"limit": limit, "offset": offset, "pvId": str(pv_id or "")},
1243
- headers=headers,
1244
- )
1416
+ resp = await client.get(url, params=params, headers=headers)
1245
1417
  if resp.status_code >= 400:
1246
1418
  logger.warning("client.list_jira_projects failed status=%s body=%s",
1247
1419
  resp.status_code, resp.text[:300])
1248
1420
  return []
1249
- records = ((resp.json() or {}).get("data") or {}).get("records") or []
1250
- return records if isinstance(records, list) else []
1421
+ return _jira_project_records(resp.json() or {})
1251
1422
  except Exception as exc: # noqa: BLE001
1252
1423
  logger.warning("client.list_jira_projects error err=%s", exc)
1253
1424
  return []
1254
1425
 
1426
+ async def find_jira_project_by_key(self, key: str) -> dict | None:
1427
+ """The project whose `key` matches, case-insensitively, or None.
1428
+
1429
+ The verification step before filing tickets: a key that no longer resolves
1430
+ means the board was renamed or deleted, and creating N tickets against it
1431
+ would fail N times with nothing naming the cause. Matched locally over the
1432
+ full list because the gateway exposes no `?key=` filter.
1433
+ """
1434
+ wanted = str(key or "").strip().casefold()
1435
+ if not wanted:
1436
+ return None
1437
+ for record in await self.list_jira_projects(all_=True):
1438
+ candidate = record.get("key")
1439
+ if isinstance(candidate, str) and candidate.strip().casefold() == wanted:
1440
+ return record
1441
+ return None
1442
+
1255
1443
  async def add_comment(self, task_id: str, body: str) -> bool:
1256
1444
  """POST /tasks-ml/{id}/comments — add a comment to a ticket (best-effort).
1257
1445