devops-bot-sdk 1.6.57__tar.gz → 1.6.60__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 (228) hide show
  1. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
  4. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/cli.py +168 -1
  6. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/client.py +85 -0
  7. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ticket_fields.py +51 -0
  8. devops_bot_sdk-1.6.60/sdk/tickets.py +177 -0
  9. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/api.py +58 -0
  10. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/models.py +32 -0
  11. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/app.js +122 -0
  12. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/styles.css +4 -0
  13. devops_bot_sdk-1.6.60/sdk/ui/static/tickets.html +119 -0
  14. devops_bot_sdk-1.6.60/tests/test_ticket_cli.py +342 -0
  15. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ticket_fields.py +6 -1
  16. devops_bot_sdk-1.6.57/sdk/ui/static/tickets.html +0 -47
  17. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/README.md +0 -0
  18. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  19. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  20. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/requires.txt +0 -0
  21. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  22. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/pyproject.toml +0 -0
  23. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/__init__.py +0 -0
  24. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/classifier.py +0 -0
  25. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/__init__.py +0 -0
  26. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/common.py +0 -0
  27. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/linux_auditd.py +0 -0
  28. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/macos_eslogger.py +0 -0
  29. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/windows_eventlog.py +0 -0
  30. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/models.py +0 -0
  31. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/os_setup.py +0 -0
  32. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/recorder.py +0 -0
  33. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/redact.py +0 -0
  34. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/settings.py +0 -0
  35. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/shipper.py +0 -0
  36. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/spool.py +0 -0
  37. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agentd.py +0 -0
  38. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/__init__.py +0 -0
  39. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/activity.py +0 -0
  40. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/builder.py +0 -0
  41. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/chat.py +0 -0
  42. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/connectors.py +0 -0
  43. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/__init__.py +0 -0
  44. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/claude_code.py +0 -0
  45. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/http.py +0 -0
  46. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/mcp.py +0 -0
  47. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/n8n.py +0 -0
  48. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/shell.py +0 -0
  49. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/integrations.py +0 -0
  50. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/inventory.py +0 -0
  51. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/library/__init__.py +0 -0
  52. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/manifest.py +0 -0
  53. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/prompt_studio.py +0 -0
  54. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/prompt_template.py +0 -0
  55. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/prompt_vars.py +0 -0
  56. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/run_log.py +0 -0
  57. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/schedules.py +0 -0
  58. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/secrets.py +0 -0
  59. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/ticket_run.py +0 -0
  60. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/usage.py +0 -0
  61. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/varstore.py +0 -0
  62. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/browser_profile.py +0 -0
  63. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/__init__.py +0 -0
  64. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/files.py +0 -0
  65. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/process.py +0 -0
  66. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/screenshot.py +0 -0
  67. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/config.py +0 -0
  68. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/crucial.py +0 -0
  69. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/departments.py +0 -0
  70. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/__init__.py +0 -0
  71. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/assets.py +0 -0
  72. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/bootstrap.py +0 -0
  73. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/browser.py +0 -0
  74. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/compare.py +0 -0
  75. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/loop.py +0 -0
  76. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/estimator.py +0 -0
  77. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/exceptions.py +0 -0
  78. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/firewall.py +0 -0
  79. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/formats.py +0 -0
  80. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/git_ops.py +0 -0
  81. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/github_fetch.py +0 -0
  82. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/github_ingest.py +0 -0
  83. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/graph_recon.py +0 -0
  84. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/graphify.py +0 -0
  85. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/hooks/__init__.py +0 -0
  86. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/hooks/crucial_guard.py +0 -0
  87. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/intake.py +0 -0
  88. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ipc/__init__.py +0 -0
  89. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ipc/electron_bridge.py +0 -0
  90. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ipc/handlers.py +0 -0
  91. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/journal.py +0 -0
  92. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/local_exec.py +0 -0
  93. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/memory_guard.py +0 -0
  94. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/__init__.py +0 -0
  95. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/envelope.py +0 -0
  96. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/requests.py +0 -0
  97. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/responses.py +0 -0
  98. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/snapshots.py +0 -0
  99. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/phase_timer.py +0 -0
  100. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/plan_run.py +0 -0
  101. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/platform_compat.py +0 -0
  102. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/prd_progress.py +0 -0
  103. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/proof_readers.py +0 -0
  104. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/providers.py +0 -0
  105. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/py.typed +0 -0
  106. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/resume.py +0 -0
  107. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/router.py +0 -0
  108. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/run_auto.py +0 -0
  109. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/scheduler.py +0 -0
  110. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/self_report.py +0 -0
  111. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/self_report_local.py +0 -0
  112. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/sse.py +0 -0
  113. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/swagger.py +0 -0
  114. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/task_file.py +0 -0
  115. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/task_types.py +0 -0
  116. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/test.py +0 -0
  117. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/test_pipeline.py +0 -0
  118. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/tiers/__init__.py +0 -0
  119. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/tiers/mini_agent.py +0 -0
  120. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/__init__.py +0 -0
  121. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/dashboard.py +0 -0
  122. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/health.py +0 -0
  123. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/ip_allowlist.py +0 -0
  124. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/jobs.py +0 -0
  125. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/runner.py +0 -0
  126. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/security.py +0 -0
  127. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/server.py +0 -0
  128. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/service.py +0 -0
  129. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/session.py +0 -0
  130. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/setup_manager.py +0 -0
  131. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/agents.html +0 -0
  132. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/chat.html +0 -0
  133. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/custom-agents.html +0 -0
  134. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/default-agents.html +0 -0
  135. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/index.html +0 -0
  136. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/report.html +0 -0
  137. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/runs.html +0 -0
  138. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/schedules.html +0 -0
  139. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/setup.html +0 -0
  140. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/swagger.py +0 -0
  141. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/updater.py +0 -0
  142. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/url_access.py +0 -0
  143. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  144. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/vps_health.py +0 -0
  145. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/write_hook.py +0 -0
  146. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/setup.cfg +0 -0
  147. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_collectors.py +0 -0
  148. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_core.py +0 -0
  149. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_macos.py +0 -0
  150. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_os_setup.py +0 -0
  151. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_pipeline.py +0 -0
  152. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_activity.py +0 -0
  153. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_activity_tracking.py +0 -0
  154. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_chat_features.py +0 -0
  155. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_listing.py +0 -0
  156. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_prompt_async.py +0 -0
  157. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_prompt_models.py +0 -0
  158. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_run_ticket.py +0 -0
  159. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_schedules.py +0 -0
  160. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_workspace_settings.py +0 -0
  161. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agents_connectors_builder.py +0 -0
  162. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_allowed_tools_config.py +0 -0
  163. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_auto_agent_dispatch.py +0 -0
  164. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_browser_profile.py +0 -0
  165. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_cheap_tier.py +0 -0
  166. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_client_jira_projects.py +0 -0
  167. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_config_token_diagnostics.py +0 -0
  168. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_configure_token_owner.py +0 -0
  169. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_console_encoding.py +0 -0
  170. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_crash_recovery.py +0 -0
  171. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_departments.py +0 -0
  172. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_done_transition.py +0 -0
  173. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_epic_decompose_run.py +0 -0
  174. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_epic_inherited_agent_id.py +0 -0
  175. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_epic_session_isolation.py +0 -0
  176. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_estimator.py +0 -0
  177. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_fallback_cli.py +0 -0
  178. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_firewall_open_on_update.py +0 -0
  179. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_formats.py +0 -0
  180. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_git_action.py +0 -0
  181. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_github_fetch.py +0 -0
  182. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_github_ingest.py +0 -0
  183. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_graph_recon.py +0 -0
  184. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_graphify_build.py +0 -0
  185. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_graphify_hook_install.py +0 -0
  186. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_health.py +0 -0
  187. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_intake_job_outcome.py +0 -0
  188. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_intake_plan_review.py +0 -0
  189. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_journal.py +0 -0
  190. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_local_exec_provider.py +0 -0
  191. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_mcp_route_contract.py +0 -0
  192. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_memory_guard.py +0 -0
  193. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_phase_timer.py +0 -0
  194. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_plan_new_command.py +0 -0
  195. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_plan_run_activity.py +0 -0
  196. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_pr_identity.py +0 -0
  197. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_prd_artifact.py +0 -0
  198. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_project_path_resolution.py +0 -0
  199. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_proof_readers.py +0 -0
  200. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_providers.py +0 -0
  201. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_recon_session_isolation.py +0 -0
  202. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_resume.py +0 -0
  203. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_router.py +0 -0
  204. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_run_auto_agent_only.py +0 -0
  205. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_run_outputs.py +0 -0
  206. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_run_verdict_is_honest.py +0 -0
  207. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_schedule_api.py +0 -0
  208. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_schedule_outputs_and_plans.py +0 -0
  209. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_scheduler.py +0 -0
  210. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_self_report.py +0 -0
  211. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_service_token_warning.py +0 -0
  212. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_stale_panel_restart.py +0 -0
  213. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_swagger_command.py +0 -0
  214. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_task_file_and_plan_run.py +0 -0
  215. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_task_file_format_doc.py +0 -0
  216. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ticket_run_resolve_by_id.py +0 -0
  217. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ticket_speed_fixes.py +0 -0
  218. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_tier_flag_parity.py +0 -0
  219. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ui_gate.py +0 -0
  220. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ui_lifespan.py +0 -0
  221. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_update_timer.py +0 -0
  222. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_updater_extras.py +0 -0
  223. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_updater_pep668.py +0 -0
  224. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_updater_reports_the_truth.py +0 -0
  225. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_url_access.py +0 -0
  226. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_vps_health.py +0 -0
  227. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_windows_loop_identity.py +0 -0
  228. {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_write_hook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.57
3
+ Version: 1.6.60
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.57
3
+ Version: 1.6.60
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
@@ -47,6 +47,7 @@ sdk/task_types.py
47
47
  sdk/test.py
48
48
  sdk/test_pipeline.py
49
49
  sdk/ticket_fields.py
50
+ sdk/tickets.py
50
51
  sdk/updater.py
51
52
  sdk/url_access.py
52
53
  sdk/vps_health.py
@@ -207,6 +208,7 @@ tests/test_stale_panel_restart.py
207
208
  tests/test_swagger_command.py
208
209
  tests/test_task_file_and_plan_run.py
209
210
  tests/test_task_file_format_doc.py
211
+ tests/test_ticket_cli.py
210
212
  tests/test_ticket_fields.py
211
213
  tests/test_ticket_run_resolve_by_id.py
212
214
  tests/test_ticket_speed_fixes.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.52
3
+ Version: 1.6.60
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.57" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.60" # 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
@@ -49,6 +49,21 @@ Command surface:
49
49
  agentos agent rm <name>
50
50
  De-register the agent from the backend and delete its local manifest.
51
51
 
52
+ agentos ticket create --project KEY --summary TEXT [--type T] [--priority P]
53
+ [--assignee EMAIL] [--description[-file] …] [--instructions[-file] …]
54
+ [--parent ID] [--status S] [--points N]
55
+ File one ticket (POST /tasks-ml) and print its Jira key. Type defaults to
56
+ Task, priority to Medium, assignee to the token owner. Validated locally
57
+ first; a gateway rejection prints the gateway's own messages.
58
+ agentos ticket update KEY_OR_ID [--summary …] [--description[-file] …]
59
+ [--instructions[-file] …] [--priority P] [--status S] [--points N]
60
+ [--assignee EMAIL] [--type T]
61
+ Change only the given fields (PUT /tasks-ml/{id}). Takes a Jira key or
62
+ the platform id. See sdk.tickets — the Tickets page uses the same code.
63
+ agentos ticket delete KEY_OR_ID [--yes]
64
+ Show the ticket and, after a y/N confirmation (skipped by --yes), delete it
65
+ (DELETE /tasks-ml/{id}). Cannot be undone.
66
+
52
67
  agentos integration add <path>
53
68
  Register an integration (MCP server, etc.) from a JSON file.
54
69
  agentos integration test <name>
@@ -162,6 +177,10 @@ def main() -> None:
162
177
  _integration_command(rest)
163
178
  return
164
179
 
180
+ if noun == "ticket":
181
+ _ticket_command(rest)
182
+ return
183
+
165
184
  if noun == "agentd":
166
185
  _agentd_command(rest)
167
186
  return
@@ -202,12 +221,13 @@ def main() -> None:
202
221
 
203
222
 
204
223
  def _print_usage() -> None:
205
- print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser|fallback|firewall|activity-setup|swagger> ...")
224
+ print("usage: agentos <configure|chat|agent|ticket|integration|default-agents|agentd|github|report|browser|fallback|firewall|activity-setup|swagger> ...")
206
225
  print()
207
226
  print(" agentos configure [--rotate]")
208
227
  print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
209
228
  print(" agentos agent <list|run NAME|chat NAME MSG|logs NAME|enable NAME|disable NAME|rm NAME>")
210
229
  print(" agentos agent <set-key NAME KEY[=VALUE]…|keys NAME|rm-key NAME KEY…> # persistent API keys (custom agents)")
230
+ print(" agentos ticket <create|update KEY_OR_ID|delete KEY_OR_ID|help>")
211
231
  print(" agentos integration <add PATH|test NAME|list|rm NAME>")
212
232
  print(" agentos default-agents list [--department-id UUID] # read-only; agents are created in the web app")
213
233
  print(" agentos agentd <start [--foreground]|status>")
@@ -625,6 +645,153 @@ def _pop_department_id(args: list[str]) -> tuple[str | None, list[str]]:
625
645
  return dept_id, out
626
646
 
627
647
 
648
+ # `agentos ticket` flag → POST/PUT /tasks-ml wire name. Text flags have a
649
+ # `-file` twin that reads the value from a file instead.
650
+ _TICKET_FLAGS = {
651
+ "--project": "projectKey", "--type": "type", "--summary": "summary",
652
+ "--priority": "priority", "--assignee": "assigneeEmail", "--status": "status",
653
+ "--points": "points", "--parent": "parentTaskId",
654
+ "--description": "description", "--instructions": "humanInstructions",
655
+ }
656
+ _TICKET_FILE_FLAGS = {"--description-file": "--description",
657
+ "--instructions-file": "--instructions"}
658
+ # Create-only: neither is editable through PUT /tasks-ml/{id}.
659
+ _TICKET_CREATE_ONLY = {"--project", "--parent"}
660
+
661
+
662
+ def _ticket_usage() -> None:
663
+ print("usage: agentos ticket <create|update KEY_OR_ID|delete KEY_OR_ID|help>")
664
+ print()
665
+ print(" agentos ticket create --project KEY --summary TEXT [--type TYPE] [--priority P]")
666
+ print(" [--assignee EMAIL] [--description TEXT | --description-file PATH]")
667
+ print(" [--instructions TEXT | --instructions-file PATH] [--parent ID] [--status S] [--points N]")
668
+ print(" agentos ticket update KEY_OR_ID [--summary TEXT] [--description TEXT | --description-file PATH]")
669
+ print(" [--instructions TEXT | --instructions-file PATH] [--priority P] [--status S]")
670
+ print(" [--points N] [--assignee EMAIL] [--type TYPE]")
671
+ print(" agentos ticket delete KEY_OR_ID [--yes] # asks to confirm unless --yes")
672
+ print()
673
+ print(" TYPE: Epic|Story|Task|Bug|Sub-task (default Task) P: Lowest|Low|Medium|High|Highest (default Medium)")
674
+ print(" --assignee defaults to the configured token's owner.")
675
+
676
+
677
+ def _parse_ticket_flags(args: list[str], *, creating: bool) -> dict[str, str]:
678
+ """Flags → wire fields. Exits 1 on an unknown flag, a missing value, both a
679
+ text flag and its -file twin, or an unreadable file."""
680
+ fields: dict[str, str] = {}
681
+ seen: set[str] = set()
682
+ i = 0
683
+ while i < len(args):
684
+ flag = args[i]
685
+ known = flag in _TICKET_FLAGS or flag in _TICKET_FILE_FLAGS
686
+ if not known or (not creating and flag in _TICKET_CREATE_ONLY):
687
+ print(f"error: unknown option for `agentos ticket {'create' if creating else 'update'}`: {flag}")
688
+ sys.exit(1)
689
+ if i + 1 >= len(args):
690
+ print(f"error: expected a value after {flag}")
691
+ sys.exit(1)
692
+ value = args[i + 1]
693
+ text_flag = _TICKET_FILE_FLAGS.get(flag, flag)
694
+ if text_flag in seen:
695
+ print(f"error: pass {text_flag} or {text_flag}-file once, not both")
696
+ sys.exit(1)
697
+ seen.add(text_flag)
698
+ if flag in _TICKET_FILE_FLAGS:
699
+ path = Path(value).expanduser()
700
+ try:
701
+ value = path.read_text(encoding="utf-8")
702
+ except OSError as exc:
703
+ print(f"error: {flag}: cannot read {path}: {exc.strerror or exc}")
704
+ sys.exit(1)
705
+ fields[_TICKET_FLAGS[text_flag]] = value
706
+ i += 2
707
+ return fields
708
+
709
+
710
+ def _ticket_command(args: list[str]) -> None:
711
+ """`agentos ticket create|update` — see sdk.tickets for the rules."""
712
+ verb = args[0] if args else "help"
713
+ if verb in {"help", "-h", "--help"}:
714
+ _ticket_usage()
715
+ sys.exit(0 if args else 1)
716
+ if verb not in {"create", "update", "delete"}:
717
+ _ticket_usage()
718
+ sys.exit(1)
719
+ if verb in {"update", "delete"} and (len(args) < 2 or args[1].startswith("--")):
720
+ _ticket_usage()
721
+ sys.exit(1)
722
+
723
+ from sdk import tickets
724
+ from sdk.client import BackendClient
725
+
726
+ if verb == "delete":
727
+ _ticket_delete(args[1], args[2:])
728
+ return
729
+
730
+ if verb == "create":
731
+ fields = _parse_ticket_flags(args[1:], creating=True)
732
+ if not fields.get("projectKey") or not fields.get("summary"):
733
+ print("error: --project and --summary are required")
734
+ sys.exit(1)
735
+ else:
736
+ fields = _parse_ticket_flags(args[2:], creating=False)
737
+ if not fields:
738
+ print("error: nothing to update: pass at least one field")
739
+ _ticket_usage()
740
+ sys.exit(1)
741
+ # Validate before building a client, so a bad flag never needs a token.
742
+ err = tickets.validate_fields(fields)
743
+ if err:
744
+ print(f"error: {err}")
745
+ sys.exit(1)
746
+
747
+ client = BackendClient.from_config()
748
+ if verb == "create":
749
+ res = asyncio.run(tickets.create_ticket(client, fields))
750
+ else:
751
+ res = asyncio.run(tickets.update_ticket(client, args[1], fields))
752
+ if not res["ok"]:
753
+ print(f"error: {res['error']}")
754
+ sys.exit(1)
755
+ label = res["key"] or res["id"]
756
+ if verb == "create":
757
+ print(f"created {label} (id {res['id']})")
758
+ else:
759
+ print(f"updated {label}: {', '.join(sorted(fields))}")
760
+ sys.exit(0)
761
+
762
+
763
+ def _ticket_delete(target: str, flags: list[str]) -> None:
764
+ """`agentos ticket delete KEY_OR_ID [--yes]` — shows the ticket, then asks."""
765
+ from sdk import tickets
766
+ from sdk.client import BackendClient
767
+
768
+ unknown = [f for f in flags if f not in {"--yes", "-y"}]
769
+ if unknown:
770
+ print(f"error: unknown option for `agentos ticket delete`: {unknown[0]}")
771
+ sys.exit(1)
772
+ client = BackendClient.from_config()
773
+ rec = asyncio.run(tickets.get_ticket(client, target))
774
+ if rec is None:
775
+ print(f"error: ticket not found: {target}")
776
+ sys.exit(1)
777
+ label = rec.get("key") or rec.get("id")
778
+ if not flags:
779
+ print(f"{label} [{rec.get('status') or '?'}] {rec.get('summary') or ''}")
780
+ try:
781
+ answer = input(f"Delete {label}? This cannot be undone. [y/N] ")
782
+ except EOFError:
783
+ answer = ""
784
+ if answer.strip().lower() not in {"y", "yes"}:
785
+ print("not deleted")
786
+ sys.exit(1)
787
+ res = asyncio.run(tickets.delete_ticket(client, str(rec.get("id") or target)))
788
+ if not res["ok"]:
789
+ print(f"error: {res['error']}")
790
+ sys.exit(1)
791
+ print(f"deleted {label}")
792
+ sys.exit(0)
793
+
794
+
628
795
  def _resolve_department_id(explicit: str | None) -> str:
629
796
  """Use the explicit --department-id, else the caller's own from bootstrap()."""
630
797
  if explicit:
@@ -249,6 +249,7 @@ class BackendClient:
249
249
  # busy backend (retry in 30 min) from a rejected request (wait for the
250
250
  # next slot). Empty dict means "no failure recorded".
251
251
  self.last_ingest_error: dict = {}
252
+ self.last_task_ml_error: dict = {}
252
253
  self._headers = {
253
254
  "Authorization": token, # co_<uuid>_<hex> — no Bearer prefix
254
255
  "X-SDK-Version": SDK_VERSION,
@@ -1274,6 +1275,48 @@ class BackendClient:
1274
1275
  logger.warning("client.get_task_status list failed task=%s err=%s", task_id, exc)
1275
1276
  return None
1276
1277
 
1278
+ async def get_task_ml(self, task_id: str) -> dict | None:
1279
+ """GET {BE}/tasks-ml/{id} → the full RAW record (jiraIssueKey, summary, …), or None.
1280
+
1281
+ Same read as ``get_task_status`` — direct single-record read, then a scan
1282
+ of the list endpoint for a backend that doesn't serve it — but returns the
1283
+ whole record. Matches `id` or the Jira key. Best-effort: never raises.
1284
+ """
1285
+ from sdk.config import BE_DEFAULT_URL
1286
+
1287
+ if not task_id:
1288
+ return None
1289
+ base = BE_DEFAULT_URL.rstrip("/")
1290
+ headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
1291
+ try:
1292
+ async with httpx.AsyncClient(timeout=20.0) as client:
1293
+ resp = await client.get(f"{base}/tasks-ml/{task_id}", headers=headers)
1294
+ if resp.status_code < 400:
1295
+ data = (resp.json() or {}).get("data") or {}
1296
+ if isinstance(data, dict):
1297
+ rec = data.get("record") if isinstance(data.get("record"), dict) else data
1298
+ if rec.get("id"):
1299
+ return rec
1300
+ except Exception as exc: # noqa: BLE001 — fall through to the list scan
1301
+ logger.warning("client.get_task_ml direct failed task=%s err=%s", task_id, exc)
1302
+ try:
1303
+ async with httpx.AsyncClient(timeout=30.0) as client:
1304
+ resp = await client.get(f"{base}/tasks-ml", params={"limit": 10000},
1305
+ headers=headers)
1306
+ if resp.status_code >= 400:
1307
+ return None
1308
+ records = ((resp.json() or {}).get("data") or {}).get("records") or []
1309
+ want = str(task_id).strip().upper()
1310
+ for rec in records:
1311
+ if isinstance(rec, dict) and want in {
1312
+ str(rec.get(k) or "").strip().upper()
1313
+ for k in ("id", "jiraIssueKey", "jiraKey")
1314
+ }:
1315
+ return rec
1316
+ except Exception as exc: # noqa: BLE001
1317
+ logger.warning("client.get_task_ml list failed task=%s err=%s", task_id, exc)
1318
+ return None
1319
+
1277
1320
  async def set_task_status(self, task_id: str, status: str) -> bool:
1278
1321
  """PUT {BE}/tasks-ml/{id} status=<status> (best-effort, arbitrary target).
1279
1322
 
@@ -1435,11 +1478,17 @@ class BackendClient:
1435
1478
  async with httpx.AsyncClient(timeout=30.0) as client:
1436
1479
  resp = await client.put(url, files=files, headers=headers)
1437
1480
  if resp.status_code >= 400:
1481
+ try:
1482
+ self.last_task_ml_error = resp.json() or {}
1483
+ except Exception: # noqa: BLE001
1484
+ self.last_task_ml_error = {"error": resp.text or "request failed"}
1438
1485
  logger.warning("client.update_task_ml failed task=%s status=%s body=%s",
1439
1486
  task_id, resp.status_code, resp.text[:200])
1440
1487
  return False
1488
+ self.last_task_ml_error = {}
1441
1489
  return True
1442
1490
  except Exception as exc: # noqa: BLE001
1491
+ self.last_task_ml_error = {"error": str(exc)}
1443
1492
  logger.warning("client.update_task_ml error task=%s err=%s", task_id, exc)
1444
1493
  return False
1445
1494
 
@@ -1458,6 +1507,7 @@ class BackendClient:
1458
1507
  from sdk.config import BE_DEFAULT_URL
1459
1508
  from sdk.ticket_fields import to_multipart
1460
1509
 
1510
+ self.last_task_ml_error = {}
1461
1511
  url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml"
1462
1512
  headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
1463
1513
  headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
@@ -1468,9 +1518,14 @@ class BackendClient:
1468
1518
  async with httpx.AsyncClient(timeout=30.0) as client:
1469
1519
  resp = await client.post(url, files=files, headers=headers)
1470
1520
  if resp.status_code >= 400:
1521
+ try:
1522
+ self.last_task_ml_error = resp.json() or {}
1523
+ except Exception: # noqa: BLE001
1524
+ self.last_task_ml_error = {"error": resp.text or "request failed"}
1471
1525
  logger.warning("client.create_task_ml failed status=%s body=%s",
1472
1526
  resp.status_code, resp.text[:300])
1473
1527
  return None
1528
+ self.last_task_ml_error = {}
1474
1529
  body = resp.json() or {}
1475
1530
  rec = body.get("data") or body
1476
1531
  # The created record's id may sit at data.id or data.record.id.
@@ -1478,9 +1533,39 @@ class BackendClient:
1478
1533
  return rec.get("id") or ((rec.get("record") or {}).get("id"))
1479
1534
  return None
1480
1535
  except Exception as exc: # noqa: BLE001
1536
+ self.last_task_ml_error = {"error": str(exc)}
1481
1537
  logger.warning("client.create_task_ml error err=%s", exc)
1482
1538
  return None
1483
1539
 
1540
+ async def delete_task_ml(self, task_id: str) -> bool:
1541
+ """DELETE {BE}/tasks-ml/{id} — remove a ticket. True on 2xx.
1542
+
1543
+ On failure the gateway's JSON body (e.g. ``{"message": "Task not found"}``)
1544
+ is kept in ``last_task_ml_error``, as for create/update. Never raises.
1545
+ """
1546
+ from sdk.config import BE_DEFAULT_URL
1547
+
1548
+ url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}"
1549
+ headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
1550
+ headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
1551
+ try:
1552
+ async with httpx.AsyncClient(timeout=30.0) as client:
1553
+ resp = await client.delete(url, headers=headers)
1554
+ if resp.status_code >= 400:
1555
+ try:
1556
+ self.last_task_ml_error = resp.json() or {}
1557
+ except Exception: # noqa: BLE001
1558
+ self.last_task_ml_error = {"error": resp.text or "request failed"}
1559
+ logger.warning("client.delete_task_ml failed task=%s status=%s body=%s",
1560
+ task_id, resp.status_code, resp.text[:200])
1561
+ return False
1562
+ self.last_task_ml_error = {}
1563
+ return True
1564
+ except Exception as exc: # noqa: BLE001
1565
+ self.last_task_ml_error = {"error": str(exc)}
1566
+ logger.warning("client.delete_task_ml error task=%s err=%s", task_id, exc)
1567
+ return False
1568
+
1484
1569
  # ── Jira projects ──────────────────────────────────────────────────
1485
1570
  # A Jira project is the container a ticket's `projectKey` points at, so these
1486
1571
  # sit next to create_task_ml: a flow that has no projectKey yet resolves one
@@ -23,6 +23,10 @@ from __future__ import annotations
23
23
  MultipartPart = tuple[str, tuple[None, str]]
24
24
  MultipartBody = list[MultipartPart]
25
25
 
26
+ VALID_TICKET_TYPES = frozenset({"Epic", "Story", "Task", "Bug", "Sub-task"})
27
+ VALID_PRIORITIES = frozenset({"Lowest", "Low", "Medium", "High", "Highest"})
28
+ MAX_SUMMARY_LENGTH = 255
29
+
26
30
  # The field names the SDK sends to POST /tasks-ml, from the real call sites in
27
31
  # sdk/ipc/handlers.py (_epic_inherited_fields, and the child-story `fields` dict
28
32
  # in the Epic decomposition loop). Documentation and an assertion target for
@@ -90,6 +94,46 @@ def is_sla_field(key: str) -> bool:
90
94
  return _normalise(key) in SLA_FIELDS
91
95
 
92
96
 
97
+ def validate_ticket_type(value: str) -> str | None:
98
+ """Return an error string when `value` is not an allowed ticket type."""
99
+ if value in VALID_TICKET_TYPES:
100
+ return None
101
+ return (
102
+ "invalid type: "
103
+ f"{value!r}. Expected one of: {', '.join(sorted(VALID_TICKET_TYPES))}"
104
+ )
105
+
106
+
107
+ def validate_priority(value: str) -> str | None:
108
+ """Return an error string when `value` is not an allowed priority."""
109
+ if value in VALID_PRIORITIES:
110
+ return None
111
+ return (
112
+ "invalid priority: "
113
+ f"{value!r}. Expected one of: {', '.join(sorted(VALID_PRIORITIES))}"
114
+ )
115
+
116
+
117
+ def validate_email(value: str) -> str | None:
118
+ """Return an error string when `value` is not shaped like an email."""
119
+ text = str(value or "").strip()
120
+ if text and "@" in text and not text.startswith("@") and not text.endswith("@"):
121
+ local, _, domain = text.partition("@")
122
+ if local and domain and "." in domain and not domain.startswith(".") and not domain.endswith("."):
123
+ return None
124
+ return f"invalid assignee email: {value!r}"
125
+
126
+
127
+ def validate_summary_length(value: str) -> str | None:
128
+ """Return an error string when `summary` exceeds the backend limit."""
129
+ if len(value) <= MAX_SUMMARY_LENGTH:
130
+ return None
131
+ return (
132
+ f"summary must be {MAX_SUMMARY_LENGTH} characters or fewer "
133
+ f"(got {len(value)})"
134
+ )
135
+
136
+
93
137
  def to_multipart(
94
138
  fields: dict[str, object],
95
139
  model_used: list[str] | None = None,
@@ -141,9 +185,16 @@ def to_multipart(
141
185
  # The field contract itself. The MultipartPart/MultipartBody aliases above are
142
186
  # importable annotation helpers, not part of the contract, so they stay out.
143
187
  __all__ = [
188
+ "MAX_SUMMARY_LENGTH",
144
189
  "OMITTED_FIELDS",
145
190
  "SLA_FIELDS",
146
191
  "TASKS_ML_FIELDS",
192
+ "VALID_PRIORITIES",
193
+ "VALID_TICKET_TYPES",
147
194
  "is_sla_field",
148
195
  "to_multipart",
196
+ "validate_email",
197
+ "validate_priority",
198
+ "validate_summary_length",
199
+ "validate_ticket_type",
149
200
  ]
@@ -0,0 +1,177 @@
1
+ """Create, edit and delete tickets — the one implementation behind `agentos ticket` and
2
+ the control panel's Tickets page.
3
+
4
+ Both front ends gather the same flags/fields, so the rules live here once:
5
+ validation against the gateway's POST /tasks-ml contract (``sdk.ticket_fields``),
6
+ the assignee default (the token owner's email), Jira-key → platform-id
7
+ resolution, and reading the created ticket's Jira key back. Every function
8
+ returns a result dict instead of printing or raising, so the CLI can print it
9
+ and the HTTP layer can turn it into a status code.
10
+
11
+ Result shape: ``{"ok": bool, "id": str, "key": str, "error": str}``, plus
12
+ ``"details"`` (the gateway's JSON error body) when the backend rejected the call.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import Any
18
+
19
+ from sdk.ticket_fields import (
20
+ validate_email,
21
+ validate_priority,
22
+ validate_summary_length,
23
+ validate_ticket_type,
24
+ )
25
+
26
+ # PROJ-123. A platform id is a UUID, which also contains dashes, so "has a dash"
27
+ # is not enough to tell the two apart.
28
+ _JIRA_KEY_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*-\d+$")
29
+
30
+ # Editable fields, by wire name. `None` drops a field; anything else is sent.
31
+ EDITABLE_FIELDS = (
32
+ "summary", "description", "humanInstructions", "priority", "status",
33
+ "points", "assigneeEmail", "type",
34
+ )
35
+
36
+
37
+ def is_jira_key(value: str) -> bool:
38
+ return bool(_JIRA_KEY_RE.match(str(value or "").strip()))
39
+
40
+
41
+ def _fail(error: str, **extra: Any) -> dict:
42
+ return {"ok": False, "id": "", "key": "", "error": error, **extra}
43
+
44
+
45
+ def validate_fields(fields: dict[str, Any]) -> str | None:
46
+ """The first validation error in `fields` (only the fields present), or None."""
47
+ if fields.get("summary") is not None:
48
+ if not str(fields["summary"]).strip():
49
+ return "summary must not be empty"
50
+ err = validate_summary_length(str(fields["summary"]))
51
+ if err:
52
+ return err
53
+ if fields.get("type") is not None and (err := validate_ticket_type(str(fields["type"]))):
54
+ return err
55
+ if fields.get("priority") is not None and (err := validate_priority(str(fields["priority"]))):
56
+ return err
57
+ if fields.get("assigneeEmail") is not None and (err := validate_email(str(fields["assigneeEmail"]))):
58
+ return err
59
+ if fields.get("projectKey") is not None and len(str(fields["projectKey"])) > 50:
60
+ return "project key must be 50 characters or fewer"
61
+ points = fields.get("points")
62
+ if points is not None and str(points).strip() != "":
63
+ try:
64
+ if int(str(points)) < 0:
65
+ raise ValueError
66
+ except ValueError:
67
+ return f"points must be a whole number of 0 or more (got {points!r})"
68
+ return None
69
+
70
+
71
+ async def owner_email(client) -> str:
72
+ """The configured token owner's email, or "" when it can't be read."""
73
+ try:
74
+ return str((await client.profile()).email or "").strip()
75
+ except Exception: # noqa: BLE001
76
+ return ""
77
+
78
+
79
+ async def resolve_id(client, key_or_id: str) -> str | None:
80
+ """Platform task id for a Jira key (AP2-123) or an id; None when not found."""
81
+ target = str(key_or_id or "").strip()
82
+ if not target:
83
+ return None
84
+ if not is_jira_key(target):
85
+ return target
86
+ rows = await client.list_tasks_by_keys([target])
87
+ if not rows:
88
+ return None
89
+ return str(rows[0].get("id") or "") or None
90
+
91
+
92
+ async def create_ticket(client, fields: dict[str, Any]) -> dict:
93
+ """POST /tasks-ml. `fields` uses wire names; type/priority default to Task/Medium
94
+ and assigneeEmail to the token owner. Validates before any HTTP call."""
95
+ body = {k: v for k, v in fields.items() if v is not None and v != ""}
96
+ body.setdefault("type", "Task")
97
+ body.setdefault("priority", "Medium")
98
+ if not str(body.get("projectKey") or "").strip():
99
+ return _fail("project is required")
100
+ if not str(body.get("summary") or "").strip():
101
+ return _fail("summary is required")
102
+ err = validate_fields(body)
103
+ if err:
104
+ return _fail(err)
105
+ if not body.get("assigneeEmail"):
106
+ email = await owner_email(client)
107
+ if not email:
108
+ return _fail("assignee is required: the token owner's email could not be read")
109
+ body["assigneeEmail"] = email
110
+
111
+ task_id = await client.create_task_ml(body)
112
+ if not task_id:
113
+ details = getattr(client, "last_task_ml_error", None) or {}
114
+ return _fail(_error_text(details) or "ticket create failed", details=details)
115
+ record = await client.get_task_ml(task_id) or {}
116
+ return {"ok": True, "id": str(task_id), "key": str(record.get("jiraIssueKey") or ""),
117
+ "error": ""}
118
+
119
+
120
+ async def update_ticket(client, key_or_id: str, fields: dict[str, Any]) -> dict:
121
+ """PUT /tasks-ml/{id} with only the given fields. Validates before any HTTP call."""
122
+ body = {k: v for k, v in fields.items() if k in EDITABLE_FIELDS and v is not None}
123
+ if not body:
124
+ return _fail("nothing to update: pass at least one field")
125
+ err = validate_fields(body)
126
+ if err:
127
+ return _fail(err)
128
+ task_id = await resolve_id(client, key_or_id)
129
+ if not task_id:
130
+ return _fail(f"ticket not found: {key_or_id}")
131
+ ok = await client.update_task_ml(task_id, body)
132
+ if not ok:
133
+ details = getattr(client, "last_task_ml_error", None) or {}
134
+ return _fail(_error_text(details) or "ticket update failed", details=details)
135
+ return {"ok": True, "id": task_id,
136
+ "key": key_or_id if is_jira_key(key_or_id) else "", "error": ""}
137
+
138
+
139
+ async def delete_ticket(client, key_or_id: str) -> dict:
140
+ """DELETE /tasks-ml/{id}. Irreversible — callers confirm with the person first."""
141
+ task_id = await resolve_id(client, key_or_id)
142
+ if not task_id:
143
+ return _fail(f"ticket not found: {key_or_id}")
144
+ if not await client.delete_task_ml(task_id):
145
+ details = getattr(client, "last_task_ml_error", None) or {}
146
+ error = _error_text(details) or "ticket delete failed"
147
+ if error.lower() == "task not found":
148
+ error = f"ticket not found: {key_or_id}"
149
+ return _fail(error, details=details)
150
+ return {"ok": True, "id": task_id,
151
+ "key": key_or_id if is_jira_key(key_or_id) else "", "error": ""}
152
+
153
+
154
+ async def get_ticket(client, key_or_id: str) -> dict | None:
155
+ """The editable view of one ticket, or None when it can't be found."""
156
+ task_id = await resolve_id(client, key_or_id)
157
+ record = await client.get_task_ml(task_id) if task_id else None
158
+ if not record:
159
+ return None
160
+ out = {k: record.get(k) for k in EDITABLE_FIELDS if k != "assigneeEmail"}
161
+ out.update({"id": record.get("id") or task_id,
162
+ "key": record.get("jiraIssueKey") or record.get("jiraKey") or "",
163
+ "projectKey": record.get("projectKey") or ""})
164
+ return out
165
+
166
+
167
+ def _error_text(details: Any) -> str:
168
+ """One line from the gateway's error body: `{"message": [...]}` / `{"errors": ...}`."""
169
+ if not isinstance(details, dict):
170
+ return str(details or "")
171
+ for key in ("message", "errors", "error", "detail"):
172
+ val = details.get(key)
173
+ if isinstance(val, list) and val:
174
+ return "; ".join(str(v) for v in val)
175
+ if val:
176
+ return str(val)
177
+ return ""