devops-bot-sdk 1.6.68__tar.gz → 1.6.70__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 (235) hide show
  1. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/SOURCES.txt +5 -0
  4. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/os_setup.py +5 -0
  6. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/shipper.py +46 -1
  7. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/activity.py +1 -1
  8. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/chat.py +51 -1
  9. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/client.py +39 -8
  10. devops_bot_sdk-1.6.70/sdk/schedule_tickets.py +201 -0
  11. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/scheduler.py +119 -15
  12. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/vps_health.py +9 -1
  13. devops_bot_sdk-1.6.70/sdk/vps_identity.py +174 -0
  14. devops_bot_sdk-1.6.70/tests/test_schedule_tickets.py +218 -0
  15. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_scheduler.py +36 -0
  16. devops_bot_sdk-1.6.70/tests/test_scheduler_transient_retry.py +230 -0
  17. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_vps_health.py +9 -0
  18. devops_bot_sdk-1.6.70/tests/test_vps_only_reporting.py +234 -0
  19. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/README.md +0 -0
  20. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  21. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  22. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/requires.txt +0 -0
  23. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  24. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/pyproject.toml +0 -0
  25. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/__init__.py +0 -0
  26. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/classifier.py +0 -0
  27. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/__init__.py +0 -0
  28. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/common.py +0 -0
  29. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/linux_auditd.py +0 -0
  30. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/macos_eslogger.py +0 -0
  31. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/windows_eventlog.py +0 -0
  32. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/models.py +0 -0
  33. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/recorder.py +0 -0
  34. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/redact.py +0 -0
  35. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/settings.py +0 -0
  36. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/spool.py +0 -0
  37. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agentd.py +0 -0
  38. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/__init__.py +0 -0
  39. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/builder.py +0 -0
  40. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/connectors.py +0 -0
  41. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/__init__.py +0 -0
  42. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/claude_code.py +0 -0
  43. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/http.py +0 -0
  44. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/mcp.py +0 -0
  45. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/n8n.py +0 -0
  46. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/shell.py +0 -0
  47. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/integrations.py +0 -0
  48. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/inventory.py +0 -0
  49. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/library/__init__.py +0 -0
  50. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/manifest.py +0 -0
  51. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_studio.py +0 -0
  52. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_template.py +0 -0
  53. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_vars.py +0 -0
  54. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/run_log.py +0 -0
  55. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/schedules.py +0 -0
  56. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/secrets.py +0 -0
  57. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/ticket_run.py +0 -0
  58. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/usage.py +0 -0
  59. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/varstore.py +0 -0
  60. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/browser_profile.py +0 -0
  61. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/cli.py +0 -0
  62. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/__init__.py +0 -0
  63. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/files.py +0 -0
  64. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/process.py +0 -0
  65. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/screenshot.py +0 -0
  66. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/config.py +0 -0
  67. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/crucial.py +0 -0
  68. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/custom_agents.py +0 -0
  69. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/departments.py +0 -0
  70. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/__init__.py +0 -0
  71. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/assets.py +0 -0
  72. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/bootstrap.py +0 -0
  73. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/browser.py +0 -0
  74. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/compare.py +0 -0
  75. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/loop.py +0 -0
  76. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/estimator.py +0 -0
  77. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/exceptions.py +0 -0
  78. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/firewall.py +0 -0
  79. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/formats.py +0 -0
  80. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/git_ops.py +0 -0
  81. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/github_fetch.py +0 -0
  82. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/github_ingest.py +0 -0
  83. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/graph_recon.py +0 -0
  84. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/graphify.py +0 -0
  85. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/hooks/__init__.py +0 -0
  86. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/hooks/crucial_guard.py +0 -0
  87. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/intake.py +0 -0
  88. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ipc/__init__.py +0 -0
  89. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ipc/electron_bridge.py +0 -0
  90. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ipc/handlers.py +0 -0
  91. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/journal.py +0 -0
  92. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/local_exec.py +0 -0
  93. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/memory_guard.py +0 -0
  94. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/__init__.py +0 -0
  95. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/envelope.py +0 -0
  96. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/requests.py +0 -0
  97. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/responses.py +0 -0
  98. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/snapshots.py +0 -0
  99. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/phase_timer.py +0 -0
  100. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/plan_run.py +0 -0
  101. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/platform_compat.py +0 -0
  102. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/prd_progress.py +0 -0
  103. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/proof_readers.py +0 -0
  104. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/providers.py +0 -0
  105. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/py.typed +0 -0
  106. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/resume.py +0 -0
  107. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/router.py +0 -0
  108. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/run_auto.py +0 -0
  109. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/self_report.py +0 -0
  110. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/self_report_local.py +0 -0
  111. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/sse.py +0 -0
  112. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/swagger.py +0 -0
  113. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/task_file.py +0 -0
  114. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/task_types.py +0 -0
  115. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/test.py +0 -0
  116. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/test_pipeline.py +0 -0
  117. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ticket_fields.py +0 -0
  118. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/tickets.py +0 -0
  119. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/tiers/__init__.py +0 -0
  120. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/tiers/mini_agent.py +0 -0
  121. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/__init__.py +0 -0
  122. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/api.py +0 -0
  123. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/dashboard.py +0 -0
  124. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/health.py +0 -0
  125. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/ip_allowlist.py +0 -0
  126. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/jobs.py +0 -0
  127. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/models.py +0 -0
  128. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/runner.py +0 -0
  129. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/security.py +0 -0
  130. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/server.py +0 -0
  131. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/service.py +0 -0
  132. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/session.py +0 -0
  133. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/setup_manager.py +0 -0
  134. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/agents.html +0 -0
  135. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/app.js +0 -0
  136. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/chat.html +0 -0
  137. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/custom-agents.html +0 -0
  138. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/default-agents.html +0 -0
  139. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/index.html +0 -0
  140. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/report.html +0 -0
  141. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/runs.html +0 -0
  142. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/schedules.html +0 -0
  143. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/setup.html +0 -0
  144. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/styles.css +0 -0
  145. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/tickets.html +0 -0
  146. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/swagger.py +0 -0
  147. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/updater.py +0 -0
  148. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/url_access.py +0 -0
  149. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  150. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/write_hook.py +0 -0
  151. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/setup.cfg +0 -0
  152. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_collectors.py +0 -0
  153. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_core.py +0 -0
  154. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_macos.py +0 -0
  155. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_os_setup.py +0 -0
  156. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_pipeline.py +0 -0
  157. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_activity.py +0 -0
  158. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_activity_tracking.py +0 -0
  159. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_chat_features.py +0 -0
  160. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_crud_cli.py +0 -0
  161. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_listing.py +0 -0
  162. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_async.py +0 -0
  163. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_models.py +0 -0
  164. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_run_ticket.py +0 -0
  165. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_schedules.py +0 -0
  166. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_workspace_settings.py +0 -0
  167. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agents_connectors_builder.py +0 -0
  168. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_allowed_tools_config.py +0 -0
  169. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_auto_agent_dispatch.py +0 -0
  170. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_browser_profile.py +0 -0
  171. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_cheap_tier.py +0 -0
  172. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_client_jira_projects.py +0 -0
  173. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_config_token_diagnostics.py +0 -0
  174. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_configure_token_owner.py +0 -0
  175. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_console_encoding.py +0 -0
  176. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_crash_recovery.py +0 -0
  177. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_departments.py +0 -0
  178. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_done_transition.py +0 -0
  179. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_epic_decompose_run.py +0 -0
  180. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_epic_inherited_agent_id.py +0 -0
  181. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_epic_session_isolation.py +0 -0
  182. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_estimator.py +0 -0
  183. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_fallback_cli.py +0 -0
  184. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_firewall_open_on_update.py +0 -0
  185. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_formats.py +0 -0
  186. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_git_action.py +0 -0
  187. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_github_fetch.py +0 -0
  188. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_github_ingest.py +0 -0
  189. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_graph_recon.py +0 -0
  190. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_graphify_build.py +0 -0
  191. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_graphify_hook_install.py +0 -0
  192. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_health.py +0 -0
  193. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_intake_job_outcome.py +0 -0
  194. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_intake_plan_review.py +0 -0
  195. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_journal.py +0 -0
  196. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_local_exec_provider.py +0 -0
  197. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_mcp_route_contract.py +0 -0
  198. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_memory_guard.py +0 -0
  199. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_phase_timer.py +0 -0
  200. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_plan_new_command.py +0 -0
  201. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_plan_run_activity.py +0 -0
  202. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_pr_identity.py +0 -0
  203. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_prd_artifact.py +0 -0
  204. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_project_path_resolution.py +0 -0
  205. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_proof_readers.py +0 -0
  206. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_providers.py +0 -0
  207. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_recon_session_isolation.py +0 -0
  208. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_resume.py +0 -0
  209. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_router.py +0 -0
  210. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_run_auto_agent_only.py +0 -0
  211. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_run_outputs.py +0 -0
  212. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_run_verdict_is_honest.py +0 -0
  213. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_schedule_api.py +0 -0
  214. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_schedule_outputs_and_plans.py +0 -0
  215. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_self_report.py +0 -0
  216. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_service_token_warning.py +0 -0
  217. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_stale_panel_restart.py +0 -0
  218. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_swagger_command.py +0 -0
  219. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_task_file_and_plan_run.py +0 -0
  220. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_task_file_format_doc.py +0 -0
  221. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_cli.py +0 -0
  222. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_fields.py +0 -0
  223. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_run_resolve_by_id.py +0 -0
  224. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_speed_fixes.py +0 -0
  225. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_tier_flag_parity.py +0 -0
  226. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ui_gate.py +0 -0
  227. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ui_lifespan.py +0 -0
  228. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_update_timer.py +0 -0
  229. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_updater_extras.py +0 -0
  230. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_updater_pep668.py +0 -0
  231. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_updater_reports_the_truth.py +0 -0
  232. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_url_access.py +0 -0
  233. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_windows_loop_identity.py +0 -0
  234. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_windows_task_paths.py +0 -0
  235. {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/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.68
3
+ Version: 1.6.70
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.68
3
+ Version: 1.6.70
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
@@ -38,6 +38,7 @@ sdk/py.typed
38
38
  sdk/resume.py
39
39
  sdk/router.py
40
40
  sdk/run_auto.py
41
+ sdk/schedule_tickets.py
41
42
  sdk/scheduler.py
42
43
  sdk/self_report.py
43
44
  sdk/self_report_local.py
@@ -52,6 +53,7 @@ sdk/tickets.py
52
53
  sdk/updater.py
53
54
  sdk/url_access.py
54
55
  sdk/vps_health.py
56
+ sdk/vps_identity.py
55
57
  sdk/write_hook.py
56
58
  sdk/activity/__init__.py
57
59
  sdk/activity/classifier.py
@@ -203,7 +205,9 @@ tests/test_run_outputs.py
203
205
  tests/test_run_verdict_is_honest.py
204
206
  tests/test_schedule_api.py
205
207
  tests/test_schedule_outputs_and_plans.py
208
+ tests/test_schedule_tickets.py
206
209
  tests/test_scheduler.py
210
+ tests/test_scheduler_transient_retry.py
207
211
  tests/test_self_report.py
208
212
  tests/test_service_token_warning.py
209
213
  tests/test_stale_panel_restart.py
@@ -223,6 +227,7 @@ tests/test_updater_pep668.py
223
227
  tests/test_updater_reports_the_truth.py
224
228
  tests/test_url_access.py
225
229
  tests/test_vps_health.py
230
+ tests/test_vps_only_reporting.py
226
231
  tests/test_windows_loop_identity.py
227
232
  tests/test_windows_task_paths.py
228
233
  tests/test_write_hook.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.68
3
+ Version: 1.6.70
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.68" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.70" # 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
@@ -188,6 +188,11 @@ def _flag_value() -> str:
188
188
  return value.strip().strip('"').strip("'")
189
189
  except OSError:
190
190
  pass
191
+ # Under sudo, settings.setting reads ROOT's .env. Falling back to it made a
192
+ # flag left in /root/.agentos/.env answer for the panel user, so _enable_flag
193
+ # never wrote theirs and the collector never started — on every restart.
194
+ if _is_privileged() and _panel_user() is not None:
195
+ return ""
191
196
  return settings.setting(AUDIT_FLAG)
192
197
 
193
198
 
@@ -176,12 +176,57 @@ async def flush_once(
176
176
  return result
177
177
 
178
178
 
179
+ def discard_spool(spool: Spool | None = None) -> int:
180
+ """Delete everything queued, without sending it. Returns the rows dropped."""
181
+ spool = spool or settings.get_spool()
182
+ dropped = 0
183
+ while True:
184
+ chunk = spool.next_chunk()
185
+ if not chunk:
186
+ return dropped
187
+ dropped += len(chunk)
188
+ spool.ack(chunk)
189
+
190
+
191
+ async def ship_tick(
192
+ is_vps: Callable[[], Awaitable[bool | None]] | None = None,
193
+ ) -> FlushResult | None:
194
+ """One cron tick: ship only from the caller's assigned VPS.
195
+
196
+ The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
197
+ with the same token put its own events in the VPS's audit trail. On a machine
198
+ that is definitely not the VPS the queue is discarded — kept, it would only grow
199
+ forever, since that machine will never be allowed to send it. When the answer
200
+ is unknown (profile unreachable) everything is kept for the next tick.
201
+ """
202
+ if is_vps is None:
203
+ from sdk import vps_identity
204
+ is_vps = vps_identity.is_assigned_vps
205
+ try:
206
+ verdict = await is_vps()
207
+ except Exception: # noqa: BLE001 — is_assigned_vps never raises, but be certain
208
+ verdict = None
209
+ if verdict is None:
210
+ return None
211
+ if verdict is False:
212
+ try:
213
+ dropped = discard_spool()
214
+ except Exception as exc: # noqa: BLE001
215
+ logger.warning("activity: could not discard the spool: %s", exc)
216
+ return None
217
+ if dropped:
218
+ logger.info("activity.not_assigned_vps discarded=%d (this machine is not the "
219
+ "user's VPS; nothing is shipped from it)", dropped)
220
+ return None
221
+ return await flush_once()
222
+
223
+
179
224
  async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
180
225
  """Flush every ``interval_s`` (default 5 minutes) until cancelled."""
181
226
  await asyncio.sleep(initial_delay_s)
182
227
  while True:
183
228
  try:
184
- await flush_once()
229
+ await ship_tick()
185
230
  every = interval_s if interval_s is not None else settings.ship_interval_seconds()
186
231
  await asyncio.sleep(every)
187
232
  except asyncio.CancelledError:
@@ -79,7 +79,7 @@ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
79
79
  # per-run event. So an agent run costs one POST no matter how chatty it is.
80
80
  _LINE_BUF: dict[str, list[str]] = {}
81
81
  _LINE_BUF_MAX_LINES = 40 # keep the tail — recent lines are the useful ones
82
- _LINE_BUF_MAX_CHARS = 4000 # bounded like _ACTIVITY_FLUSH_MAX_CHARS
82
+ _LINE_BUF_MAX_CHARS = 4000 # trimmed to the gateway limit by client._fit_activity
83
83
 
84
84
 
85
85
  def note(bucket: str, text: str) -> None:
@@ -41,6 +41,52 @@ def _tee_events(log_path, on_event: "EventHandler | None") -> "EventHandler":
41
41
  EventHandler = Callable[[dict], Awaitable[None]]
42
42
 
43
43
 
44
+ #: Pauses between attempts when an agent list cannot be read. The AI backend
45
+ #: verifies the co_ token against the ML gateway on every call and that check
46
+ #: flakes (a spurious 401 or a dropped connection for a few seconds), which made a
47
+ #: scheduled run give up on the first miss. Short, because a human may be waiting.
48
+ _LOOKUP_RETRY_DELAYS_S: tuple[float, ...] = (2.0, 5.0)
49
+
50
+
51
+ async def _fetch_rows(fetch) -> list[dict]:
52
+ """``fetch()`` with brief retries; raises the last error when every attempt fails."""
53
+ import asyncio
54
+
55
+ last: Exception | None = None
56
+ for delay in (0.0, *_LOOKUP_RETRY_DELAYS_S):
57
+ if delay:
58
+ await asyncio.sleep(delay)
59
+ try:
60
+ return await fetch() or []
61
+ except Exception as exc: # noqa: BLE001 — classified by the caller
62
+ last = exc
63
+ assert last is not None
64
+ raise last
65
+
66
+
67
+ async def _gateway_manifest(client, name: str):
68
+ """The agent from the ML gateway's listing, or ``None``.
69
+
70
+ The gateway reads the same ``agents`` table the AI backend re-projects, with no
71
+ second token-verification hop, so it stays readable when the backend flakes.
72
+ Consulted only after a backend source could not be read.
73
+ """
74
+ from sdk.agents.manifest import manifest_from_backend_row
75
+
76
+ fetch = getattr(client, "list_gateway_agents_by_scope", None)
77
+ if fetch is None:
78
+ return None
79
+ try:
80
+ scopes = await fetch() or {}
81
+ except Exception: # noqa: BLE001 — the fallback failing changes nothing
82
+ return None
83
+ for scope in ("personal", "department"):
84
+ for row in scopes.get(scope) or []:
85
+ if isinstance(row, dict) and row.get("name") == name:
86
+ return manifest_from_backend_row(row)
87
+ return None
88
+
89
+
44
90
  class AgentLookupUnavailable(LookupError):
45
91
  """The agent could not be found, but at least one source could not be READ.
46
92
 
@@ -88,7 +134,7 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
88
134
  if fetch is None:
89
135
  continue
90
136
  try:
91
- rows = await fetch() or []
137
+ rows = await _fetch_rows(fetch)
92
138
  except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
93
139
  unreadable = True
94
140
  reasons.append(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
@@ -107,6 +153,10 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
107
153
  "not in ~/.agentos/agents/, your backend agents, or your department defaults"
108
154
  )
109
155
  if unreadable:
156
+ manifest = await _gateway_manifest(client, name)
157
+ if manifest is not None:
158
+ logger.info("agent lookup fell back to the ML gateway listing for %r", name)
159
+ return manifest
110
160
  # Do NOT say "no agent named …" here: a source went unread, so absence was
111
161
  # never established. Claiming it sends the user hunting for a typo when the
112
162
  # fix is a token or a network.
@@ -121,9 +121,19 @@ def _notify_status(task_id: str, status: str) -> None:
121
121
  # `logger.info` line regardless of whether it buffers or POSTs.
122
122
  _ACTIVITY_FLUSH_MIN_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MIN_S", "") or 180.0)
123
123
  _ACTIVITY_FLUSH_MAX_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MAX_S", "") or 300.0)
124
- # Cap the coalesced payload so one flush can't POST a multi-megabyte body after a
125
- # very long, chatty run; keep the tail (most recent lines are the most useful).
126
- _ACTIVITY_FLUSH_MAX_CHARS = 8000
124
+ # The gateway rejects an `activity` over 2000 characters with a 400 ("Activity
125
+ # must be shorter than or equal to 2000 characters"). The cap used to be 8000, so
126
+ # every coalesced flush and every long scheduled-run summary was refused and the
127
+ # run left no trace in agent_activities. Keep the tail: the most recent lines are
128
+ # the most useful.
129
+ _ACTIVITY_MAX_CHARS = 2000
130
+
131
+
132
+ def _fit_activity(text: str) -> str:
133
+ """``text`` cut to the gateway's limit, keeping the tail behind a ``…`` marker."""
134
+ if len(text) <= _ACTIVITY_MAX_CHARS:
135
+ return text
136
+ return "…\n" + text[-(_ACTIVITY_MAX_CHARS - 2):]
127
137
 
128
138
 
129
139
  class ActivityFlusher:
@@ -167,9 +177,7 @@ class ActivityFlusher:
167
177
  if not self._buf:
168
178
  return # nothing pending → don't open a connection at all
169
179
  lines, self._buf = self._buf, [] # swap out atomically (no await between)
170
- body = "\n".join(lines)
171
- if len(body) > _ACTIVITY_FLUSH_MAX_CHARS:
172
- body = "…\n" + body[-_ACTIVITY_FLUSH_MAX_CHARS:]
180
+ body = _fit_activity("\n".join(lines))
173
181
  try:
174
182
  await self._client.log_activity(body, task_id=self._task_id, immediate=True)
175
183
  except Exception as exc: # noqa: BLE001 — never let a flush break a run
@@ -313,6 +321,29 @@ class BackendClient:
313
321
  except Exception as exc:
314
322
  raise BackendUnreachable(self._base_url, str(exc)) from exc
315
323
 
324
+ async def assigned_vps_ip(self) -> str:
325
+ """The IP of the caller's assigned VPS from ``GET /ml-api/users/profile``.
326
+
327
+ ``""`` when the profile names no VPS. Raises BackendUnreachable /
328
+ BackendAuthFailed — the caller (``sdk.vps_identity``) turns those into
329
+ "unknown" rather than "not a VPS".
330
+ """
331
+ from sdk.config import BE_DEFAULT_URL
332
+ from sdk.vps_identity import extract_vps_ip
333
+
334
+ url = f"{BE_DEFAULT_URL.rstrip('/')}/ml-api/users/profile"
335
+ headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
336
+ try:
337
+ async with httpx.AsyncClient(timeout=15.0) as client:
338
+ resp = await client.get(url, headers=headers)
339
+ _check_status(resp.status_code, url)
340
+ data = (resp.json() or {}).get("data") or {}
341
+ except (BackendAuthFailed, BackendVersionTooOld):
342
+ raise
343
+ except Exception as exc:
344
+ raise BackendUnreachable(url, str(exc)) from exc
345
+ return extract_vps_ip(data.get("user") or {})
346
+
316
347
  async def profile(self) -> UserProfile:
317
348
  """GET {BE}/ml-api/users/profile — the caller's role + department.
318
349
 
@@ -1766,7 +1797,7 @@ class BackendClient:
1766
1797
  "developer-name": "null",
1767
1798
  "product": "",
1768
1799
  }
1769
- body: dict[str, str] = {"activity": str(activity)}
1800
+ body: dict[str, str] = {"activity": _fit_activity(str(activity))}
1770
1801
  tid = str(task_id or "").strip()
1771
1802
  if tid:
1772
1803
  body["taskId"] = tid
@@ -1800,7 +1831,7 @@ class BackendClient:
1800
1831
  return False
1801
1832
  url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/agent-activity"
1802
1833
  headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
1803
- body: dict[str, str] = {"activity": str(activity), "agentId": aid}
1834
+ body: dict[str, str] = {"activity": _fit_activity(str(activity)), "agentId": aid}
1804
1835
  tid = str(task_id or "").strip()
1805
1836
  if tid:
1806
1837
  body["taskId"] = tid
@@ -0,0 +1,201 @@
1
+ """A Jira ticket per scheduled run: open it, mark it In Progress, close it on proof.
2
+
3
+ Opt-in per schedule with a ``ticket`` block on the schedule record::
4
+
5
+ "ticket": {"project_key": "DBNF", "type": "Task", "priority": "Low"}
6
+
7
+ Lifecycle, driven by ``sdk.scheduler.fire``:
8
+
9
+ 1. ``open_ticket`` — POST /tasks-ml, then PUT status "In Progress".
10
+ 2. the agent runs exactly as it would without a ticket.
11
+ 3. ``close_ticket`` — "Done" only when the run succeeded AND its output file
12
+ exists and was written during this run (``evidence_path``); otherwise
13
+ "backlog" with the reason appended to the description.
14
+
15
+ Uses the ML gateway's /tasks-ml directly (``create_task_ml`` / ``update_task_ml``),
16
+ the same calls the Epic decomposition already relies on, so it works without any
17
+ backend deploy. Every function here is best-effort and never raises: a ticket is
18
+ bookkeeping, and losing a nightly run because Jira was slow would be the wrong
19
+ trade. The lowercase "backlog" is deliberate — the gateway validates against
20
+ ``{To Do, In Progress, Done, backlog}`` and 400s on "Backlog".
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from dataclasses import dataclass
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+
29
+ logger = logging.getLogger("sdk.schedule_tickets")
30
+
31
+ DONE = "Done"
32
+ IN_PROGRESS = "In Progress"
33
+ BACKLOG = "backlog"
34
+ _MAX_REASON = 500
35
+
36
+
37
+ @dataclass
38
+ class RunTicket:
39
+ task_id: str
40
+ key: str
41
+ description: str
42
+
43
+
44
+ def to_record(ticket: RunTicket | None) -> dict | None:
45
+ """Plain dict to persist on the schedule while a run is being retried."""
46
+ if ticket is None:
47
+ return None
48
+ return {"task_id": ticket.task_id, "key": ticket.key, "description": ticket.description}
49
+
50
+
51
+ def from_record(record) -> RunTicket | None:
52
+ """The ticket a previous attempt of this run opened, if one was persisted."""
53
+ if not isinstance(record, dict) or not record.get("task_id"):
54
+ return None
55
+ return RunTicket(task_id=str(record["task_id"]), key=str(record.get("key") or ""),
56
+ description=str(record.get("description") or ""))
57
+
58
+
59
+ def enabled(schedule: dict) -> bool:
60
+ cfg = schedule.get("ticket")
61
+ return isinstance(cfg, dict) and bool(str(cfg.get("project_key") or "").strip())
62
+
63
+
64
+ def _summary(schedule: dict, when: datetime) -> str:
65
+ variables = schedule.get("variables") or {}
66
+ target = variables.get("repo") or ""
67
+ branch = variables.get("branch") or ""
68
+ if target and branch:
69
+ target = f"{target}@{branch}"
70
+ parts = ["[Scheduled]", str(schedule.get("agent") or "agent")]
71
+ if target:
72
+ parts.append(f"— {target}")
73
+ parts.append(f"— {when:%Y-%m-%d}")
74
+ return " ".join(parts)[:255]
75
+
76
+
77
+ def _description(schedule: dict, expected: str) -> str:
78
+ variables = schedule.get("variables") or {}
79
+ lines = [
80
+ f"Automated run of agent '{schedule.get('agent')}' by the AgentOS scheduler.",
81
+ f"Schedule: {schedule.get('cron') or schedule.get('run_at')} ({schedule.get('timezone')}), "
82
+ f"id {schedule.get('schedule_id')}.",
83
+ ]
84
+ if expected:
85
+ lines.append(f"Output: {expected}")
86
+ shown = {k: v for k, v in variables.items() if k in ("repo", "repo_path", "branch", "output_file")}
87
+ if shown:
88
+ lines.append("Inputs: " + ", ".join(f"{k}={v}" for k, v in shown.items()))
89
+ lines.append("Closed as Done only when the run succeeds and its output file is written.")
90
+ return "\n".join(lines)
91
+
92
+
93
+ def expected_output(schedule: dict) -> str:
94
+ """The file (or folder) this run is expected to write, for the ticket text."""
95
+ variables = schedule.get("variables") or {}
96
+ return str(variables.get("output_file") or schedule.get("output_dir") or "")
97
+
98
+
99
+ def evidence_path(schedule: dict, record: Path | None, delivered_to: str,
100
+ started_at: datetime) -> tuple[Path | None, str]:
101
+ """``(path, "")`` when the run's output exists and is fresh, else ``(None, why)``.
102
+
103
+ The agent's own ``output_file`` variable is the strongest proof (the per-repo
104
+ suggestion and quality files). Failing that, the run record copied into the
105
+ schedule's export folder (the AI Research Agent's daily file). Failing that,
106
+ the run record itself.
107
+ """
108
+ variables = schedule.get("variables") or {}
109
+ want = str(variables.get("output_file") or "").strip()
110
+ if want:
111
+ candidate = Path(want).expanduser()
112
+ elif delivered_to and record is not None:
113
+ candidate = Path(delivered_to) / Path(record).name
114
+ elif record is not None:
115
+ candidate = Path(record)
116
+ else:
117
+ return None, "the run produced no output file"
118
+ try:
119
+ stat = candidate.stat()
120
+ except OSError:
121
+ return None, f"output file was not created: {candidate}"
122
+ written = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
123
+ if written < started_at.replace(microsecond=0):
124
+ return None, f"output file was not updated by this run: {candidate}"
125
+ return candidate, ""
126
+
127
+
128
+ async def _owner_email(client) -> str:
129
+ try:
130
+ return str((await client.profile()).email or "").strip()
131
+ except Exception: # noqa: BLE001
132
+ return ""
133
+
134
+
135
+ async def open_ticket(client, schedule: dict, when: datetime | None = None) -> RunTicket | None:
136
+ """Create the run's ticket and move it to In Progress. ``None`` on any failure."""
137
+ if not enabled(schedule):
138
+ return None
139
+ cfg = schedule["ticket"]
140
+ when = when or datetime.now(timezone.utc)
141
+ description = _description(schedule, expected_output(schedule))
142
+ fields = {
143
+ "projectKey": str(cfg["project_key"]).strip(),
144
+ "type": str(cfg.get("type") or "Task"),
145
+ "priority": str(cfg.get("priority") or "Low"),
146
+ "summary": _summary(schedule, when),
147
+ "description": description,
148
+ }
149
+ email = await _owner_email(client)
150
+ if email:
151
+ fields["assigneeEmail"] = email
152
+ try:
153
+ task_id = await client.create_task_ml(fields)
154
+ except Exception as exc: # noqa: BLE001
155
+ logger.warning("schedule_tickets.create_failed sid=%s err=%s", schedule.get("schedule_id"), exc)
156
+ return None
157
+ if not task_id:
158
+ logger.warning("schedule_tickets.create_failed sid=%s detail=%s",
159
+ schedule.get("schedule_id"), getattr(client, "last_task_ml_error", ""))
160
+ return None
161
+ key = ""
162
+ try:
163
+ record = await client.get_task_ml(task_id) or {}
164
+ key = str(record.get("jiraIssueKey") or record.get("jiraKey") or "")
165
+ except Exception: # noqa: BLE001 — the key is for display only
166
+ pass
167
+ ticket = RunTicket(task_id=str(task_id), key=key, description=description)
168
+ await _set_status(client, ticket, IN_PROGRESS)
169
+ return ticket
170
+
171
+
172
+ async def _set_status(client, ticket: RunTicket, status: str, extra: dict | None = None) -> bool:
173
+ try:
174
+ ok = await client.update_task_ml(ticket.task_id, {"status": status, **(extra or {})})
175
+ except Exception as exc: # noqa: BLE001
176
+ logger.warning("schedule_tickets.status_failed task=%s status=%s err=%s",
177
+ ticket.task_id, status, exc)
178
+ return False
179
+ if not ok:
180
+ logger.warning("schedule_tickets.status_failed task=%s status=%s detail=%s",
181
+ ticket.task_id, status, getattr(client, "last_task_ml_error", ""))
182
+ return bool(ok)
183
+
184
+
185
+ async def close_ticket(client, ticket: RunTicket | None, *, ok: bool, reason: str = "",
186
+ output: Path | None = None) -> str:
187
+ """Done on proven success, backlog otherwise. Returns the status set ("" if none)."""
188
+ if ticket is None:
189
+ return ""
190
+ if ok and output is not None:
191
+ done = await _set_status(client, ticket, DONE,
192
+ {"description": f"{ticket.description}\nResult: {output}"})
193
+ return DONE if done else ""
194
+ why = (reason or "the run did not complete").strip()[:_MAX_REASON]
195
+ moved = await _set_status(client, ticket, BACKLOG,
196
+ {"description": f"{ticket.description}\nFailed: {why}"})
197
+ return BACKLOG if moved else ""
198
+
199
+
200
+ def label(ticket: RunTicket | None) -> str:
201
+ return (ticket.key or ticket.task_id) if ticket else ""