devops-bot-sdk 1.6.91__tar.gz → 1.6.93__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/client.py +9 -0
  6. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/helpdesk.py +4 -0
  7. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ipc/handlers.py +83 -38
  8. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/run_auto.py +224 -16
  9. devops_bot_sdk-1.6.93/tests/test_approval_park.py +261 -0
  10. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_auto_ticket_queue.py +2 -2
  11. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_run_auto_agent_only.py +1 -1
  12. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ticket_schedule_and_agent.py +1 -1
  13. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/README.md +0 -0
  14. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  15. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  16. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/devops_bot_sdk.egg-info/requires.txt +0 -0
  17. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  18. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/pyproject.toml +0 -0
  19. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/__init__.py +0 -0
  20. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/classifier.py +0 -0
  21. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/collectors/__init__.py +0 -0
  22. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/collectors/common.py +0 -0
  23. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/collectors/linux_auditd.py +0 -0
  24. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/collectors/macos_eslogger.py +0 -0
  25. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/collectors/windows_eventlog.py +0 -0
  26. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/models.py +0 -0
  27. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/os_setup.py +0 -0
  28. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/recorder.py +0 -0
  29. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/redact.py +0 -0
  30. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/settings.py +0 -0
  31. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/shipper.py +0 -0
  32. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/activity/spool.py +0 -0
  33. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agentd.py +0 -0
  34. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/__init__.py +0 -0
  35. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/activity.py +0 -0
  36. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/builder.py +0 -0
  37. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/chat.py +0 -0
  38. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/connectors.py +0 -0
  39. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/engines/__init__.py +0 -0
  40. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/engines/claude_code.py +0 -0
  41. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/engines/http.py +0 -0
  42. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/engines/mcp.py +0 -0
  43. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/engines/n8n.py +0 -0
  44. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/engines/shell.py +0 -0
  45. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/integrations.py +0 -0
  46. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/inventory.py +0 -0
  47. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/library/__init__.py +0 -0
  48. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/manifest.py +0 -0
  49. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/prompt_studio.py +0 -0
  50. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/prompt_template.py +0 -0
  51. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/prompt_vars.py +0 -0
  52. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/run_log.py +0 -0
  53. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/schedules.py +0 -0
  54. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/secrets.py +0 -0
  55. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/ticket_run.py +0 -0
  56. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/usage.py +0 -0
  57. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/agents/varstore.py +0 -0
  58. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/browser_profile.py +0 -0
  59. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/claude_usage.py +0 -0
  60. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/cli.py +0 -0
  61. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/collectors/__init__.py +0 -0
  62. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/collectors/files.py +0 -0
  63. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/collectors/process.py +0 -0
  64. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/collectors/screenshot.py +0 -0
  65. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/config.py +0 -0
  66. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/crucial.py +0 -0
  67. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/custom_agents.py +0 -0
  68. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/departments.py +0 -0
  69. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/design_verify/__init__.py +0 -0
  70. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/design_verify/assets.py +0 -0
  71. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/design_verify/bootstrap.py +0 -0
  72. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/design_verify/browser.py +0 -0
  73. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/design_verify/compare.py +0 -0
  74. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/design_verify/loop.py +0 -0
  75. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/estimator.py +0 -0
  76. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/exceptions.py +0 -0
  77. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/fallback_handoff.py +0 -0
  78. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/firewall.py +0 -0
  79. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/formats.py +0 -0
  80. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/git_ops.py +0 -0
  81. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/github_fetch.py +0 -0
  82. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/github_ingest.py +0 -0
  83. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/graph_recon.py +0 -0
  84. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/graphify.py +0 -0
  85. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/helpdesk_kb.md +0 -0
  86. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/hooks/__init__.py +0 -0
  87. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/hooks/crucial_guard.py +0 -0
  88. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/intake.py +0 -0
  89. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ipc/__init__.py +0 -0
  90. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ipc/electron_bridge.py +0 -0
  91. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/journal.py +0 -0
  92. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/local_exec.py +0 -0
  93. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/memory_guard.py +0 -0
  94. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/models/__init__.py +0 -0
  95. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/models/envelope.py +0 -0
  96. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/models/requests.py +0 -0
  97. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/models/responses.py +0 -0
  98. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/models/snapshots.py +0 -0
  99. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/phase_timer.py +0 -0
  100. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/plan_run.py +0 -0
  101. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/platform_compat.py +0 -0
  102. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/prd_progress.py +0 -0
  103. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/proof_readers.py +0 -0
  104. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/providers.py +0 -0
  105. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/py.typed +0 -0
  106. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/resume.py +0 -0
  107. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/router.py +0 -0
  108. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/schedule_tickets.py +0 -0
  109. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/scheduler.py +0 -0
  110. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/self_report.py +0 -0
  111. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/self_report_local.py +0 -0
  112. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/sse.py +0 -0
  113. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/swagger.py +0 -0
  114. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/task_file.py +0 -0
  115. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/task_types.py +0 -0
  116. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/test.py +0 -0
  117. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/test_pipeline.py +0 -0
  118. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ticket_fields.py +0 -0
  119. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/tickets.py +0 -0
  120. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/tiers/__init__.py +0 -0
  121. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/tiers/mini_agent.py +0 -0
  122. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/__init__.py +0 -0
  123. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/api.py +0 -0
  124. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/dashboard.py +0 -0
  125. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/health.py +0 -0
  126. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/ip_allowlist.py +0 -0
  127. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/jobs.py +0 -0
  128. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/models.py +0 -0
  129. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/runner.py +0 -0
  130. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/security.py +0 -0
  131. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/server.py +0 -0
  132. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/service.py +0 -0
  133. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/session.py +0 -0
  134. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/setup_manager.py +0 -0
  135. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/agents.html +0 -0
  136. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/app.js +0 -0
  137. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/chat.html +0 -0
  138. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/custom-agents.html +0 -0
  139. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/default-agents.html +0 -0
  140. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/help.html +0 -0
  141. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/index.html +0 -0
  142. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/report.html +0 -0
  143. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/runs.html +0 -0
  144. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/schedules.html +0 -0
  145. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/setup.html +0 -0
  146. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/styles.css +0 -0
  147. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/static/tickets.html +0 -0
  148. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/ui/swagger.py +0 -0
  149. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/updater.py +0 -0
  150. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/url_access.py +0 -0
  151. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/vps_health.py +0 -0
  152. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/vps_identity.py +0 -0
  153. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/sdk/write_hook.py +0 -0
  154. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/setup.cfg +0 -0
  155. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_claude_runs.py +0 -0
  156. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_collectors.py +0 -0
  157. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_core.py +0 -0
  158. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_live_tracking.py +0 -0
  159. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_macos.py +0 -0
  160. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_os_setup.py +0 -0
  161. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_activity_pipeline.py +0 -0
  162. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_activity.py +0 -0
  163. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_activity_tracking.py +0 -0
  164. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_chat_features.py +0 -0
  165. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_crud_cli.py +0 -0
  166. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_listing.py +0 -0
  167. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_prompt_async.py +0 -0
  168. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_prompt_models.py +0 -0
  169. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_run_ticket.py +0 -0
  170. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_schedules.py +0 -0
  171. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agent_workspace_settings.py +0 -0
  172. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_agents_connectors_builder.py +0 -0
  173. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_allowed_tools_config.py +0 -0
  174. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_auto_agent_dispatch.py +0 -0
  175. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_browser_profile.py +0 -0
  176. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_cheap_tier.py +0 -0
  177. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_claude_usage_gate.py +0 -0
  178. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_client_jira_projects.py +0 -0
  179. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_config_token_diagnostics.py +0 -0
  180. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_configure_token_owner.py +0 -0
  181. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_console_encoding.py +0 -0
  182. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_crash_recovery.py +0 -0
  183. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_create_safe_retry.py +0 -0
  184. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_departments.py +0 -0
  185. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_done_transition.py +0 -0
  186. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_epic_decompose_run.py +0 -0
  187. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_epic_inherited_agent_id.py +0 -0
  188. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_epic_session_isolation.py +0 -0
  189. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_estimator.py +0 -0
  190. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_fallback_cli.py +0 -0
  191. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_fallback_cli_upgrade.py +0 -0
  192. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_firewall_open_on_update.py +0 -0
  193. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_formats.py +0 -0
  194. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_git_action.py +0 -0
  195. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_github_fetch.py +0 -0
  196. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_github_ingest.py +0 -0
  197. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_graph_recon.py +0 -0
  198. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_graphify_build.py +0 -0
  199. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_graphify_hook_install.py +0 -0
  200. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_health.py +0 -0
  201. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_help_attachments.py +0 -0
  202. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_helpdesk.py +0 -0
  203. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_infographic_data.py +0 -0
  204. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_intake_job_outcome.py +0 -0
  205. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_intake_plan_review.py +0 -0
  206. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_journal.py +0 -0
  207. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_limit_fallback_handoff.py +0 -0
  208. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_local_exec_provider.py +0 -0
  209. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_mcp_route_contract.py +0 -0
  210. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_memory_guard.py +0 -0
  211. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_panel_help.py +0 -0
  212. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_panel_ticket_schedules.py +0 -0
  213. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_phase_timer.py +0 -0
  214. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_plan_new_command.py +0 -0
  215. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_plan_run_activity.py +0 -0
  216. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_pr_identity.py +0 -0
  217. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_prd_artifact.py +0 -0
  218. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_project_path_resolution.py +0 -0
  219. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_proof_readers.py +0 -0
  220. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_providers.py +0 -0
  221. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_recon_session_isolation.py +0 -0
  222. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_resume.py +0 -0
  223. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_router.py +0 -0
  224. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_run_outputs.py +0 -0
  225. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_run_verdict_is_honest.py +0 -0
  226. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_schedule_api.py +0 -0
  227. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_schedule_outputs_and_plans.py +0 -0
  228. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_schedule_tickets.py +0 -0
  229. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_scheduler.py +0 -0
  230. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_scheduler_transient_retry.py +0 -0
  231. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_self_report.py +0 -0
  232. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_service_token_warning.py +0 -0
  233. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_stale_panel_restart.py +0 -0
  234. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_swagger_command.py +0 -0
  235. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_task_file_and_plan_run.py +0 -0
  236. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_task_file_format_doc.py +0 -0
  237. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ticket_cli.py +0 -0
  238. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ticket_fields.py +0 -0
  239. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ticket_run_resolve_by_id.py +0 -0
  240. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ticket_speed_fixes.py +0 -0
  241. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_tier_flag_parity.py +0 -0
  242. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ui_gate.py +0 -0
  243. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_ui_lifespan.py +0 -0
  244. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_update_timer.py +0 -0
  245. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_updater_extras.py +0 -0
  246. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_updater_ignores_cwd.py +0 -0
  247. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_updater_pep668.py +0 -0
  248. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_updater_reports_the_truth.py +0 -0
  249. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_url_access.py +0 -0
  250. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_vps_health.py +0 -0
  251. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_vps_only_reporting.py +0 -0
  252. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_windows_loop_identity.py +0 -0
  253. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_windows_panel_task.py +0 -0
  254. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_windows_stop_and_pidfile.py +0 -0
  255. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/tests/test_windows_task_paths.py +0 -0
  256. {devops_bot_sdk-1.6.91 → devops_bot_sdk-1.6.93}/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.91
3
+ Version: 1.6.93
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.91
3
+ Version: 1.6.93
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
@@ -165,6 +165,7 @@ tests/test_agent_schedules.py
165
165
  tests/test_agent_workspace_settings.py
166
166
  tests/test_agents_connectors_builder.py
167
167
  tests/test_allowed_tools_config.py
168
+ tests/test_approval_park.py
168
169
  tests/test_auto_agent_dispatch.py
169
170
  tests/test_auto_ticket_queue.py
170
171
  tests/test_browser_profile.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.91
3
+ Version: 1.6.93
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.91" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.93" # 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
@@ -1234,6 +1234,15 @@ class BackendClient:
1234
1234
  try:
1235
1235
  async with httpx.AsyncClient(timeout=20.0) as client:
1236
1236
  resp = await client.put(url, files={"status": (None, "backlog")}, headers=headers)
1237
+ # The backend 404s a Backlog move on a ticket with no running timer
1238
+ # ("No running timer found") — e.g. an Epic/Story whose approval gate
1239
+ # timed out, which is never marked In Progress. Start a timer (mark In
1240
+ # Progress) and retry once, as set_task_status does.
1241
+ if resp.status_code >= 400 and "no running timer" in resp.text.lower():
1242
+ await client.put(url, files={"status": (None, "In Progress")},
1243
+ headers=headers)
1244
+ resp = await client.put(url, files={"status": (None, "backlog")},
1245
+ headers=headers)
1237
1246
  if resp.status_code >= 400:
1238
1247
  logger.warning("client.move_to_backlog failed task=%s status=%s body=%s",
1239
1248
  task_id, resp.status_code, resp.text[:200])
@@ -169,6 +169,7 @@ def _loop_state() -> dict:
169
169
  "current_ticket": run_auto._read_current(),
170
170
  "stop_pending": run_auto._stop_requested(),
171
171
  "queue": run_auto._read_queue(),
172
+ "parked": run_auto._read_parked(),
172
173
  }
173
174
 
174
175
 
@@ -254,12 +255,15 @@ def key_facts() -> list[str]:
254
255
 
255
256
  facts: list[str] = []
256
257
  try:
258
+ from sdk import run_auto
257
259
  st = _loop_state()
258
260
  facts.append(f"agentos-auto loop running: {yn(st['running_pid'])}"
259
261
  + (f" (pid {st['running_pid']})" if st["running_pid"] else ""))
260
262
  facts.append(f"stop flag set (~/.agentos/auto.stop): {yn(st['stop_pending'])}")
261
263
  facts.append(f"ticket in flight: {st['current_ticket'] or 'none'}")
262
264
  facts.append(f"queued ticket batches: {len(st['queue'])}")
265
+ facts.append("parked awaiting approval: "
266
+ + (", ".join(run_auto._describe_parked(p) for p in st["parked"]) or "none"))
263
267
  except Exception as exc: # noqa: BLE001
264
268
  facts.append(f"agentos-auto loop state: unknown ({exc})")
265
269
  try:
@@ -4715,13 +4715,15 @@ async def _poll_fresh_approval(
4715
4715
  async def _gate_transform_approval(
4716
4716
  client: BackendClient, tid: str, jira_key: str, summary: str | None, action: str,
4717
4717
  send: Callable, thread_id: str, should_stop: Callable[[], bool] | None,
4718
+ on_park: Callable[[dict], None] | None = None,
4718
4719
  ) -> str:
4719
4720
  """Human-approval gate for a non-coding TRANSFORM (Epic decomposition /
4720
4721
  Story→Task conversion) when agentMode=True. Sends the WhatsApp request and WAITS
4721
- for the decision. Returns 'approved' | 'rejected' | 'stopped'. On a 15-min timeout
4722
- the ticket goes to Backlog and we keep polling for a late reply. Does NOT change
4723
- ticket status itself — the handler owns that (an Epic marks itself In Progress; a
4724
- Story never does)."""
4722
+ for the decision. Returns 'approved' | 'rejected' | 'stopped' | 'parked'. On a
4723
+ 15-min timeout the ticket goes to Backlog; with ``on_park`` it is handed to the
4724
+ caller to watch (see :func:`_park_or_wait`), otherwise we keep polling for a late
4725
+ reply. Does NOT change ticket status itself — the handler owns that (an Epic marks
4726
+ itself In Progress; a Story never does)."""
4725
4727
  label = jira_key or tid
4726
4728
  wa_summary = summary or jira_key or "task"
4727
4729
  await send(Envelope(
@@ -4753,18 +4755,58 @@ async def _gate_transform_approval(
4753
4755
  )
4754
4756
  if decision == "timeout":
4755
4757
  await client.move_to_backlog(tid)
4758
+ return await _park_or_wait(client, tid, jira_key, send, thread_id,
4759
+ should_stop, on_park)
4760
+ return decision
4761
+
4762
+
4763
+ async def _park_or_wait(
4764
+ client: BackendClient, tid: str, jira_key: str, send: Callable, thread_id: str,
4765
+ should_stop: Callable[[], bool] | None, on_park: Callable[[dict], None] | None,
4766
+ ) -> str:
4767
+ """After a 15-min approval timeout (ticket already in Backlog): park it, or wait.
4768
+
4769
+ With ``on_park`` (the `agentos-auto` loop) the ticket is handed over and we
4770
+ return 'parked' at once, so the rest of the run and every queued batch keep
4771
+ going; the loop re-runs the ticket when a late approval lands. Without it (a
4772
+ caller with nothing to watch the parked set) we poll for the late reply as
4773
+ before — which blocks everything behind this ticket until someone answers.
4774
+ Returns 'parked' | 'approved' | 'rejected' | 'stopped'.
4775
+ """
4776
+ label = jira_key or tid
4777
+ if on_park is not None:
4778
+ on_park({"task_id": tid, "jira_key": jira_key})
4756
4779
  await send(Envelope(
4757
4780
  type="step_update", thread_id=thread_id,
4758
- data={"current_step": f"{label}: no approval in 15m — Backlog; polling for late approval"},
4781
+ data={"current_step": f"{label}: no approval in 15m — Backlog; parked, "
4782
+ "moving on (runs when approved)"},
4759
4783
  ).model_dump())
4760
- while True:
4761
- if should_stop is not None and should_stop():
4762
- return "stopped"
4763
- late = await client.approval_status(tid)
4764
- if late in ("approved", "rejected"):
4765
- return late
4766
- await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
4767
- return decision
4784
+ try:
4785
+ await client.log_activity(
4786
+ f"Task {tid}: no approval in 15m — parked in Backlog; the loop moves on "
4787
+ "and runs it when a late approval arrives", task_id=tid)
4788
+ except Exception: # noqa: BLE001
4789
+ pass
4790
+ return "parked"
4791
+ await send(Envelope(
4792
+ type="step_update", thread_id=thread_id,
4793
+ data={"current_step": f"{label}: no approval in 15m — Backlog; polling for late approval"},
4794
+ ).model_dump())
4795
+ while True:
4796
+ if should_stop is not None and should_stop():
4797
+ return "stopped"
4798
+ late = await client.approval_status(tid)
4799
+ if late in ("approved", "rejected"):
4800
+ return late
4801
+ await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
4802
+
4803
+
4804
+ def _take(ids: set[str], tid: str) -> bool:
4805
+ """Remove ``tid`` from ``ids``; True if it was there (a one-shot pre-approval)."""
4806
+ if tid in ids:
4807
+ ids.discard(tid)
4808
+ return True
4809
+ return False
4768
4810
 
4769
4811
 
4770
4812
  # ── Agent routing (a ticket that names its own agent) ──────────────────────
@@ -5011,6 +5053,7 @@ async def _dispatch_ticket_agent(
5011
5053
  async def handle_orchestrate_auto_local_run(
5012
5054
  client: BackendClient, payload: dict, send: Callable,
5013
5055
  should_stop: Callable[[], bool] | None = None,
5056
+ on_park: Callable[[dict], None] | None = None,
5014
5057
  ) -> None:
5015
5058
  """Backs orchestrate-auto:run — run every To Do task on the USER's machine.
5016
5059
 
@@ -5019,7 +5062,13 @@ async def handle_orchestrate_auto_local_run(
5019
5062
  The server plans + gates; the user's local `claude` writes the code locally and
5020
5063
  the result is reported back. Code never lands on the AI server.
5021
5064
 
5022
- payload: {thread_id?, workspace_root?, project_paths?: {task_id: path}, role_slug?}
5065
+ payload: {thread_id?, workspace_root?, project_paths?: {task_id: path}, role_slug?,
5066
+ approved_ids?: [task_id]}
5067
+
5068
+ ``on_park``: called with ``{task_id, jira_key}`` when a ticket's approval gate
5069
+ times out; the ticket is then skipped instead of blocking the run (see
5070
+ :func:`_park_or_wait`). ``approved_ids``: tickets whose late approval the caller
5071
+ already saw — their first gate is skipped, once.
5023
5072
  """
5024
5073
  from sdk import local_exec
5025
5074
  from sdk.client import ActivityFlusher, _ACTIVE_ACTIVITY_FLUSHER
@@ -5081,6 +5130,9 @@ async def handle_orchestrate_auto_local_run(
5081
5130
 
5082
5131
  processed = 0
5083
5132
  total = len(tasks)
5133
+ # A late approval stands in for a parked ticket's FIRST gate only — a Story
5134
+ # approved for conversion still gates the coding of the Task it becomes.
5135
+ pre_approved = {str(t) for t in (payload.get("approved_ids") or []) if t}
5084
5136
  for idx, task in enumerate(tasks, 1):
5085
5137
  # Cooperative stop: pause after the previous task when a stop was
5086
5138
  # requested (e.g. `agentos-auto stop` wrote the flag file). Already
@@ -5220,11 +5272,11 @@ async def handle_orchestrate_auto_local_run(
5220
5272
  processed += 1
5221
5273
  continue
5222
5274
  # agentMode gate: wait for human approval before decomposing.
5223
- if agent_mode:
5275
+ if agent_mode and not _take(pre_approved, tid):
5224
5276
  dec = await _gate_transform_approval(
5225
5277
  client, tid, jira_key, task.get("summary"),
5226
5278
  "decompose this Epic into user stories",
5227
- send, thread_id, should_stop,
5279
+ send, thread_id, should_stop, on_park,
5228
5280
  )
5229
5281
  if dec == "stopped":
5230
5282
  await send(Envelope(
@@ -5264,7 +5316,7 @@ async def handle_orchestrate_auto_local_run(
5264
5316
  continue
5265
5317
  # agentMode gate: wait for human approval before actioning the
5266
5318
  # Story (update-in-place when instructed, else convert to Task).
5267
- if agent_mode:
5319
+ if agent_mode and not _take(pre_approved, tid):
5268
5320
  action = (
5269
5321
  # A routed Story is neither PRD-enriched nor converted,
5270
5322
  # so the approval request must describe what will
@@ -5278,7 +5330,7 @@ async def handle_orchestrate_auto_local_run(
5278
5330
  )
5279
5331
  dec = await _gate_transform_approval(
5280
5332
  client, tid, jira_key, task.get("summary"), action,
5281
- send, thread_id, should_stop,
5333
+ send, thread_id, should_stop, on_park,
5282
5334
  )
5283
5335
  if dec == "stopped":
5284
5336
  await send(Envelope(
@@ -5366,6 +5418,8 @@ async def handle_orchestrate_auto_local_run(
5366
5418
  # dropped the next iteration back to `attempt == 1` and re-entered the
5367
5419
  # approval gate → forever-poll for a late approval (a permanent hang).
5368
5420
  limit_retries = 0
5421
+ # A parked ticket's late approval: plan without the gate, like a retry.
5422
+ approved_already = _take(pre_approved, tid)
5369
5423
  while True:
5370
5424
  attempt += 1
5371
5425
  # Mark In Progress before any work begins.
@@ -5376,7 +5430,7 @@ async def handle_orchestrate_auto_local_run(
5376
5430
  # Human already approved on attempt 1; a new timer firing mid-run
5377
5431
  # would erroneously demote the ticket to Backlog while claude works.
5378
5432
  run_payload = (
5379
- task_payload if attempt == 1
5433
+ task_payload if attempt == 1 and not approved_already
5380
5434
  else {**task_payload, "agent_mode": False}
5381
5435
  )
5382
5436
 
@@ -5414,7 +5468,7 @@ async def handle_orchestrate_auto_local_run(
5414
5468
  # ── First-approval gate (agentMode=True, attempt 1 only) ──────
5415
5469
  # Skipped on retries — human already approved; re-running the gate
5416
5470
  # would time out and advance to the next ticket instead of retrying.
5417
- if agent_mode and attempt == 1:
5471
+ if agent_mode and attempt == 1 and not approved_already:
5418
5472
  await send(Envelope(
5419
5473
  type=Envelope.TYPE_AWAITING_APPROVAL, thread_id=thread_id,
5420
5474
  data={"jira_key": jira_key, "session_id": session_id, "task_id": tid,
@@ -5473,25 +5527,16 @@ async def handle_orchestrate_auto_local_run(
5473
5527
  return
5474
5528
 
5475
5529
  if decision == "timeout":
5476
- # 15 min elapsed with no WhatsApp reply.
5477
- # Move to Backlog; keep the terminal ALIVE and poll
5478
- # indefinitely — no tickets start until approval arrives.
5530
+ # 15 min elapsed with no WhatsApp reply. Move to Backlog,
5531
+ # then park it (the loop moves on and re-runs it on a
5532
+ # late approval) or, with no one to watch, wait for one.
5479
5533
  await client.move_to_backlog(tid)
5480
- await send(Envelope(
5481
- type="step_update", thread_id=thread_id,
5482
- data={"current_step": f"{jira_key}: no approval in 15m — moved to"
5483
- " Backlog; polling for late approval "
5484
- "(terminal stays alive)"},
5485
- ).model_dump())
5486
- while True:
5487
- if should_stop is not None and should_stop():
5488
- _PENDING_LOCAL.pop(session_id, None)
5489
- return
5490
- late = await client.approval_status(tid)
5491
- if late in ("approved", "rejected"):
5492
- decision = late
5493
- break
5494
- await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
5534
+ decision = await _park_or_wait(
5535
+ client, tid, jira_key, send, thread_id, should_stop, on_park,
5536
+ )
5537
+ if decision == "stopped":
5538
+ _PENDING_LOCAL.pop(session_id, None)
5539
+ return
5495
5540
  if decision == "approved":
5496
5541
  # Late approval — move back to In Progress before executing.
5497
5542
  await client.mark_in_progress(tid)
@@ -33,7 +33,9 @@ Commands:
33
33
  agentos-auto set-status … change ticket status from one status to another, by
34
34
  key or in bulk by current status (alias: move)
35
35
  agentos-auto stop ask the running loop to pause after the current task
36
- agentos-auto status show whether the background loop is running / stop pending
36
+ agentos-auto status show whether the background loop is running / stop pending,
37
+ the queue, and tickets parked awaiting approval
38
+ agentos-auto skip KEY [...] stop waiting on a parked ticket's approval (stays in Backlog)
37
39
  agentos-auto logs [-f] print the loop log (~/.agentos/auto.log); -f to follow live
38
40
  agentos-auto update upgrade the SDK to the latest version
39
41
 
@@ -59,6 +61,13 @@ A `ticket`/`story` command while a loop is running is QUEUED (~/.agentos/auto.qu
59
61
  and runs after the current work, first-asked first-run; `status` lists the queue
60
62
  and `stop` drops it.
61
63
 
64
+ Approval timeouts never block the queue: a ticket with no WhatsApp reply in 15 min
65
+ goes to Backlog and is PARKED (~/.agentos/auto.parked) while the loop moves on. The
66
+ loop re-checks parked tickets between batches and while idle: a late approval
67
+ queues the ticket first (its gate is not asked again), a rejection drops it, and
68
+ after AGENTOS_APPROVAL_PARK_MAX_H hours (default 24) it is dropped. A targeted run
69
+ stays alive while anything is parked; `skip KEY` stops waiting on one.
70
+
62
71
  Auto-update: on `run`/`resume`, the SDK checks for a newer version and upgrades
63
72
  itself (then re-runs on the new code) BEFORE detaching, so non-technical users
64
73
  never reinstall.
@@ -108,6 +117,13 @@ _WAKE_FILE = _AGENTOS_DIR / "auto.wake"
108
117
  _QUEUE_FILE = _AGENTOS_DIR / "auto.queue"
109
118
  _QUEUE_LOCK = _AGENTOS_DIR / "auto.queue.lock"
110
119
  _QUEUE_LOCK_STALE_S = 30.0
120
+ # Tickets whose approval gate timed out (15 min, no WhatsApp reply). The loop no
121
+ # longer waits on them — it moves on, re-checks each one between batches and while
122
+ # idle, and queues it (gate already passed) once a late approval lands. Dropped on
123
+ # rejection, on `skip`, or after _PARK_MAX_AGE_S. Lives as long as the loop does.
124
+ _PARKED_FILE = _AGENTOS_DIR / "auto.parked"
125
+ _PARK_MAX_AGE_S = float(os.getenv("AGENTOS_APPROVAL_PARK_MAX_H", "") or 24.0) * 3600
126
+ _PARK_RECHECK_S = float(os.getenv("AGENTOS_PARK_RECHECK_S", "") or 60.0)
111
127
 
112
128
 
113
129
  def _stop_requested() -> bool:
@@ -200,11 +216,16 @@ def _write_queue(batches: list[dict]) -> None:
200
216
  os.replace(tmp, _QUEUE_FILE) # atomic: a reader never sees half a file
201
217
 
202
218
 
203
- def _enqueue_batch(batch: dict) -> int | None:
219
+ def _enqueue_batch(batch: dict, *, front: bool = False) -> int | None:
204
220
  """Append a batch; return its 1-based position, or None when every key in it
205
- is already waiting in the queue (re-running the same command is a no-op)."""
221
+ is already waiting in the queue (re-running the same command is a no-op).
222
+ ``front`` puts it first and skips that check — a late-approved parked ticket,
223
+ which was asked for before anything now waiting."""
206
224
  with _queue_lock():
207
225
  queue = _read_queue()
226
+ if front:
227
+ _write_queue([batch, *queue])
228
+ return 1
208
229
  if batch.get("keys"):
209
230
  # Same key with a DIFFERENT --agent is a different request, not a repeat.
210
231
  pending = {k for b in queue for k in (b.get("keys") or [])
@@ -259,9 +280,144 @@ def _batch_payload(batch: dict, workspace_root: str | None) -> dict:
259
280
  payload["agent_only"] = True
260
281
  if batch.get("agent_id"):
261
282
  payload["agent_override"] = batch["agent_id"]
283
+ if batch.get("approved_ids"):
284
+ payload["approved_ids"] = list(batch["approved_ids"])
262
285
  return payload
263
286
 
264
287
 
288
+ def _read_parked() -> list[dict]:
289
+ try:
290
+ data = json.loads(_PARKED_FILE.read_text())
291
+ except (FileNotFoundError, ValueError, TypeError):
292
+ return []
293
+ return [p for p in data if isinstance(p, dict)] if isinstance(data, list) else []
294
+
295
+
296
+ def _write_parked(entries: list[dict]) -> None:
297
+ if not entries:
298
+ try:
299
+ _PARKED_FILE.unlink()
300
+ except FileNotFoundError:
301
+ pass
302
+ return
303
+ _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
304
+ tmp = _PARKED_FILE.with_suffix(".tmp")
305
+ tmp.write_text(json.dumps(entries))
306
+ os.replace(tmp, _PARKED_FILE)
307
+
308
+
309
+ def _park(entry: dict, payload: dict) -> None:
310
+ """Record a ticket whose approval timed out, with what is needed to re-run it
311
+ the way it was asked for (same workspace, agent and story-only mode)."""
312
+ import time as _t
313
+
314
+ tid = str(entry.get("task_id") or "")
315
+ if not tid:
316
+ return
317
+ with _queue_lock(): # `skip` edits this file from another process
318
+ kept = [p for p in _read_parked() if p.get("task_id") != tid]
319
+ kept.append({
320
+ "task_id": tid,
321
+ "jira_key": str(entry.get("jira_key") or ""),
322
+ "parked_at": _t.time(),
323
+ "workspace_root": payload.get("workspace_root") or "",
324
+ "agent_id": payload.get("agent_override") or "",
325
+ "stories_only": bool(payload.get("stories_only")),
326
+ })
327
+ _write_parked(kept)
328
+ print(f"⏸ parked {entry.get('jira_key') or tid} — waiting for approval; moving on")
329
+
330
+
331
+ def _unpark(ref: str) -> dict | None:
332
+ """Drop the parked ticket whose Jira key or id is ``ref``; return it."""
333
+ ref = ref.strip().upper()
334
+ with _queue_lock():
335
+ entries = _read_parked()
336
+ hit = next((p for p in entries
337
+ if ref in (str(p.get("jira_key") or "").upper(),
338
+ str(p.get("task_id")).upper())), None)
339
+ if hit is not None:
340
+ _write_parked([p for p in entries if p is not hit])
341
+ return hit
342
+
343
+
344
+ def _describe_parked(p: dict) -> str:
345
+ import time as _t
346
+
347
+ mins = int((_t.time() - float(p.get("parked_at") or 0)) // 60)
348
+ age = f"{mins // 60}h{mins % 60:02d}m" if mins >= 60 else f"{mins}m"
349
+ return f"{p.get('jira_key') or p.get('task_id')} (waiting {age})"
350
+
351
+
352
+ def _release_parked(client) -> int:
353
+ """Act on parked tickets' late decisions. Returns how many were queued to run.
354
+
355
+ approved → queued FIRST, flagged so its gate is not asked again;
356
+ rejected → dropped (stays in Backlog); older than _PARK_MAX_AGE_S → dropped
357
+ with an activity note (stays in Backlog). Pending ones stay parked.
358
+ """
359
+ import time as _t
360
+
361
+ entries = _read_parked()
362
+ if not entries:
363
+ return 0
364
+
365
+ async def _check() -> list[tuple[dict, str]]:
366
+ out = []
367
+ for p in entries:
368
+ if _t.time() - float(p.get("parked_at") or 0) > _PARK_MAX_AGE_S:
369
+ out.append((p, "expired"))
370
+ continue
371
+ try:
372
+ out.append((p, await client.approval_status(p["task_id"])))
373
+ except Exception: # noqa: BLE001 — unreadable = still pending
374
+ out.append((p, "pending"))
375
+ return out
376
+
377
+ async def _expire(p: dict) -> None:
378
+ hours = int(_PARK_MAX_AGE_S // 3600)
379
+ try:
380
+ await client.log_activity(
381
+ f"Task {p['task_id']}: no approval within {hours}h of parking — "
382
+ "dropped; left in Backlog (move it to To Do to try again)",
383
+ task_id=p["task_id"], immediate=True)
384
+ except Exception: # noqa: BLE001
385
+ pass
386
+
387
+ try:
388
+ decisions = asyncio.run(_check())
389
+ except Exception as exc: # noqa: BLE001 — a blip leaves everything parked
390
+ print(f"note: could not check parked approvals ({exc})")
391
+ return 0
392
+
393
+ queued = 0
394
+ for p, status in decisions:
395
+ if status not in ("approved", "rejected", "expired"):
396
+ continue
397
+ _unpark(p["task_id"])
398
+ label = p.get("jira_key") or p["task_id"]
399
+ if status == "rejected":
400
+ print(f"✗ {label}: rejected — left in Backlog")
401
+ elif status == "expired":
402
+ print(f"✗ {label}: no approval within {int(_PARK_MAX_AGE_S // 3600)}h — "
403
+ "dropped, left in Backlog")
404
+ with contextlib.suppress(Exception):
405
+ asyncio.run(_expire(p))
406
+ elif p.get("jira_key"):
407
+ _enqueue_batch({"keys": [p["jira_key"]], "approved_ids": [p["task_id"]],
408
+ "workspace_root": p.get("workspace_root") or None,
409
+ "agent_id": p.get("agent_id") or "",
410
+ "stories_only": bool(p.get("stories_only"))}, front=True)
411
+ print(f"✓ {label}: late approval — queued to run next")
412
+ queued += 1
413
+ else:
414
+ # No Jira key to target it by: send it back to To Do for the next fetch.
415
+ with contextlib.suppress(Exception):
416
+ asyncio.run(client.move_to_todo(p["task_id"]))
417
+ print(f"✓ {label}: late approval — moved to To Do")
418
+ return queued
419
+
420
+
265
421
  def _write_current(task_id: str) -> None:
266
422
  try:
267
423
  _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
@@ -1549,22 +1705,33 @@ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
1549
1705
  _clear_stop() # a fresh run/resume clears any stale stop flag
1550
1706
  _clear_wake() # ditto for a wake left behind by an earlier loop
1551
1707
  _clear_backlogged() # session-scoped: only track Backlog moves from THIS run
1708
+ _write_parked([]) # ditto: a parked ticket is watched only by the loop that parked it
1552
1709
  register_status_observer(_track_status) # keep the run's Backlog set current
1553
1710
  base_payload = payload
1554
1711
  try:
1555
1712
  import time as _t
1713
+ # A targeted run waiting only on parked approvals has nothing to re-run; it
1714
+ # just re-checks them (below), so the handler is skipped on those passes.
1715
+ run_handler = True
1716
+ last_wait = None
1556
1717
  while True:
1557
- asyncio.run(
1558
- handlers.handle_orchestrate_auto_local_run(
1559
- client, payload, _send, should_stop=_stop_requested,
1718
+ if run_handler:
1719
+ asyncio.run(
1720
+ handlers.handle_orchestrate_auto_local_run(
1721
+ client, payload, _send, should_stop=_stop_requested,
1722
+ # Parked with THIS batch's settings (bound, not late-bound).
1723
+ on_park=lambda entry, _p=payload: _park(entry, _p),
1724
+ )
1560
1725
  )
1561
- )
1562
- if _stop_requested():
1563
- break
1726
+ if _stop_requested():
1727
+ break
1728
+ run_handler = True
1729
+ # A late approval puts its ticket at the head of the queue.
1730
+ _release_parked(client)
1564
1731
  # Batches queued by `ticket`/`story` while we were busy run next, in
1565
1732
  # the order they were asked for.
1566
1733
  nxt = _dequeue_batch()
1567
- if nxt is None and not continuous:
1734
+ if nxt is None and not continuous and not _read_parked():
1568
1735
  # About to exit. Drop our PID record FIRST, then look once more: a
1569
1736
  # command that queued a batch just now either sees us gone (and runs
1570
1737
  # the queue itself) or its batch is found here — never neither.
@@ -1581,21 +1748,39 @@ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
1581
1748
  payload = _batch_payload(nxt, workspace_root)
1582
1749
  print(f"▶ next queued batch — {_describe_batch(nxt)}")
1583
1750
  continue
1584
- if payload is not base_payload:
1751
+ if continuous and payload is not base_payload:
1585
1752
  # Queue drained; a continuous run goes straight back to To Do.
1586
1753
  payload = base_payload
1587
1754
  continue
1588
1755
  # Idle between batches, polling the stop flag so `agentos-auto stop`
1589
1756
  # stays responsive; then loop back and the handler re-fetches To Do.
1590
- print(f"… idle — watching for new To Do tickets "
1591
- f"(re-checking every {int(_idle_refetch_s)}s; `agentos-auto stop` to end)")
1757
+ parked = _read_parked()
1758
+ waiting = (f"; {len(parked)} parked awaiting approval: "
1759
+ + ", ".join(_describe_parked(p) for p in parked)) if parked else ""
1760
+ if continuous:
1761
+ _idle_s = _idle_refetch_s
1762
+ print(f"… idle — watching for new To Do tickets "
1763
+ f"(re-checking every {int(_idle_s)}s{waiting}; "
1764
+ f"`agentos-auto stop` to end)")
1765
+ else:
1766
+ _idle_s = _PARK_RECHECK_S
1767
+ if waiting != last_wait: # say it once per change, not every minute
1768
+ print(f"… waiting{waiting} (re-checking every {int(_idle_s)}s; "
1769
+ f"`agentos-auto skip KEY` drops one, `agentos-auto stop` ends)")
1770
+ last_wait = waiting
1592
1771
  _waited = 0.0
1593
- while (_waited < _idle_refetch_s and not _stop_requested()
1772
+ while (_waited < _idle_s and not _stop_requested()
1594
1773
  and not _wake_requested()):
1595
- _t.sleep(min(2.0, _idle_refetch_s - _waited))
1774
+ _t.sleep(min(2.0, _idle_s - _waited))
1596
1775
  _waited += 2.0
1597
1776
  if _stop_requested():
1598
1777
  break
1778
+ if not continuous:
1779
+ # Nothing to re-fetch: go back up, re-check parked, take the queue.
1780
+ # The wake (a newly queued batch) is consumed there via the dequeue.
1781
+ _clear_wake()
1782
+ run_handler = False
1783
+ continue
1599
1784
  if _wake_requested():
1600
1785
  # Consume the signal BEFORE re-fetching, so a ticket created while
1601
1786
  # this batch runs still leaves a wake behind for the next idle.
@@ -1620,6 +1805,7 @@ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
1620
1805
  pass
1621
1806
  _clear_current()
1622
1807
  _clear_backlogged()
1808
+ _write_parked([])
1623
1809
 
1624
1810
 
1625
1811
  def main() -> None:
@@ -1731,6 +1917,9 @@ def main() -> None:
1731
1917
  pid = _daemon_running()
1732
1918
  cur = _read_current()
1733
1919
  backlogged = _read_backlogged() # tickets THIS run parked in Backlog
1920
+ # Parked approvals too — normally already in `backlogged`, unless the move
1921
+ # to Backlog itself failed.
1922
+ backlogged |= {str(p.get("task_id")) for p in _read_parked() if p.get("task_id")}
1734
1923
 
1735
1924
  # 1. Kill the loop's ENTIRE process group first (daemon + any child
1736
1925
  # `claude` it spawned) so nothing keeps mutating the ticket. The daemon
@@ -1766,7 +1955,7 @@ def main() -> None:
1766
1955
  print(f"warning: could not restore tickets to To Do ({exc})")
1767
1956
 
1768
1957
  # 3. Clean up state files.
1769
- for f in (_PID_FILE, _CURRENT_FILE, _BACKLOGGED_FILE, _WAKE_FILE):
1958
+ for f in (_PID_FILE, _CURRENT_FILE, _BACKLOGGED_FILE, _WAKE_FILE, _PARKED_FILE):
1770
1959
  try:
1771
1960
  f.unlink()
1772
1961
  except FileNotFoundError:
@@ -1788,9 +1977,28 @@ def main() -> None:
1788
1977
  print(f"queued batches: {len(queue)}")
1789
1978
  for i, batch in enumerate(queue, 1):
1790
1979
  print(f" {i}. {_describe_batch(batch)}")
1980
+ parked = _read_parked()
1981
+ if parked:
1982
+ print(f"parked awaiting approval: {len(parked)} (they run when approved; "
1983
+ "`agentos-auto skip KEY` drops one)")
1984
+ for p in parked:
1985
+ print(f" · {_describe_parked(p)}")
1791
1986
  print("stop pending: yes" if _stop_requested() else "stop pending: no")
1792
1987
  return
1793
1988
 
1989
+ if cmd == "skip":
1990
+ # Stop waiting on a parked ticket's approval, without stopping the loop.
1991
+ refs = [a for a in argv[1:] if not a.startswith("-")]
1992
+ if not refs:
1993
+ print("usage: agentos-auto skip KEY [KEY …] (see `agentos-auto status`)")
1994
+ return
1995
+ for ref in refs:
1996
+ hit = _unpark(ref)
1997
+ print(f"dropped {hit.get('jira_key') or hit['task_id']} — no longer waiting "
1998
+ "for its approval; it stays in Backlog" if hit
1999
+ else f"{ref}: not parked (nothing to skip)")
2000
+ return
2001
+
1794
2002
  if cmd == "logs":
1795
2003
  _show_logs(argv)
1796
2004
  return