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.
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/pyproject.toml +1 -1
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/__init__.py +1 -1
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agentd.py +48 -11
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ipc/handlers.py +5 -2
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/local_exec.py +303 -1
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/platform_compat.py +12 -3
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/run_auto.py +63 -17
- devops_bot_sdk-1.6.56/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- devops_bot_sdk-1.6.56/tests/test_fallback_cli.py +175 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_project_path_resolution.py +12 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_run_auto_agent_only.py +86 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/README.md +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.53 → devops_bot_sdk-1.6.56}/tests/test_write_hook.py +0 -0
|
@@ -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
|
|
@@ -34,7 +34,7 @@ Rules:
|
|
|
34
34
|
- All data egress through submit_webhook only
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.6.
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
1489
|
-
if
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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:
|
|
Binary file
|