devops-bot-sdk 1.6.53__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.53 → devops_bot_sdk-1.6.56}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
  4. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/pyproject.toml +1 -1
  5. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/__init__.py +1 -1
  6. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agentd.py +48 -11
  7. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ipc/handlers.py +5 -2
  8. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/local_exec.py +303 -1
  9. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/platform_compat.py +12 -3
  10. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/run_auto.py +63 -17
  11. devops_bot_sdk-1.6.56/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  12. devops_bot_sdk-1.6.56/tests/test_fallback_cli.py +175 -0
  13. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_project_path_resolution.py +12 -0
  14. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_run_auto_agent_only.py +86 -0
  15. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/README.md +0 -0
  16. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  17. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  18. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/requires.txt +0 -0
  19. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  20. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/__init__.py +0 -0
  21. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/classifier.py +0 -0
  22. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/__init__.py +0 -0
  23. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/common.py +0 -0
  24. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/linux_auditd.py +0 -0
  25. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/macos_eslogger.py +0 -0
  26. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/windows_eventlog.py +0 -0
  27. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/models.py +0 -0
  28. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/os_setup.py +0 -0
  29. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/recorder.py +0 -0
  30. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/redact.py +0 -0
  31. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/settings.py +0 -0
  32. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/shipper.py +0 -0
  33. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/spool.py +0 -0
  34. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/__init__.py +0 -0
  35. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/activity.py +0 -0
  36. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/builder.py +0 -0
  37. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/chat.py +0 -0
  38. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/connectors.py +0 -0
  39. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/__init__.py +0 -0
  40. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/claude_code.py +0 -0
  41. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/http.py +0 -0
  42. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/mcp.py +0 -0
  43. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/n8n.py +0 -0
  44. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/shell.py +0 -0
  45. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/integrations.py +0 -0
  46. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/inventory.py +0 -0
  47. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/library/__init__.py +0 -0
  48. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/manifest.py +0 -0
  49. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_studio.py +0 -0
  50. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_template.py +0 -0
  51. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_vars.py +0 -0
  52. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/run_log.py +0 -0
  53. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/schedules.py +0 -0
  54. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/secrets.py +0 -0
  55. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/ticket_run.py +0 -0
  56. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/usage.py +0 -0
  57. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/varstore.py +0 -0
  58. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/browser_profile.py +0 -0
  59. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/cli.py +0 -0
  60. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/client.py +0 -0
  61. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/__init__.py +0 -0
  62. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/files.py +0 -0
  63. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/process.py +0 -0
  64. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/screenshot.py +0 -0
  65. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/config.py +0 -0
  66. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/crucial.py +0 -0
  67. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/departments.py +0 -0
  68. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/__init__.py +0 -0
  69. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/assets.py +0 -0
  70. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/bootstrap.py +0 -0
  71. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/browser.py +0 -0
  72. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/compare.py +0 -0
  73. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/loop.py +0 -0
  74. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/estimator.py +0 -0
  75. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/exceptions.py +0 -0
  76. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/firewall.py +0 -0
  77. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/formats.py +0 -0
  78. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/git_ops.py +0 -0
  79. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/github_fetch.py +0 -0
  80. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/github_ingest.py +0 -0
  81. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/graph_recon.py +0 -0
  82. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/graphify.py +0 -0
  83. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/hooks/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/hooks/crucial_guard.py +0 -0
  85. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/intake.py +0 -0
  86. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ipc/__init__.py +0 -0
  87. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ipc/electron_bridge.py +0 -0
  88. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/journal.py +0 -0
  89. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/memory_guard.py +0 -0
  90. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/__init__.py +0 -0
  91. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/envelope.py +0 -0
  92. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/requests.py +0 -0
  93. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/responses.py +0 -0
  94. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/snapshots.py +0 -0
  95. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/phase_timer.py +0 -0
  96. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/plan_run.py +0 -0
  97. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/prd_progress.py +0 -0
  98. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/proof_readers.py +0 -0
  99. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/providers.py +0 -0
  100. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/py.typed +0 -0
  101. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/resume.py +0 -0
  102. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/router.py +0 -0
  103. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/scheduler.py +0 -0
  104. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/self_report.py +0 -0
  105. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/self_report_local.py +0 -0
  106. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/sse.py +0 -0
  107. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/swagger.py +0 -0
  108. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/task_file.py +0 -0
  109. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/task_types.py +0 -0
  110. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/test.py +0 -0
  111. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/test_pipeline.py +0 -0
  112. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ticket_fields.py +0 -0
  113. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/tiers/__init__.py +0 -0
  114. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/tiers/mini_agent.py +0 -0
  115. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/__init__.py +0 -0
  116. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/api.py +0 -0
  117. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/dashboard.py +0 -0
  118. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/health.py +0 -0
  119. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/ip_allowlist.py +0 -0
  120. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/jobs.py +0 -0
  121. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/models.py +0 -0
  122. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/runner.py +0 -0
  123. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/security.py +0 -0
  124. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/server.py +0 -0
  125. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/service.py +0 -0
  126. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/session.py +0 -0
  127. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/setup_manager.py +0 -0
  128. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/agents.html +0 -0
  129. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/app.js +0 -0
  130. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/chat.html +0 -0
  131. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/custom-agents.html +0 -0
  132. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/default-agents.html +0 -0
  133. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/index.html +0 -0
  134. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/report.html +0 -0
  135. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/runs.html +0 -0
  136. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/schedules.html +0 -0
  137. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/setup.html +0 -0
  138. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/styles.css +0 -0
  139. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/tickets.html +0 -0
  140. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/swagger.py +0 -0
  141. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/updater.py +0 -0
  142. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/url_access.py +0 -0
  143. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/vps_health.py +0 -0
  144. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/write_hook.py +0 -0
  145. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/setup.cfg +0 -0
  146. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_collectors.py +0 -0
  147. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_core.py +0 -0
  148. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_macos.py +0 -0
  149. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_os_setup.py +0 -0
  150. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_pipeline.py +0 -0
  151. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_activity.py +0 -0
  152. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_activity_tracking.py +0 -0
  153. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_chat_features.py +0 -0
  154. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_listing.py +0 -0
  155. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_prompt_async.py +0 -0
  156. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_prompt_models.py +0 -0
  157. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_run_ticket.py +0 -0
  158. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_schedules.py +0 -0
  159. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_workspace_settings.py +0 -0
  160. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agents_connectors_builder.py +0 -0
  161. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_allowed_tools_config.py +0 -0
  162. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_auto_agent_dispatch.py +0 -0
  163. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_browser_profile.py +0 -0
  164. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_cheap_tier.py +0 -0
  165. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_client_jira_projects.py +0 -0
  166. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_config_token_diagnostics.py +0 -0
  167. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_configure_token_owner.py +0 -0
  168. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_console_encoding.py +0 -0
  169. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_crash_recovery.py +0 -0
  170. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_departments.py +0 -0
  171. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_done_transition.py +0 -0
  172. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_epic_decompose_run.py +0 -0
  173. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_epic_inherited_agent_id.py +0 -0
  174. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_epic_session_isolation.py +0 -0
  175. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_estimator.py +0 -0
  176. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_firewall_open_on_update.py +0 -0
  177. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_formats.py +0 -0
  178. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_git_action.py +0 -0
  179. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_github_fetch.py +0 -0
  180. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_github_ingest.py +0 -0
  181. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_graph_recon.py +0 -0
  182. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_graphify_build.py +0 -0
  183. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_graphify_hook_install.py +0 -0
  184. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_health.py +0 -0
  185. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_intake_job_outcome.py +0 -0
  186. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_intake_plan_review.py +0 -0
  187. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_journal.py +0 -0
  188. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_local_exec_provider.py +0 -0
  189. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_mcp_route_contract.py +0 -0
  190. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_memory_guard.py +0 -0
  191. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_phase_timer.py +0 -0
  192. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_plan_new_command.py +0 -0
  193. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_plan_run_activity.py +0 -0
  194. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_pr_identity.py +0 -0
  195. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_prd_artifact.py +0 -0
  196. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_proof_readers.py +0 -0
  197. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_providers.py +0 -0
  198. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_recon_session_isolation.py +0 -0
  199. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_resume.py +0 -0
  200. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_router.py +0 -0
  201. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_run_outputs.py +0 -0
  202. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_run_verdict_is_honest.py +0 -0
  203. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_schedule_api.py +0 -0
  204. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_schedule_outputs_and_plans.py +0 -0
  205. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_scheduler.py +0 -0
  206. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_self_report.py +0 -0
  207. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_service_token_warning.py +0 -0
  208. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_stale_panel_restart.py +0 -0
  209. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_swagger_command.py +0 -0
  210. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_task_file_and_plan_run.py +0 -0
  211. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_task_file_format_doc.py +0 -0
  212. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ticket_fields.py +0 -0
  213. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ticket_run_resolve_by_id.py +0 -0
  214. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ticket_speed_fixes.py +0 -0
  215. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_tier_flag_parity.py +0 -0
  216. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ui_gate.py +0 -0
  217. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ui_lifespan.py +0 -0
  218. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_update_timer.py +0 -0
  219. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_updater_extras.py +0 -0
  220. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_updater_pep668.py +0 -0
  221. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_updater_reports_the_truth.py +0 -0
  222. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_url_access.py +0 -0
  223. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_vps_health.py +0 -0
  224. {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_windows_loop_identity.py +0 -0
  225. {devops_bot_sdk-1.6.53 → 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.53
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.53
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.53" # 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
@@ -199,6 +199,11 @@ def _daemon_running() -> int | None:
199
199
  return None
200
200
 
201
201
 
202
+ #: Set in the environment right before the macOS re-exec below, read back by the
203
+ #: fresh interpreter so it knows it IS the detached daemon and must not fork again.
204
+ _DAEMONIZED_ENV = "AGENTOS_AGENTD_DAEMONIZED"
205
+
206
+
202
207
  def _daemonize() -> bool:
203
208
  """Detach the current process from the terminal (POSIX double-fork).
204
209
 
@@ -207,10 +212,23 @@ def _daemonize() -> bool:
207
212
  _LOG_FILE and a PID file is written so `status` can find the daemon.
208
213
  Returns False (stays in the foreground) on platforms without os.fork
209
214
  (e.g. Windows).
215
+
216
+ On macOS the detached child re-execs a fresh interpreter instead of running
217
+ the daemon in-process (see below); ``main()`` re-enters here with the
218
+ ``_DAEMONIZED_ENV`` marker set, where setsid, the redirected fds 0/1/2 and the
219
+ PID file already survive the exec, so we only re-point the stream wrappers.
210
220
  """
211
221
  if not hasattr(os, "fork"):
212
222
  return False # caller falls back to foreground
213
223
 
224
+ if os.environ.get(_DAEMONIZED_ENV) == "1":
225
+ # Don't leak the one-shot marker to subprocesses the daemon later spawns
226
+ # (claude, git, …) — they must never think THEY are the detached daemon.
227
+ os.environ.pop(_DAEMONIZED_ENV, None)
228
+ sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
229
+ sys.stderr = sys.stdout
230
+ return True
231
+
214
232
  _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
215
233
 
216
234
  # First fork: let the shell reap the parent and return the prompt.
@@ -232,13 +250,27 @@ def _daemonize() -> bool:
232
250
  os.close(devnull)
233
251
  os.close(logfd)
234
252
 
253
+ _PID_FILE.write_text(str(os.getpid()))
254
+
255
+ # macOS: re-exec a FRESH interpreter so the daemon's networking runs in a clean
256
+ # process image. A bare double-fork() with NO exec() leaves Apple's higher-level
257
+ # frameworks (Network.framework DNS/NAT64, the libsystem_trace os_log cache)
258
+ # half-initialised in the child, and the first getaddrinfo on the asyncio resolver
259
+ # thread then SIGSEGVs ("crashed on child side of fork pre-exec"). OBJC_DISABLE_
260
+ # INITIALIZE_FORK_SAFETY silences only the objc guard, not these — so it is not
261
+ # enough on macOS 26. The exec'd process keeps this PID, setsid session and fds.
262
+ if sys.platform == "darwin":
263
+ os.environ[_DAEMONIZED_ENV] = "1"
264
+ try:
265
+ os.execv(sys.executable, [sys.executable, sys.argv[0], *sys.argv[1:]])
266
+ except Exception: # noqa: BLE001 — exec failed; fall back to in-process daemon
267
+ os.environ.pop(_DAEMONIZED_ENV, None)
268
+
235
269
  # Line-buffer stdout/stderr so progress reaches the log live rather than
236
270
  # sitting in Python's block-buffer for a non-tty stream (same fix
237
271
  # sdk/run_auto.py applies for the same reason).
238
272
  sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
239
273
  sys.stderr = sys.stdout
240
-
241
- _PID_FILE.write_text(str(os.getpid()))
242
274
  return True
243
275
 
244
276
 
@@ -736,17 +768,22 @@ class AgentDaemon:
736
768
 
737
769
  def start(foreground: bool = False) -> None:
738
770
  """Start agentd — in the background by default, attached with --foreground."""
771
+ # macOS re-exec of the already-detached daemon (see _daemonize): this process
772
+ # IS the daemon — the duplicate guard, banner and stop-flag clear all ran in
773
+ # the original process before the fork, so skip straight to re-detaching.
774
+ resuming = os.environ.get(_DAEMONIZED_ENV) == "1"
739
775
  if not foreground:
740
- pid = _daemon_running()
741
- if pid:
742
- print(f"agentd is already running in the background (pid {pid}).")
743
- print(f" logs: {_LOG_FILE}")
744
- return
776
+ if not resuming:
777
+ pid = _daemon_running()
778
+ if pid:
779
+ print(f"agentd is already running in the background (pid {pid}).")
780
+ print(f" logs: {_LOG_FILE}")
781
+ return
745
782
 
746
- print("agentd starting in the background — it keeps running after you "
747
- "close this terminal.")
748
- print(f" logs: {_LOG_FILE}")
749
- _clear_stop() # a fresh start clears any stale stop flag
783
+ print("agentd starting in the background — it keeps running after you "
784
+ "close this terminal.")
785
+ print(f" logs: {_LOG_FILE}")
786
+ _clear_stop() # a fresh start clears any stale stop flag
750
787
  if not _daemonize():
751
788
  print("note: background mode unavailable on this platform — running "
752
789
  "in the foreground.")
@@ -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
@@ -80,12 +80,21 @@ def ensure_utf8_stdio() -> None:
80
80
  def ensure_macos_fork_safety() -> None:
81
81
  """Stop macOS from killing our background daemons with a native abort.
82
82
 
83
- ``agentos-auto`` / ``agentos-agentd`` double-fork (fork + setsid, no exec)
84
- into a background daemon that then does networking (httpx → CFNetwork /
83
+ ``agentos-auto`` / ``agentos-agentd`` double-fork (fork + setsid) into a
84
+ background daemon that then does networking (httpx → CFNetwork /
85
85
  SystemConfiguration) and TLS. When the Objective-C runtime detects objc use
86
86
  after a bare ``fork()`` with no ``exec()`` it calls ``abort()`` — which macOS
87
87
  surfaces as the "Python quit unexpectedly" crash dialog. Exporting
88
- ``OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`` disables that guard.
88
+ ``OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`` disables that *objc* guard.
89
+
90
+ It is NOT sufficient on its own: Apple's higher-level frameworks (Network.
91
+ framework's DNS/NAT64 path, the libsystem_trace ``os_log`` cache) are also
92
+ fork-unsafe and will SIGSEGV in the forked child on the first ``getaddrinfo``
93
+ — the env var does not cover them. The actual fix for that is in each daemon's
94
+ ``_daemonize()``, which re-execs a fresh interpreter after the double-fork so
95
+ the daemon runs in a clean process image. This shim remains the belt to that
96
+ braces: it quiets the objc guard and short-circuits the ``_scproxy`` lookup for
97
+ any pre-fork or non-daemon objc use, and seeds the var for the exec'd child.
89
98
 
90
99
  The objc runtime reads that variable when its dylib is first mapped (that is
91
100
  interpreter startup on framework Python builds), so setting it via
@@ -330,6 +330,23 @@ def _daemon_running() -> int | None:
330
330
  return None
331
331
 
332
332
 
333
+ #: Set in the environment right before the macOS re-exec below, read back by the
334
+ #: fresh interpreter so it knows it IS the detached daemon and must not fork again.
335
+ _DAEMONIZED_ENV = "AGENTOS_AUTO_DAEMONIZED"
336
+
337
+
338
+ def _relog_line_buffered() -> None:
339
+ """Point sys.stdout/stderr at _LOG_FILE line-buffered.
340
+
341
+ When stdout is a file (not a tty) Python block-buffers it, so progress printed
342
+ via _send() would sit in the buffer and never reach the log until the process
343
+ exits — making the daemon look idle. A line-buffered writer flushes every
344
+ progress line live, exactly like the foreground run.
345
+ """
346
+ sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
347
+ sys.stderr = sys.stdout
348
+
349
+
333
350
  def _daemonize() -> bool:
334
351
  """Detach the current process from the terminal (POSIX double-fork).
335
352
 
@@ -337,10 +354,23 @@ def _daemonize() -> bool:
337
354
  os._exit() and never return. stdout/stderr are redirected to _LOG_FILE and a
338
355
  PID file is written so `stop`/`status` can find the loop. Returns False (stays
339
356
  in the foreground) on platforms without os.fork (e.g. Windows).
357
+
358
+ On macOS the detached child does NOT run the loop in-process: it re-execs a
359
+ fresh interpreter (see below) and ``main()`` re-enters here with the
360
+ ``_DAEMONIZED_ENV`` marker set — at which point setsid, the redirected fds
361
+ 0/1/2 and the PID file already survive from before the exec, so we only
362
+ re-point the Python stream wrappers and report "I am the daemon".
340
363
  """
341
364
  if not hasattr(os, "fork"):
342
365
  return False # caller falls back to foreground
343
366
 
367
+ if os.environ.get(_DAEMONIZED_ENV) == "1":
368
+ # Don't leak the one-shot marker to subprocesses the loop later spawns
369
+ # (claude, git, …) — they must never think THEY are the detached daemon.
370
+ os.environ.pop(_DAEMONIZED_ENV, None)
371
+ _relog_line_buffered()
372
+ return True
373
+
344
374
  _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
345
375
 
346
376
  # First fork: let the shell reap the parent and return the prompt.
@@ -362,15 +392,24 @@ def _daemonize() -> bool:
362
392
  os.close(devnull)
363
393
  os.close(logfd)
364
394
 
365
- # CRITICAL: when stdout is a file (not a tty) Python block-buffers it, so
366
- # progress printed via _send() would sit in the buffer and never reach the
367
- # log until the process exits — making the daemon look idle. Replace the
368
- # streams with line-buffered writers so every progress line is flushed live,
369
- # exactly like the foreground run.
370
- sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
371
- sys.stderr = sys.stdout
372
-
373
395
  _write_pid()
396
+
397
+ # macOS: re-exec a FRESH interpreter so the daemon that does networking runs in
398
+ # a clean process image. A bare double-fork() with NO exec() leaves Apple's
399
+ # higher-level frameworks (Network.framework's DNS/NAT64 path, the libsystem_trace
400
+ # os_log cache) half-initialised in the child, and the FIRST getaddrinfo on the
401
+ # asyncio resolver thread then SIGSEGVs ("crashed on child side of fork pre-exec").
402
+ # OBJC_DISABLE_INITIALIZE_FORK_SAFETY only silences the objc +initialize guard,
403
+ # NOT these frameworks — so the env var alone is not enough on macOS 26. The exec'd
404
+ # process keeps this PID, this setsid session and the fds 0/1/2 redirected above.
405
+ if sys.platform == "darwin":
406
+ os.environ[_DAEMONIZED_ENV] = "1"
407
+ try:
408
+ os.execv(sys.executable, [sys.executable, sys.argv[0], *sys.argv[1:]])
409
+ except Exception: # noqa: BLE001 — exec failed; fall back to in-process daemon
410
+ os.environ.pop(_DAEMONIZED_ENV, None)
411
+
412
+ _relog_line_buffered()
374
413
  return True
375
414
 
376
415
 
@@ -1460,6 +1499,11 @@ def main() -> None:
1460
1499
  # ticket K... → ONLY the named Jira keys (any status, in order)
1461
1500
  # story [K...] → convert Stories → Tasks only (with PRD); NO coding.
1462
1501
  # With keys: those Stories; without: every To Do Story.
1502
+ # macOS re-exec of the already-detached daemon (see _daemonize): this
1503
+ # process IS the loop owner — the update, the duplicate-loop guard and the
1504
+ # banner all ran in the original process before the fork, so skip them and
1505
+ # go straight to detaching (a no-op fork here) and running the loop.
1506
+ resuming = os.environ.get(_DAEMONIZED_ENV) == "1"
1463
1507
  only_keys: list[str] | None = None
1464
1508
  stories_only = cmd in ("story", "stories")
1465
1509
  # `--agent-only` narrows whichever mode was chosen to the agent-tagged
@@ -1480,17 +1524,19 @@ def main() -> None:
1480
1524
  # long-lived old daemon would pin `stop`/`status` to a stale version. Runs
1481
1525
  # in the FOREGROUND so the user sees update output; re-execs on the new
1482
1526
  # code. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
1483
- if "--no-update" not in argv:
1527
+ if not resuming and "--no-update" not in argv:
1484
1528
  from sdk.updater import auto_update_and_reexec
1485
1529
  auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
1486
1530
 
1487
1531
  # Refuse to start a second loop — one background daemon owns the queue.
1488
- pid = _daemon_running()
1489
- if pid:
1490
- print(f"agentos-auto is already running in the background (pid {pid}).")
1491
- print(f" logs: {_LOG_FILE}")
1492
- print(" stop: agentos-auto stop")
1493
- return
1532
+ # (The re-exec'd daemon IS that loop; its own live PID would trip this.)
1533
+ if not resuming:
1534
+ pid = _daemon_running()
1535
+ if pid:
1536
+ print(f"agentos-auto is already running in the background (pid {pid}).")
1537
+ print(f" logs: {_LOG_FILE}")
1538
+ print(" stop: agentos-auto stop")
1539
+ return
1494
1540
 
1495
1541
  workspace_root = _parse_workspace_root(argv)
1496
1542
 
@@ -1513,7 +1559,7 @@ def main() -> None:
1513
1559
  # the loop — it runs until `agentos-auto stop`. `--foreground`/`-f` keeps it
1514
1560
  # attached (useful for debugging). On platforms without fork we stay attached.
1515
1561
  foreground = "--foreground" in argv or "-f" in argv
1516
- if not foreground and hasattr(os, "fork"):
1562
+ if not foreground and not resuming and hasattr(os, "fork"):
1517
1563
  if stories_only:
1518
1564
  target = (f" — stories→tasks: {', '.join(only_keys)}" if only_keys
1519
1565
  else " — converting all To Do Stories → Tasks")
@@ -1527,7 +1573,7 @@ def main() -> None:
1527
1573
  print(" status: agentos-auto status")
1528
1574
  print(" stop: agentos-auto stop")
1529
1575
  if not foreground and _daemonize():
1530
- # detached daemon child only
1576
+ # detached daemon child only (on macOS, the re-exec'd process)
1531
1577
  _run_loop(workspace_root, only_keys, stories_only, agent_only)
1532
1578
  return
1533
1579
  if not foreground: