devops-bot-sdk 1.6.54__tar.gz → 1.6.56__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 (225) hide show
  1. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
  4. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/pyproject.toml +1 -1
  5. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/__init__.py +1 -1
  6. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ipc/handlers.py +5 -2
  7. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/local_exec.py +303 -1
  8. devops_bot_sdk-1.6.56/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  9. devops_bot_sdk-1.6.56/tests/test_fallback_cli.py +175 -0
  10. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_project_path_resolution.py +12 -0
  11. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/README.md +0 -0
  12. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  13. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  14. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/requires.txt +0 -0
  15. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  16. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/__init__.py +0 -0
  17. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/classifier.py +0 -0
  18. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/__init__.py +0 -0
  19. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/common.py +0 -0
  20. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/linux_auditd.py +0 -0
  21. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/macos_eslogger.py +0 -0
  22. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/windows_eventlog.py +0 -0
  23. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/models.py +0 -0
  24. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/os_setup.py +0 -0
  25. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/recorder.py +0 -0
  26. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/redact.py +0 -0
  27. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/settings.py +0 -0
  28. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/shipper.py +0 -0
  29. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/activity/spool.py +0 -0
  30. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agentd.py +0 -0
  31. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/__init__.py +0 -0
  32. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/activity.py +0 -0
  33. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/builder.py +0 -0
  34. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/chat.py +0 -0
  35. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/connectors.py +0 -0
  36. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/engines/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/engines/claude_code.py +0 -0
  38. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/engines/http.py +0 -0
  39. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/engines/mcp.py +0 -0
  40. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/engines/n8n.py +0 -0
  41. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/engines/shell.py +0 -0
  42. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/integrations.py +0 -0
  43. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/inventory.py +0 -0
  44. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/library/__init__.py +0 -0
  45. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/manifest.py +0 -0
  46. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_studio.py +0 -0
  47. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_template.py +0 -0
  48. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_vars.py +0 -0
  49. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/run_log.py +0 -0
  50. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/schedules.py +0 -0
  51. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/secrets.py +0 -0
  52. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/ticket_run.py +0 -0
  53. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/usage.py +0 -0
  54. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/agents/varstore.py +0 -0
  55. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/browser_profile.py +0 -0
  56. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/cli.py +0 -0
  57. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/client.py +0 -0
  58. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/collectors/__init__.py +0 -0
  59. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/collectors/files.py +0 -0
  60. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/collectors/process.py +0 -0
  61. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/collectors/screenshot.py +0 -0
  62. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/config.py +0 -0
  63. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/crucial.py +0 -0
  64. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/departments.py +0 -0
  65. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/design_verify/__init__.py +0 -0
  66. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/design_verify/assets.py +0 -0
  67. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/design_verify/bootstrap.py +0 -0
  68. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/design_verify/browser.py +0 -0
  69. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/design_verify/compare.py +0 -0
  70. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/design_verify/loop.py +0 -0
  71. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/estimator.py +0 -0
  72. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/exceptions.py +0 -0
  73. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/firewall.py +0 -0
  74. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/formats.py +0 -0
  75. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/git_ops.py +0 -0
  76. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/github_fetch.py +0 -0
  77. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/github_ingest.py +0 -0
  78. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/graph_recon.py +0 -0
  79. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/graphify.py +0 -0
  80. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/hooks/__init__.py +0 -0
  81. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/hooks/crucial_guard.py +0 -0
  82. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/intake.py +0 -0
  83. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ipc/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ipc/electron_bridge.py +0 -0
  85. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/journal.py +0 -0
  86. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/memory_guard.py +0 -0
  87. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/models/__init__.py +0 -0
  88. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/models/envelope.py +0 -0
  89. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/models/requests.py +0 -0
  90. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/models/responses.py +0 -0
  91. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/models/snapshots.py +0 -0
  92. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/phase_timer.py +0 -0
  93. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/plan_run.py +0 -0
  94. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/platform_compat.py +0 -0
  95. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/prd_progress.py +0 -0
  96. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/proof_readers.py +0 -0
  97. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/providers.py +0 -0
  98. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/py.typed +0 -0
  99. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/resume.py +0 -0
  100. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/router.py +0 -0
  101. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/run_auto.py +0 -0
  102. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/scheduler.py +0 -0
  103. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/self_report.py +0 -0
  104. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/self_report_local.py +0 -0
  105. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/sse.py +0 -0
  106. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/swagger.py +0 -0
  107. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/task_file.py +0 -0
  108. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/task_types.py +0 -0
  109. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/test.py +0 -0
  110. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/test_pipeline.py +0 -0
  111. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ticket_fields.py +0 -0
  112. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/tiers/__init__.py +0 -0
  113. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/tiers/mini_agent.py +0 -0
  114. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/__init__.py +0 -0
  115. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/api.py +0 -0
  116. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/dashboard.py +0 -0
  117. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/health.py +0 -0
  118. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/ip_allowlist.py +0 -0
  119. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/jobs.py +0 -0
  120. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/models.py +0 -0
  121. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/runner.py +0 -0
  122. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/security.py +0 -0
  123. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/server.py +0 -0
  124. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/service.py +0 -0
  125. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/session.py +0 -0
  126. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/setup_manager.py +0 -0
  127. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/agents.html +0 -0
  128. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/app.js +0 -0
  129. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/chat.html +0 -0
  130. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/custom-agents.html +0 -0
  131. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/default-agents.html +0 -0
  132. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/index.html +0 -0
  133. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/report.html +0 -0
  134. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/runs.html +0 -0
  135. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/schedules.html +0 -0
  136. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/setup.html +0 -0
  137. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/styles.css +0 -0
  138. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/static/tickets.html +0 -0
  139. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/ui/swagger.py +0 -0
  140. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/updater.py +0 -0
  141. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/url_access.py +0 -0
  142. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/vps_health.py +0 -0
  143. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/sdk/write_hook.py +0 -0
  144. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/setup.cfg +0 -0
  145. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_activity_collectors.py +0 -0
  146. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_activity_core.py +0 -0
  147. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_activity_macos.py +0 -0
  148. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_activity_os_setup.py +0 -0
  149. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_activity_pipeline.py +0 -0
  150. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_activity.py +0 -0
  151. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_activity_tracking.py +0 -0
  152. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_chat_features.py +0 -0
  153. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_listing.py +0 -0
  154. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_prompt_async.py +0 -0
  155. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_prompt_models.py +0 -0
  156. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_run_ticket.py +0 -0
  157. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_schedules.py +0 -0
  158. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agent_workspace_settings.py +0 -0
  159. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_agents_connectors_builder.py +0 -0
  160. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_allowed_tools_config.py +0 -0
  161. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_auto_agent_dispatch.py +0 -0
  162. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_browser_profile.py +0 -0
  163. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_cheap_tier.py +0 -0
  164. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_client_jira_projects.py +0 -0
  165. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_config_token_diagnostics.py +0 -0
  166. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_configure_token_owner.py +0 -0
  167. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_console_encoding.py +0 -0
  168. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_crash_recovery.py +0 -0
  169. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_departments.py +0 -0
  170. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_done_transition.py +0 -0
  171. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_epic_decompose_run.py +0 -0
  172. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_epic_inherited_agent_id.py +0 -0
  173. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_epic_session_isolation.py +0 -0
  174. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_estimator.py +0 -0
  175. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_firewall_open_on_update.py +0 -0
  176. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_formats.py +0 -0
  177. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_git_action.py +0 -0
  178. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_github_fetch.py +0 -0
  179. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_github_ingest.py +0 -0
  180. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_graph_recon.py +0 -0
  181. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_graphify_build.py +0 -0
  182. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_graphify_hook_install.py +0 -0
  183. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_health.py +0 -0
  184. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_intake_job_outcome.py +0 -0
  185. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_intake_plan_review.py +0 -0
  186. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_journal.py +0 -0
  187. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_local_exec_provider.py +0 -0
  188. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_mcp_route_contract.py +0 -0
  189. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_memory_guard.py +0 -0
  190. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_phase_timer.py +0 -0
  191. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_plan_new_command.py +0 -0
  192. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_plan_run_activity.py +0 -0
  193. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_pr_identity.py +0 -0
  194. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_prd_artifact.py +0 -0
  195. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_proof_readers.py +0 -0
  196. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_providers.py +0 -0
  197. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_recon_session_isolation.py +0 -0
  198. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_resume.py +0 -0
  199. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_router.py +0 -0
  200. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_run_auto_agent_only.py +0 -0
  201. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_run_outputs.py +0 -0
  202. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_run_verdict_is_honest.py +0 -0
  203. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_schedule_api.py +0 -0
  204. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_schedule_outputs_and_plans.py +0 -0
  205. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_scheduler.py +0 -0
  206. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_self_report.py +0 -0
  207. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_service_token_warning.py +0 -0
  208. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_stale_panel_restart.py +0 -0
  209. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_swagger_command.py +0 -0
  210. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_task_file_and_plan_run.py +0 -0
  211. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_task_file_format_doc.py +0 -0
  212. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_ticket_fields.py +0 -0
  213. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_ticket_run_resolve_by_id.py +0 -0
  214. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_ticket_speed_fixes.py +0 -0
  215. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_tier_flag_parity.py +0 -0
  216. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_ui_gate.py +0 -0
  217. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_ui_lifespan.py +0 -0
  218. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_update_timer.py +0 -0
  219. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_updater_extras.py +0 -0
  220. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_updater_pep668.py +0 -0
  221. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_updater_reports_the_truth.py +0 -0
  222. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_url_access.py +0 -0
  223. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_vps_health.py +0 -0
  224. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/tests/test_windows_loop_identity.py +0 -0
  225. {devops_bot_sdk-1.6.54 → devops_bot_sdk-1.6.56}/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.54
3
+ Version: 1.6.56
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.54
3
+ Version: 1.6.56
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
@@ -137,6 +137,7 @@ sdk/ui/static/schedules.html
137
137
  sdk/ui/static/setup.html
138
138
  sdk/ui/static/styles.css
139
139
  sdk/ui/static/tickets.html
140
+ sdk/vendor/aidraftsman-cli-0.1.0.tgz
140
141
  tests/test_activity_collectors.py
141
142
  tests/test_activity_core.py
142
143
  tests/test_activity_macos.py
@@ -167,6 +168,7 @@ tests/test_epic_decompose_run.py
167
168
  tests/test_epic_inherited_agent_id.py
168
169
  tests/test_epic_session_isolation.py
169
170
  tests/test_estimator.py
171
+ tests/test_fallback_cli.py
170
172
  tests/test_firewall_open_on_update.py
171
173
  tests/test_formats.py
172
174
  tests/test_git_action.py
@@ -68,7 +68,7 @@ where = ["."]
68
68
  include = ["sdk*"]
69
69
 
70
70
  [tool.setuptools.package-data]
71
- sdk = ["py.typed", "ui/static/*"]
71
+ sdk = ["py.typed", "ui/static/*", "vendor/*.tgz"]
72
72
 
73
73
  [tool.ruff]
74
74
  line-length = 100
@@ -34,7 +34,7 @@ Rules:
34
34
  - All data egress through submit_webhook only
35
35
  """
36
36
 
37
- __version__ = "1.6.54" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.56" # 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
@@ -3920,12 +3920,15 @@ _HOME_PATH_RE = re.compile(r"(?<![\w.])~(?:/[A-Za-z0-9._\- ]+)+")
3920
3920
  # `[\\/]+` in the separator position, not a single `\\`: a doubled separator is a
3921
3921
  # thing people write (`D://Nexus-AI` — the exact form in one bug report) and the
3922
3922
  # old pattern rejected it outright, sending the ticket to the ~/AgentOS fallback.
3923
- _WIN_PATH_RE = re.compile(r"[A-Za-z]:[\\/]+(?:[^\\/:*?\"<>|\r\n]+[\\/]*)+")
3923
+ # The drive letter must not follow a word character: in `https://host/...` the
3924
+ # `s:` of the scheme otherwise reads as drive S:, so any ticket quoting a web link
3925
+ # "named" the folder `s://host/...` and was refused as unresolvable.
3926
+ _WIN_PATH_RE = re.compile(r"(?<![A-Za-z0-9_])[A-Za-z]:[\\/]+(?:[^\\/:*?\"<>|\r\n]+[\\/]*)+")
3924
3927
  # Windows path written with FORWARD slashes (e.g. C:/Users/me/new_tasks) — common on
3925
3928
  # Windows. Segments here also allow spaces, for the same reason the backslash form
3926
3929
  # does; over-capture is settled afterwards by `_trim_to_existing_dir`, not by trying
3927
3930
  # to tell a folder name from prose with a regex.
3928
- _WIN_FS_PATH_RE = re.compile(r"[A-Za-z]:/+(?:[^\\/:*?\"<>|\r\n]+/*)+")
3931
+ _WIN_FS_PATH_RE = re.compile(r"(?<![A-Za-z0-9_])[A-Za-z]:/+(?:[^\\/:*?\"<>|\r\n]+/*)+")
3929
3932
  # A QUOTED path (single/double/back quotes) — the reliable way to give a path that
3930
3933
  # contains spaces, on any OS. Accepts a drive letter with EITHER separator (C:\ or C:/).
3931
3934
  _QUOTED_PATH_RE = re.compile(r"""["'`]\s*((?:[A-Za-z]:[\\/]|~/|/)[^"'`\r\n]+?)\s*["'`]""")
@@ -552,7 +552,7 @@ def _local_mcp_servers() -> dict[str, dict]:
552
552
  return servers
553
553
 
554
554
 
555
- async def run_claude_local(
555
+ async def _run_claude_cli(
556
556
  prompt: str,
557
557
  project_path: str,
558
558
  *,
@@ -1042,3 +1042,305 @@ async def run_claude_local(
1042
1042
  provider=provider,
1043
1043
  model=cli_model["name"] or None,
1044
1044
  ))
1045
+
1046
+
1047
+ # ── Fallback: the AI Draftsman CLI ─────────────────────────────────────────
1048
+ # When the `claude` CLI is not working — not installed, logged out, out of
1049
+ # usage, or dying before it even starts a session — the ticket is re-run on
1050
+ # our own AI Draftsman CLI (`aidraftsman`, npm package @aidraftsman/cli) instead
1051
+ # of failing or parking. It is the same job against the same project dir: the
1052
+ # prompt goes in on stdin, `-p --output-format json` prints one JSON object.
1053
+ # The CLI is bundled in sdk/vendor/ and npm-installed into ~/.agentos/aidraftsman
1054
+ # the first time it is needed; one already on PATH is used instead. It needs
1055
+ # its own login on the box (`ad auth login`, or AD_API_KEY in the env).
1056
+ #
1057
+ # What it does NOT do: honour the PreToolUse approval guard, resume a Claude
1058
+ # session, or stream progress events. So a resumed run never falls back (the
1059
+ # prompt would be "continue" with no context behind it), and the permission
1060
+ # mode defaults to acceptEdits — file edits apply, but a shell command that is
1061
+ # not read-only is denied rather than run unguarded. Set
1062
+ # AGENTOS_FALLBACK_PERMISSION_MODE=bypass to let it run every command.
1063
+ #
1064
+ # Settings (process env or ~/.agentos/.env):
1065
+ # AGENTOS_FALLBACK_CLI path to the CLI (default: PATH, then bundled), or "off"
1066
+ # AGENTOS_FALLBACK_AGENT agent id/name for `--agent` (default: gateway's choice)
1067
+ # AGENTOS_FALLBACK_PERMISSION_MODE default | acceptEdits | plan | bypass
1068
+
1069
+ _FALLBACK_MODES = ("default", "acceptEdits", "plan", "bypass")
1070
+
1071
+
1072
+ def _setting(key: str) -> str:
1073
+ value = os.environ.get(key, "").strip()
1074
+ if value:
1075
+ return value
1076
+ try:
1077
+ from sdk.config import _dotenv_value
1078
+ return (_dotenv_value(key) or "").strip()
1079
+ except Exception: # noqa: BLE001 — a config hiccup must never break resolution
1080
+ return ""
1081
+
1082
+
1083
+ #: The CLI ships inside the SDK (sdk/vendor/*.tgz) so every box has it without
1084
+ #: a separate install step; it is npm-installed from there on first use.
1085
+ _VENDOR_DIR = Path(__file__).parent / "vendor"
1086
+
1087
+
1088
+ def _fallback_disabled() -> bool:
1089
+ return _setting("AGENTOS_FALLBACK_CLI").lower() in ("off", "0", "false", "no", "none")
1090
+
1091
+
1092
+ def _bundled_tarball() -> Path | None:
1093
+ tarballs = sorted(_VENDOR_DIR.glob("aidraftsman-cli-*.tgz"))
1094
+ return tarballs[-1] if tarballs else None
1095
+
1096
+
1097
+ def _bundled_bin(tarball: Path) -> Path:
1098
+ """Where the bundled tarball's `aidraftsman` lands once installed.
1099
+
1100
+ One prefix per tarball name, so an SDK update that ships a new CLI version
1101
+ installs it fresh rather than reusing the old one.
1102
+ """
1103
+ prefix = config._CONFIG_DIR / "aidraftsman" / tarball.name.removesuffix(".tgz")
1104
+ name = "aidraftsman.cmd" if sys.platform == "win32" else "aidraftsman"
1105
+ return prefix / "node_modules" / ".bin" / name
1106
+
1107
+
1108
+ def fallback_cli_path() -> str | None:
1109
+ """Absolute path to an installed `aidraftsman` CLI, or None if absent or disabled.
1110
+
1111
+ Resolution order: ``AGENTOS_FALLBACK_CLI``, then ``PATH``, then the copy
1112
+ installed from the bundled tarball. Never installs anything — see
1113
+ :func:`ensure_fallback_cli` for that.
1114
+ """
1115
+ if _fallback_disabled():
1116
+ return None
1117
+ override = _setting("AGENTOS_FALLBACK_CLI")
1118
+ if override and Path(override).exists():
1119
+ return override
1120
+ found = shutil.which("aidraftsman")
1121
+ if found:
1122
+ return found
1123
+ tarball = _bundled_tarball()
1124
+ if tarball and _bundled_bin(tarball).exists():
1125
+ return str(_bundled_bin(tarball))
1126
+ return None
1127
+
1128
+
1129
+ def _install_bundled(tarball: Path) -> str | None:
1130
+ """``npm install`` the bundled tarball into a private prefix (no sudo, no -g)."""
1131
+ npm = shutil.which("npm")
1132
+ if not npm:
1133
+ print("aidraftsman fallback unavailable: npm not found (Node.js 20+ required)", flush=True)
1134
+ return None
1135
+ bin_path = _bundled_bin(tarball)
1136
+ prefix = bin_path.parents[2]
1137
+ prefix.mkdir(parents=True, exist_ok=True)
1138
+ cmd = [npm, "install", "--prefix", str(prefix), "--no-audit", "--no-fund",
1139
+ "--omit=dev", str(tarball)]
1140
+ if sys.platform == "win32" and npm.lower().endswith((".cmd", ".bat")):
1141
+ cmd = [os.environ.get("COMSPEC", "cmd.exe"), "/c", *cmd]
1142
+ print(f"installing bundled {tarball.name} into {prefix}", flush=True)
1143
+ try:
1144
+ proc = subprocess.run(cmd, capture_output=True, text=True, errors="replace",
1145
+ timeout=600)
1146
+ except Exception as exc: # noqa: BLE001 — never raise into a ticket run
1147
+ print(f"aidraftsman install failed: {exc}", flush=True)
1148
+ return None
1149
+ if proc.returncode != 0 or not bin_path.exists():
1150
+ print(f"aidraftsman install failed: {(proc.stderr or proc.stdout).strip()[-300:]}",
1151
+ flush=True)
1152
+ return None
1153
+ return str(bin_path)
1154
+
1155
+
1156
+ async def ensure_fallback_cli() -> str | None:
1157
+ """:func:`fallback_cli_path`, installing the bundled CLI first if needed."""
1158
+ found = fallback_cli_path()
1159
+ if found or _fallback_disabled():
1160
+ return found
1161
+ tarball = _bundled_tarball()
1162
+ if not tarball:
1163
+ return None
1164
+ return await asyncio.to_thread(_install_bundled, tarball)
1165
+
1166
+
1167
+ def _claude_not_working(result: dict[str, Any]) -> bool:
1168
+ """True when the run failed because of the `claude` CLI, not the task.
1169
+
1170
+ A task that ran and failed on its own merits (an error result, a full
1171
+ context window, a capped response) would fail the same way elsewhere, so
1172
+ those stay with the caller's existing recovery.
1173
+ """
1174
+ if result.get("ok"):
1175
+ return False
1176
+ if result.get("auth_expired") or result.get("limit_exceeded"):
1177
+ return True
1178
+ if result.get("context_exceeded") or result.get("output_cap_exceeded"):
1179
+ return False
1180
+ # No session id means the CLI never got as far as its init event: missing,
1181
+ # crashed on start-up, or hung before doing anything.
1182
+ return not result.get("session_id")
1183
+
1184
+
1185
+ def _git_changed_files(path: Path) -> set[str] | None:
1186
+ """Paths `git status` reports as changed/untracked, or None outside a repo."""
1187
+ try:
1188
+ proc = subprocess.run(["git", "status", "--porcelain", "-uall"], cwd=str(path),
1189
+ capture_output=True, text=True, timeout=30)
1190
+ except Exception: # noqa: BLE001 — no git, or a slow repo: just skip the list
1191
+ return None
1192
+ if proc.returncode != 0:
1193
+ return None
1194
+ out = set()
1195
+ for line in proc.stdout.splitlines():
1196
+ name = line[3:].split(" -> ")[-1].strip().strip('"')
1197
+ if name:
1198
+ out.add(str(path / name))
1199
+ return out
1200
+
1201
+
1202
+ async def run_fallback_cli(
1203
+ prompt: str,
1204
+ project_path: str,
1205
+ *,
1206
+ timeout: float = _DEFAULT_TIMEOUT_S,
1207
+ github_token: str | None = None,
1208
+ extra_env: dict[str, str] | None = None,
1209
+ tier: str | None = None,
1210
+ provider: str | None = None,
1211
+ ) -> dict[str, Any]:
1212
+ """Run ``prompt`` on the AI Draftsman CLI. Same result shape as the claude run."""
1213
+ cli = await ensure_fallback_cli()
1214
+ if not cli:
1215
+ return _result(ok=False, error="AI Draftsman CLI not available (needs Node.js 20+ "
1216
+ "and npm to install the bundled copy).",
1217
+ tier=tier, provider=provider)
1218
+
1219
+ path = Path(project_path).expanduser()
1220
+ path.mkdir(parents=True, exist_ok=True)
1221
+
1222
+ mode = _setting("AGENTOS_FALLBACK_PERMISSION_MODE") or "acceptEdits"
1223
+ if mode not in _FALLBACK_MODES:
1224
+ mode = "acceptEdits"
1225
+ cmd = [cli, "-p", "--output-format", "json", "--permission-mode", mode]
1226
+ agent = _setting("AGENTOS_FALLBACK_AGENT")
1227
+ if agent:
1228
+ cmd += ["--agent", agent]
1229
+ # npm installs a .cmd shim on Windows — same reason as the claude spawn.
1230
+ if sys.platform == "win32" and cli.lower().endswith((".cmd", ".bat")):
1231
+ cmd = [os.environ.get("COMSPEC", "cmd.exe"), "/c", *cmd]
1232
+
1233
+ env = dict(os.environ)
1234
+ if extra_env:
1235
+ env.update({k: str(v) for k, v in extra_env.items() if v is not None})
1236
+ if github_token:
1237
+ env["GH_TOKEN"] = github_token
1238
+ env["GITHUB_TOKEN"] = github_token
1239
+ env["GIT_CONFIG_COUNT"] = "1"
1240
+ env["GIT_CONFIG_KEY_0"] = (
1241
+ f"url.https://x-access-token:{github_token}@github.com/.insteadOf"
1242
+ )
1243
+ env["GIT_CONFIG_VALUE_0"] = "https://github.com/"
1244
+
1245
+ before = _git_changed_files(path)
1246
+ print(f"aidraftsman fallback spawn | mode={mode} | {path}", flush=True)
1247
+ proc = await asyncio.create_subprocess_exec(
1248
+ *cmd, cwd=str(path), env=env,
1249
+ stdin=asyncio.subprocess.PIPE,
1250
+ stdout=asyncio.subprocess.PIPE,
1251
+ stderr=asyncio.subprocess.PIPE,
1252
+ )
1253
+ try:
1254
+ stdout, stderr = await asyncio.wait_for(
1255
+ proc.communicate((prompt or "").encode("utf-8")), timeout=timeout)
1256
+ except asyncio.TimeoutError:
1257
+ proc.kill()
1258
+ return _result(ok=False, error=f"aidraftsman CLI timed out after {timeout:.0f}s",
1259
+ tier=tier, provider=provider, model="aidraftsman")
1260
+ except asyncio.CancelledError:
1261
+ try:
1262
+ proc.kill()
1263
+ except ProcessLookupError:
1264
+ pass
1265
+ raise
1266
+
1267
+ after = _git_changed_files(path)
1268
+ files_changed = sorted(after - before) if before is not None and after is not None else []
1269
+ err_text = stderr.decode("utf-8", errors="replace").strip()
1270
+
1271
+ data: dict[str, Any] = {}
1272
+ for line in reversed(stdout.decode("utf-8", errors="replace").splitlines()):
1273
+ try:
1274
+ data = json.loads(line)
1275
+ break
1276
+ except json.JSONDecodeError:
1277
+ continue
1278
+ if proc.returncode != 0 or not isinstance(data, dict) or "result" not in data:
1279
+ err = err_text[-500:] or f"aidraftsman exited with code {proc.returncode}"
1280
+ return _result(ok=False, error=err, files_changed=files_changed,
1281
+ auth_expired=proc.returncode == 2 or _is_auth_error(err),
1282
+ tier=tier, provider=provider, model="aidraftsman")
1283
+
1284
+ usage = data.get("usage") or {}
1285
+ in_tok = int(usage.get("prompt_tokens", 0) or 0)
1286
+ out_tok = int(usage.get("completion_tokens", 0) or 0)
1287
+ text = str(data.get("result") or "")
1288
+ return _result(
1289
+ ok=True,
1290
+ result=text,
1291
+ longest_text=text,
1292
+ input_tokens=in_tok,
1293
+ output_tokens=out_tok,
1294
+ total_tokens=int(usage.get("total_tokens", 0) or 0) or in_tok + out_tok,
1295
+ files_changed=files_changed,
1296
+ tier=tier,
1297
+ provider=provider,
1298
+ model=f"aidraftsman:{data.get('agent') or 'auto'}",
1299
+ )
1300
+
1301
+
1302
+ async def run_claude_local(
1303
+ prompt: str,
1304
+ project_path: str,
1305
+ *,
1306
+ allowed_tools: list[str] | None = None,
1307
+ on_event: EventHandler | None = None,
1308
+ timeout: float = _DEFAULT_TIMEOUT_S,
1309
+ github_token: str | None = None,
1310
+ resume_session_id: str | None = None,
1311
+ approval_dir: str | None = None,
1312
+ idle_timeout: float = _IDLE_TIMEOUT_S,
1313
+ max_output_tokens: int | None = None,
1314
+ extra_env: dict[str, str] | None = None,
1315
+ unset_env: list[str] | None = None,
1316
+ settings_env: dict[str, str] | None = None,
1317
+ tier: str | None = None,
1318
+ provider: str | None = None,
1319
+ ) -> dict[str, Any]:
1320
+ """Run the local `claude` CLI; if the CLI itself is not working, re-run on
1321
+ the AI Draftsman CLI. See ``_run_claude_cli`` for the arguments and the
1322
+ result shape, and the fallback notes above ``run_fallback_cli``.
1323
+ """
1324
+ result = await _run_claude_cli(
1325
+ prompt, project_path,
1326
+ allowed_tools=allowed_tools, on_event=on_event, timeout=timeout,
1327
+ github_token=github_token, resume_session_id=resume_session_id,
1328
+ approval_dir=approval_dir, idle_timeout=idle_timeout,
1329
+ max_output_tokens=max_output_tokens, extra_env=extra_env,
1330
+ unset_env=unset_env, settings_env=settings_env, tier=tier, provider=provider,
1331
+ )
1332
+ if resume_session_id or not _claude_not_working(result) or _fallback_disabled():
1333
+ return result
1334
+
1335
+ print(f"claude CLI not working ({(result.get('error') or '')[:200]}) — "
1336
+ f"falling back to aidraftsman", flush=True)
1337
+ fallback = await run_fallback_cli(
1338
+ prompt, project_path, timeout=timeout, github_token=github_token,
1339
+ extra_env=extra_env, tier=tier, provider=provider,
1340
+ )
1341
+ if fallback["ok"]:
1342
+ return fallback
1343
+ # Both failed: keep the claude result (its limit/auth flags drive the
1344
+ # caller's parking and alerts) and say what the fallback hit too.
1345
+ result["error"] = f"{result.get('error')} | aidraftsman fallback: {fallback.get('error')}"
1346
+ return result
@@ -0,0 +1,175 @@
1
+ """`run_claude_local` falls back to the AI Draftsman CLI when claude is not working."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import sys
7
+
8
+ import pytest
9
+
10
+ from sdk import local_exec
11
+
12
+ pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="POSIX script stand-in")
13
+
14
+
15
+ def _fake_cli(tmp_path, *, exit_code=0, payload=None, stderr=""):
16
+ """An `aidraftsman` stand-in: records argv/stdin/cwd, writes a file, prints JSON."""
17
+ record = tmp_path / "record.json"
18
+ script = tmp_path / "aidraftsman"
19
+ script.write_text(f"""#!{sys.executable}
20
+ import json, os, sys
21
+ prompt = sys.stdin.read()
22
+ json.dump({{"argv": sys.argv[1:], "prompt": prompt, "cwd": os.getcwd()}}, open({str(record)!r}, "w"))
23
+ open("made_by_fallback.txt", "w").write("x")
24
+ sys.stderr.write({stderr!r})
25
+ if {payload!r} is not None:
26
+ print(json.dumps({payload!r}))
27
+ sys.exit({exit_code})
28
+ """)
29
+ script.chmod(0o755)
30
+ return script, record
31
+
32
+
33
+ @pytest.fixture()
34
+ def no_claude(monkeypatch):
35
+ monkeypatch.setattr(local_exec, "claude_cli_path", lambda: None)
36
+
37
+
38
+ def test_falls_back_when_claude_is_missing(tmp_path, monkeypatch, no_claude):
39
+ project = tmp_path / "proj"
40
+ cli, record = _fake_cli(tmp_path, payload={
41
+ "result": "fixed it", "session_id": "ad-1", "agent": "dev",
42
+ "usage": {"prompt_tokens": 7, "completion_tokens": 3, "total_tokens": 10},
43
+ "permission_denials": []})
44
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", str(cli))
45
+ monkeypatch.setenv("AGENTOS_FALLBACK_AGENT", "dev")
46
+
47
+ result = asyncio.run(local_exec.run_claude_local("do the ticket", str(project), tier="t2"))
48
+
49
+ assert result["ok"] is True
50
+ assert result["result"] == "fixed it"
51
+ assert result["total_tokens"] == 10
52
+ assert result["model"] == "aidraftsman:dev"
53
+ assert result["tier"] == "t2"
54
+ seen = json.loads(record.read_text())
55
+ assert seen["prompt"] == "do the ticket"
56
+ assert seen["cwd"] == str(project)
57
+ assert seen["argv"] == ["-p", "--output-format", "json",
58
+ "--permission-mode", "acceptEdits", "--agent", "dev"]
59
+
60
+
61
+ def test_falls_back_on_usage_limit(tmp_path, monkeypatch):
62
+ cli, _ = _fake_cli(tmp_path, payload={"result": "ok", "usage": {}})
63
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", str(cli))
64
+
65
+ async def _limited(*a, **k):
66
+ return local_exec._result(ok=False, error="usage limit reached",
67
+ limit_exceeded=True, session_id="s-1")
68
+ monkeypatch.setattr(local_exec, "_run_claude_cli", _limited)
69
+
70
+ result = asyncio.run(local_exec.run_claude_local("x", str(tmp_path / "p")))
71
+ assert result["ok"] is True and result["model"] == "aidraftsman:auto"
72
+
73
+
74
+ @pytest.mark.parametrize("claude_result", [
75
+ # The task itself failed after claude started — would fail elsewhere too.
76
+ {"ok": False, "error": "tests failed", "session_id": "s-1"},
77
+ {"ok": False, "error": "prompt too long", "context_exceeded": True},
78
+ {"ok": True, "result": "done", "session_id": "s-1"},
79
+ ])
80
+ def test_no_fallback_when_claude_itself_worked(tmp_path, monkeypatch, claude_result):
81
+ cli, record = _fake_cli(tmp_path, payload={"result": "ok"})
82
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", str(cli))
83
+
84
+ async def _run(*a, **k):
85
+ return local_exec._result(**claude_result)
86
+ monkeypatch.setattr(local_exec, "_run_claude_cli", _run)
87
+
88
+ result = asyncio.run(local_exec.run_claude_local("x", str(tmp_path / "p")))
89
+ assert result["ok"] is claude_result["ok"]
90
+ assert not record.exists()
91
+
92
+
93
+ def test_no_fallback_when_resuming_a_claude_session(tmp_path, monkeypatch, no_claude):
94
+ cli, record = _fake_cli(tmp_path, payload={"result": "ok"})
95
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", str(cli))
96
+
97
+ result = asyncio.run(local_exec.run_claude_local(
98
+ "continue", str(tmp_path / "p"), resume_session_id="s-9"))
99
+ assert result["ok"] is False
100
+ assert not record.exists()
101
+
102
+
103
+ def test_both_failing_keeps_claude_result_and_names_both(tmp_path, monkeypatch, no_claude):
104
+ cli, _ = _fake_cli(tmp_path, exit_code=2, stderr="You are not logged in.")
105
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", str(cli))
106
+
107
+ result = asyncio.run(local_exec.run_claude_local("x", str(tmp_path / "p")))
108
+ assert result["ok"] is False
109
+ assert "Claude Code CLI not found" in result["error"]
110
+ assert "aidraftsman fallback: You are not logged in." in result["error"]
111
+
112
+
113
+ def test_off_disables_the_fallback(monkeypatch):
114
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", "off")
115
+ assert local_exec.fallback_cli_path() is None
116
+
117
+
118
+ def test_files_changed_from_git(tmp_path, monkeypatch, no_claude):
119
+ import subprocess
120
+ project = tmp_path / "proj"
121
+ project.mkdir()
122
+ subprocess.run(["git", "init", "-q"], cwd=project, check=True)
123
+ cli, _ = _fake_cli(tmp_path, payload={"result": "ok"})
124
+ monkeypatch.setenv("AGENTOS_FALLBACK_CLI", str(cli))
125
+
126
+ result = asyncio.run(local_exec.run_claude_local("x", str(project)))
127
+ assert result["files_changed"] == [str(project / "made_by_fallback.txt")]
128
+
129
+
130
+ def test_tarball_is_bundled():
131
+ assert local_exec._bundled_tarball() is not None
132
+
133
+
134
+ def test_installs_bundled_cli_when_none_on_path(tmp_path, monkeypatch, no_claude):
135
+ """No CLI on PATH → npm-install the bundled tarball into ~/.agentos, then run it."""
136
+ cli, record = _fake_cli(tmp_path, payload={"result": "from bundle"})
137
+ npm_log = tmp_path / "npm.json"
138
+ npm = tmp_path / "npm"
139
+ npm.write_text(f"""#!{sys.executable}
140
+ import json, os, sys
141
+ args = sys.argv[1:]
142
+ json.dump(args, open({str(npm_log)!r}, "w"))
143
+ bin_dir = os.path.join(args[args.index("--prefix") + 1], "node_modules", ".bin")
144
+ os.makedirs(bin_dir)
145
+ os.symlink({str(cli)!r}, os.path.join(bin_dir, "aidraftsman"))
146
+ """)
147
+ npm.chmod(0o755)
148
+ monkeypatch.delenv("AGENTOS_FALLBACK_CLI")
149
+ monkeypatch.setattr(local_exec, "_setting", lambda key: "")
150
+ monkeypatch.setattr(local_exec.config, "_CONFIG_DIR", tmp_path / ".agentos")
151
+ monkeypatch.setattr(local_exec.shutil, "which",
152
+ lambda name: str(npm) if name == "npm" else None)
153
+
154
+ result = asyncio.run(local_exec.run_claude_local("x", str(tmp_path / "p")))
155
+
156
+ assert result["ok"] is True and result["result"] == "from bundle"
157
+ args = json.loads(npm_log.read_text())
158
+ assert args[-1] == str(local_exec._bundled_tarball())
159
+ assert str(tmp_path / ".agentos" / "aidraftsman") in args[args.index("--prefix") + 1]
160
+ # Second run finds the installed copy without calling npm again.
161
+ npm_log.unlink()
162
+ assert local_exec.fallback_cli_path().startswith(str(tmp_path / ".agentos"))
163
+ asyncio.run(local_exec.run_claude_local("x", str(tmp_path / "p")))
164
+ assert not npm_log.exists()
165
+
166
+
167
+ def test_no_npm_reports_why(tmp_path, monkeypatch, no_claude):
168
+ monkeypatch.delenv("AGENTOS_FALLBACK_CLI")
169
+ monkeypatch.setattr(local_exec, "_setting", lambda key: "")
170
+ monkeypatch.setattr(local_exec.config, "_CONFIG_DIR", tmp_path / ".agentos")
171
+ monkeypatch.setattr(local_exec.shutil, "which", lambda name: None)
172
+
173
+ result = asyncio.run(local_exec.run_claude_local("x", str(tmp_path / "p")))
174
+ assert result["ok"] is False
175
+ assert "Node.js 20+" in result["error"]
@@ -177,6 +177,18 @@ class TestResolveProjectPath:
177
177
  # the fact the person fixing the ticket needs.
178
178
  assert "/home/nobody/definitely-missing" in (got.unresolved or "")
179
179
 
180
+ def test_a_web_link_is_not_a_drive_path(self, tmp_path):
181
+ """`https://host/...` once read as drive S: and the ticket was refused."""
182
+ got = handlers.resolve_project_path(
183
+ _task(humanInstructions=(
184
+ "Extract all item statements verbatim from the standard at "
185
+ "https://skillsengland.education.gov.uk/apprenticeships/[ST0890]. "
186
+ "Map each item's recommended AI capabilities.")),
187
+ {"workspace_root": str(tmp_path)},
188
+ )
189
+ assert got.source == "workspace-root"
190
+ assert got.unresolved is None
191
+
180
192
  def test_an_explicit_override_wins(self, tmp_path):
181
193
  got = handlers.resolve_project_path(
182
194
  _task(humanInstructions="Work in /somewhere/else"),