devops-bot-sdk 1.6.76__tar.gz → 1.6.79__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.76 → devops_bot_sdk-1.6.79}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/cli.py +6 -3
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/local_exec.py +135 -7
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/run_auto.py +9 -9
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/updater.py +26 -4
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/vps_identity.py +7 -1
- devops_bot_sdk-1.6.79/tests/test_fallback_cli_upgrade.py +147 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_updater_reports_the_truth.py +57 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_vps_only_reporting.py +27 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/README.md +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_claude_runs.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_live_tracking.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_windows_panel_task.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/tests/test_write_hook.py +0 -0
|
@@ -175,6 +175,7 @@ tests/test_epic_inherited_agent_id.py
|
|
|
175
175
|
tests/test_epic_session_isolation.py
|
|
176
176
|
tests/test_estimator.py
|
|
177
177
|
tests/test_fallback_cli.py
|
|
178
|
+
tests/test_fallback_cli_upgrade.py
|
|
178
179
|
tests/test_firewall_open_on_update.py
|
|
179
180
|
tests/test_formats.py
|
|
180
181
|
tests/test_git_action.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.79
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -34,7 +34,7 @@ Rules:
|
|
|
34
34
|
- All data egress through submit_webhook only
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.6.
|
|
37
|
+
__version__ = "1.6.79" # 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
|
|
@@ -1383,7 +1383,7 @@ def _browser_command(args: list[str]) -> None:
|
|
|
1383
1383
|
# ── fallback ─────────────────────────────────────────────────────────────
|
|
1384
1384
|
|
|
1385
1385
|
def _fallback_usage() -> None:
|
|
1386
|
-
print("usage: agentos fallback install #
|
|
1386
|
+
print("usage: agentos fallback install # install `ad`, or update an outdated one")
|
|
1387
1387
|
print(" agentos fallback login [--browser | --api-key [KEY]] [--base-url URL] [--no-browser]")
|
|
1388
1388
|
print(" agentos fallback status [--verify] # --verify asks the server too")
|
|
1389
1389
|
print(" agentos fallback logout")
|
|
@@ -1411,8 +1411,11 @@ def _fallback_command(args: list[str]) -> None:
|
|
|
1411
1411
|
if le._fallback_disabled():
|
|
1412
1412
|
print("error: the fallback is disabled (AGENTOS_FALLBACK_CLI=off)")
|
|
1413
1413
|
sys.exit(1)
|
|
1414
|
-
#
|
|
1415
|
-
#
|
|
1414
|
+
# Installs the CLI when it is missing, and replaces an outdated one (the old
|
|
1415
|
+
# package name, or a release older than FALLBACK_MIN_VERSION).
|
|
1416
|
+
done = le.upgrade_fallback_cli(install_missing=True)
|
|
1417
|
+
if done:
|
|
1418
|
+
print(done)
|
|
1416
1419
|
cli = le.fallback_cli_path()
|
|
1417
1420
|
if not cli:
|
|
1418
1421
|
print(f"error: the AI Draftsman CLI (`ad`) was not found. Install it with:\n"
|
|
@@ -24,6 +24,7 @@ import re
|
|
|
24
24
|
import shutil
|
|
25
25
|
import subprocess
|
|
26
26
|
import sys
|
|
27
|
+
import time
|
|
27
28
|
from datetime import datetime, timedelta
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
from typing import Any, Awaitable, Callable
|
|
@@ -1047,7 +1048,7 @@ async def _run_claude_cli(
|
|
|
1047
1048
|
# ── Fallback: the AI Draftsman CLI ─────────────────────────────────────────
|
|
1048
1049
|
# When the `claude` CLI is not AVAILABLE — not installed, logged out, or dying
|
|
1049
1050
|
# before it even starts a session — the ticket runs on our own AI Draftsman CLI
|
|
1050
|
-
# (`ad`, npm package @
|
|
1051
|
+
# (`ad`, npm package @consultancy-outfit/ai-draftsman-cli) instead of failing or parking. It is the
|
|
1051
1052
|
# same job against the same project dir: the prompt goes in on stdin,
|
|
1052
1053
|
# `-p --output-format json` prints one JSON object.
|
|
1053
1054
|
#
|
|
@@ -1056,9 +1057,13 @@ async def _run_claude_cli(
|
|
|
1056
1057
|
# model mid-ticket.
|
|
1057
1058
|
#
|
|
1058
1059
|
# The CLI is the one already installed on the machine (`ad` on PATH, or an npm
|
|
1059
|
-
# global bin dir the service's PATH may not include). The SDK
|
|
1060
|
-
#
|
|
1061
|
-
#
|
|
1060
|
+
# global bin dir the service's PATH may not include). The SDK does not pin a copy
|
|
1061
|
+
# (one pinned inside the SDK went stale the day after it shipped), but it does keep
|
|
1062
|
+
# an installed one current: an `ad` from the old package name, or older than
|
|
1063
|
+
# FALLBACK_MIN_VERSION, is replaced with the latest release (`upgrade_fallback_cli`).
|
|
1064
|
+
# Before 1.1.1 every request carried a tool schema Gemini rejects (an integer
|
|
1065
|
+
# `enum`), so an old `ad` failed every ticket with "400 INVALID_ARGUMENT …
|
|
1066
|
+
# function_declarations". It needs its own login on the box, as the user the SDK
|
|
1062
1067
|
# runs as: `agentos fallback login` (browser sign-in code or an API key), or
|
|
1063
1068
|
# AD_API_KEY in the env. `agentos fallback status --verify` checks it against
|
|
1064
1069
|
# the server.
|
|
@@ -1089,8 +1094,15 @@ def _setting(key: str) -> str:
|
|
|
1089
1094
|
return ""
|
|
1090
1095
|
|
|
1091
1096
|
|
|
1097
|
+
#: The npm package that ships `ad`. It was published as @aidraftsman/cli before 1.1.1.
|
|
1098
|
+
FALLBACK_PACKAGE = "@consultancy-outfit/ai-draftsman-cli"
|
|
1099
|
+
_LEGACY_FALLBACK_PACKAGES = ("@aidraftsman/cli",)
|
|
1100
|
+
#: The oldest `ad` that works against the gateway's Gemini models (see above).
|
|
1101
|
+
FALLBACK_MIN_VERSION = (1, 1, 1)
|
|
1092
1102
|
#: How to get the CLI when it is missing — shown in every "not found" message.
|
|
1093
|
-
FALLBACK_INSTALL_HINT = "npm install -g
|
|
1103
|
+
FALLBACK_INSTALL_HINT = f"npm install -g {FALLBACK_PACKAGE}"
|
|
1104
|
+
#: `upgrade_fallback_cli` runs at most this often from the start-up path.
|
|
1105
|
+
_FALLBACK_UPGRADE_EVERY_S = 6 * 3600
|
|
1094
1106
|
|
|
1095
1107
|
|
|
1096
1108
|
def _fallback_disabled() -> bool:
|
|
@@ -1135,10 +1147,110 @@ def fallback_cli_path() -> str | None:
|
|
|
1135
1147
|
|
|
1136
1148
|
|
|
1137
1149
|
async def ensure_fallback_cli() -> str | None:
|
|
1138
|
-
""":func:`fallback_cli_path`. Kept for callers; it
|
|
1150
|
+
""":func:`fallback_cli_path`. Kept for callers; it installs nothing itself."""
|
|
1139
1151
|
return fallback_cli_path()
|
|
1140
1152
|
|
|
1141
1153
|
|
|
1154
|
+
def _version_tuple(version: str) -> tuple[int, ...]:
|
|
1155
|
+
parts = []
|
|
1156
|
+
for piece in str(version).split("-")[0].split("."):
|
|
1157
|
+
try:
|
|
1158
|
+
parts.append(int(piece))
|
|
1159
|
+
except ValueError:
|
|
1160
|
+
break
|
|
1161
|
+
return tuple(parts)
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def _npm_cmd(*args: str) -> list[str] | None:
|
|
1165
|
+
"""An npm command line, or None without npm. npm is a .cmd shim on Windows."""
|
|
1166
|
+
npm = shutil.which("npm")
|
|
1167
|
+
if not npm:
|
|
1168
|
+
return None
|
|
1169
|
+
cmd = [npm, *args]
|
|
1170
|
+
if sys.platform == "win32" and npm.lower().endswith((".cmd", ".bat")):
|
|
1171
|
+
cmd = [os.environ.get("COMSPEC", "cmd.exe"), "/c", *cmd]
|
|
1172
|
+
return cmd
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def fallback_packages() -> dict[str, str]:
|
|
1176
|
+
"""The AI Draftsman CLI packages npm has installed globally, ``{name: version}``.
|
|
1177
|
+
|
|
1178
|
+
Read from npm rather than ``ad --version``: 1.1.1 still prints "1.1.0" there,
|
|
1179
|
+
so the version flag cannot tell the fixed release from the broken one.
|
|
1180
|
+
"""
|
|
1181
|
+
cmd = _npm_cmd("ls", "-g", "--depth=0", "--json")
|
|
1182
|
+
if not cmd:
|
|
1183
|
+
return {}
|
|
1184
|
+
try:
|
|
1185
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, errors="replace",
|
|
1186
|
+
timeout=60, check=False)
|
|
1187
|
+
deps = (json.loads(proc.stdout or "{}") or {}).get("dependencies") or {}
|
|
1188
|
+
except Exception: # noqa: BLE001 — npm missing/slow/odd output: nothing known
|
|
1189
|
+
return {}
|
|
1190
|
+
wanted = (FALLBACK_PACKAGE, *_LEGACY_FALLBACK_PACKAGES)
|
|
1191
|
+
return {name: str((deps.get(name) or {}).get("version") or "")
|
|
1192
|
+
for name in wanted if name in deps}
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def upgrade_fallback_cli(*, install_missing: bool = False) -> str:
|
|
1196
|
+
"""Bring the AI Draftsman CLI up to date. Returns what was done, ``""`` if nothing.
|
|
1197
|
+
|
|
1198
|
+
Replaces an `ad` from the old package name and upgrades one older than
|
|
1199
|
+
FALLBACK_MIN_VERSION to the latest release. A machine without the CLI is left
|
|
1200
|
+
alone unless ``install_missing`` (``agentos fallback install``). Skipped when the
|
|
1201
|
+
fallback is off or AGENTOS_FALLBACK_CLI points at a CLI the owner manages. Never raises.
|
|
1202
|
+
"""
|
|
1203
|
+
if _fallback_disabled() or _setting("AGENTOS_FALLBACK_CLI"):
|
|
1204
|
+
return ""
|
|
1205
|
+
if not _npm_cmd("--version"):
|
|
1206
|
+
return ""
|
|
1207
|
+
installed = fallback_packages()
|
|
1208
|
+
legacy = [p for p in _LEGACY_FALLBACK_PACKAGES if p in installed]
|
|
1209
|
+
current = installed.get(FALLBACK_PACKAGE)
|
|
1210
|
+
if not legacy and current and _version_tuple(current) >= FALLBACK_MIN_VERSION:
|
|
1211
|
+
return ""
|
|
1212
|
+
if not legacy and not current and not install_missing:
|
|
1213
|
+
return ""
|
|
1214
|
+
try:
|
|
1215
|
+
for pkg in legacy: # same `ad` bin: the old package must go first
|
|
1216
|
+
subprocess.run(_npm_cmd("uninstall", "-g", pkg), capture_output=True,
|
|
1217
|
+
text=True, errors="replace", timeout=300, check=False)
|
|
1218
|
+
proc = subprocess.run(_npm_cmd("install", "-g", f"{FALLBACK_PACKAGE}@latest"),
|
|
1219
|
+
capture_output=True, text=True, errors="replace",
|
|
1220
|
+
timeout=600, check=False)
|
|
1221
|
+
except Exception as exc: # noqa: BLE001
|
|
1222
|
+
return f"could not update the AI Draftsman CLI: {exc} — run `{FALLBACK_INSTALL_HINT}`"
|
|
1223
|
+
if proc.returncode != 0:
|
|
1224
|
+
tail = (proc.stderr or proc.stdout or "").strip()[-300:]
|
|
1225
|
+
return f"could not update the AI Draftsman CLI ({tail}) — run `{FALLBACK_INSTALL_HINT}`"
|
|
1226
|
+
now = fallback_packages().get(FALLBACK_PACKAGE, "?")
|
|
1227
|
+
before = current or (installed.get(legacy[0]) if legacy else "") or "not installed"
|
|
1228
|
+
return f"AI Draftsman CLI updated {before} → {now}"
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def maybe_upgrade_fallback_cli() -> str:
|
|
1232
|
+
""":func:`upgrade_fallback_cli`, at most every few hours (start-up path)."""
|
|
1233
|
+
stamp = Path.home() / ".agentos" / "fallback-cli-checked"
|
|
1234
|
+
try:
|
|
1235
|
+
if time.time() - stamp.stat().st_mtime < _FALLBACK_UPGRADE_EVERY_S:
|
|
1236
|
+
return ""
|
|
1237
|
+
except OSError:
|
|
1238
|
+
pass
|
|
1239
|
+
message = upgrade_fallback_cli()
|
|
1240
|
+
try:
|
|
1241
|
+
stamp.parent.mkdir(parents=True, exist_ok=True)
|
|
1242
|
+
stamp.touch()
|
|
1243
|
+
except OSError:
|
|
1244
|
+
pass
|
|
1245
|
+
return message
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _is_stale_fallback_error(text: str | None) -> bool:
|
|
1249
|
+
"""The gateway rejected `ad`'s own tool schema — an outdated CLI, not the task."""
|
|
1250
|
+
t = text or ""
|
|
1251
|
+
return "INVALID_ARGUMENT" in t and "function_declarations" in t
|
|
1252
|
+
|
|
1253
|
+
|
|
1142
1254
|
def _fallback_argv(cli: str, *args: str) -> list[str]:
|
|
1143
1255
|
# npm installs a .cmd shim on Windows — same reason as the claude spawn.
|
|
1144
1256
|
cmd = [cli, *args]
|
|
@@ -1325,8 +1437,13 @@ async def run_fallback_cli(
|
|
|
1325
1437
|
extra_env: dict[str, str] | None = None,
|
|
1326
1438
|
tier: str | None = None,
|
|
1327
1439
|
provider: str | None = None,
|
|
1440
|
+
_upgraded: bool = False,
|
|
1328
1441
|
) -> dict[str, Any]:
|
|
1329
|
-
"""Run ``prompt`` on the AI Draftsman CLI. Same result shape as the claude run.
|
|
1442
|
+
"""Run ``prompt`` on the AI Draftsman CLI. Same result shape as the claude run.
|
|
1443
|
+
|
|
1444
|
+
An outdated `ad` fails its first request (its tool schema is rejected) before
|
|
1445
|
+
touching anything, so that one failure upgrades the CLI and runs again, once.
|
|
1446
|
+
"""
|
|
1330
1447
|
cli = await ensure_fallback_cli()
|
|
1331
1448
|
if not cli:
|
|
1332
1449
|
return _result(ok=False, error="AI Draftsman CLI (`ad`) not found on this "
|
|
@@ -1392,6 +1509,17 @@ async def run_fallback_cli(
|
|
|
1392
1509
|
continue
|
|
1393
1510
|
if proc.returncode != 0 or not isinstance(data, dict) or "result" not in data:
|
|
1394
1511
|
err = err_text[-500:] or f"aidraftsman exited with code {proc.returncode}"
|
|
1512
|
+
if _is_stale_fallback_error(err) and not _upgraded:
|
|
1513
|
+
print("aidraftsman rejected its own tool list — the CLI is outdated; updating it",
|
|
1514
|
+
flush=True)
|
|
1515
|
+
done = await asyncio.to_thread(upgrade_fallback_cli)
|
|
1516
|
+
if done:
|
|
1517
|
+
print(done, flush=True)
|
|
1518
|
+
if done.startswith("AI Draftsman CLI updated"):
|
|
1519
|
+
return await run_fallback_cli(
|
|
1520
|
+
prompt, project_path, timeout=timeout, github_token=github_token,
|
|
1521
|
+
extra_env=extra_env, tier=tier, provider=provider, _upgraded=True)
|
|
1522
|
+
err += f" — the AI Draftsman CLI is outdated; run `{FALLBACK_INSTALL_HINT}`"
|
|
1395
1523
|
auth_expired = proc.returncode == 2 or _is_auth_error(err)
|
|
1396
1524
|
if auth_expired:
|
|
1397
1525
|
err += " — log the fallback in with `agentos fallback login`"
|
|
@@ -1310,13 +1310,15 @@ def main() -> None:
|
|
|
1310
1310
|
# report before → after. Does NOT start, stop, or touch the running loop —
|
|
1311
1311
|
# a running daemon keeps its current code until it is restarted. Honours
|
|
1312
1312
|
# AGENTOS_NO_UPDATE=1 (auto_update no-ops).
|
|
1313
|
-
from sdk.updater import auto_update
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1313
|
+
from sdk.updater import auto_update
|
|
1314
|
+
# NOT quiet: auto_update's own report is the only honest answer. Quiet, a
|
|
1315
|
+
# failed pip (a locked .exe on Windows, no network, …) left the version
|
|
1316
|
+
# unchanged and this command printed "already up to date" — so boxes sat
|
|
1317
|
+
# on 1.6.51 / 1.6.73 for days while PyPI had newer, and nobody could tell.
|
|
1318
|
+
# The report says "still on vX — vY is published", quotes pip's error and
|
|
1319
|
+
# prints the manual fix command.
|
|
1320
|
+
changed, before, after = auto_update(quiet=False)
|
|
1321
|
+
if changed:
|
|
1320
1322
|
# Reload the control-panel server so newly-added /api/ui/* routes take
|
|
1321
1323
|
# effect — a running server keeps the code it imported at boot. Only
|
|
1322
1324
|
# restarts a managed (systemd/launchd/schtask) service; a manually-run
|
|
@@ -1338,8 +1340,6 @@ def main() -> None:
|
|
|
1338
1340
|
if pid:
|
|
1339
1341
|
print("note: a background loop is running the OLD version — "
|
|
1340
1342
|
"`agentos-auto stop` then `agentos-auto` to run the new one.")
|
|
1341
|
-
else:
|
|
1342
|
-
print(f"already up to date (v{after})")
|
|
1343
1343
|
|
|
1344
1344
|
# One-time GitHub history backfill — the last 50 days of THIS user's PRs.
|
|
1345
1345
|
# Fired from `update` because that is the moment every machine passes
|
|
@@ -564,6 +564,9 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
|
564
564
|
"""
|
|
565
565
|
cur = current_version()
|
|
566
566
|
if os.getenv("AGENTOS_NO_UPDATE"):
|
|
567
|
+
if not quiet:
|
|
568
|
+
_say(f"⚠ updates are switched off (AGENTOS_NO_UPDATE is set) — staying on v{cur}. "
|
|
569
|
+
"Remove it from the environment / ~/.agentos/.env to update.")
|
|
567
570
|
return (False, cur, None)
|
|
568
571
|
|
|
569
572
|
latest = latest_pypi_version()
|
|
@@ -573,6 +576,13 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
|
573
576
|
# Nothing to do on a PyPI install that is already at (or past) the published
|
|
574
577
|
# version — a git install still checks its branch, which PyPI cannot answer for.
|
|
575
578
|
if not is_github and not behind_pypi:
|
|
579
|
+
if not quiet:
|
|
580
|
+
if latest:
|
|
581
|
+
_say(f"✓ already up to date (v{cur}).")
|
|
582
|
+
else:
|
|
583
|
+
# Not "up to date": we never found out. Saying so is the difference
|
|
584
|
+
# between a box that is current and one that cannot reach PyPI.
|
|
585
|
+
_say(f"⚠ could not reach PyPI to check for updates — staying on v{cur}.")
|
|
576
586
|
audit_changed = _ensure_activity_audit(quiet)
|
|
577
587
|
# Already current — just self-heal the service, restarting only if the
|
|
578
588
|
# audit setup changed something the running panel must pick up.
|
|
@@ -637,6 +647,13 @@ def auto_update_and_reexec(module: str, argv: list[str]) -> None:
|
|
|
637
647
|
if os.getenv("AGENTOS_NO_UPDATE") or os.getenv("_AGENTOS_REEXEC"):
|
|
638
648
|
return
|
|
639
649
|
updated, _cur, _latest = auto_update(quiet=False)
|
|
650
|
+
try:
|
|
651
|
+
from sdk.local_exec import maybe_upgrade_fallback_cli
|
|
652
|
+
fallback_note = maybe_upgrade_fallback_cli()
|
|
653
|
+
if fallback_note:
|
|
654
|
+
_say(("✓ " if fallback_note.startswith("AI Draftsman CLI updated") else "⚠ ") + fallback_note)
|
|
655
|
+
except Exception: # noqa: BLE001 — the fallback CLI must never block a run
|
|
656
|
+
pass
|
|
640
657
|
|
|
641
658
|
# A self-upgrade of a RUNNING package can land broken on Windows (the active
|
|
642
659
|
# agentos-auto.exe / loaded files can't be replaced mid-pip — pip uninstalls
|
|
@@ -659,8 +676,13 @@ def auto_update_and_reexec(module: str, argv: list[str]) -> None:
|
|
|
659
676
|
if not updated:
|
|
660
677
|
return
|
|
661
678
|
_say("restarting on the updated version …\n")
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
679
|
+
try:
|
|
680
|
+
result = subprocess.run(
|
|
681
|
+
[sys.executable, "-m", module, *argv],
|
|
682
|
+
env={**os.environ, "_AGENTOS_REEXEC": "1"},
|
|
683
|
+
)
|
|
684
|
+
except KeyboardInterrupt:
|
|
685
|
+
# Ctrl+C reaches the child too, which stops cleanly and says how to resume;
|
|
686
|
+
# the parent only waits for it, so it leaves without a traceback.
|
|
687
|
+
sys.exit(130)
|
|
666
688
|
sys.exit(result.returncode)
|
|
@@ -40,6 +40,11 @@ logger = logging.getLogger("sdk.vps_identity")
|
|
|
40
40
|
#: A machine does not change into (or out of) a VPS often; a definite answer is
|
|
41
41
|
#: reused this long so a 5-minute shipper does not fetch the profile every tick.
|
|
42
42
|
CACHE_TTL_S = 6 * 3600.0
|
|
43
|
+
#: A "no" is re-asked much sooner. The usual "no" is "no VPS is assigned to this
|
|
44
|
+
#: user" on a box set up before its allocation existed — and holding that for 6 h
|
|
45
|
+
#: kept a freshly assigned VPS silent (no health, activity discarded) for the rest
|
|
46
|
+
#: of the working day. Re-asking costs one profile GET every few minutes.
|
|
47
|
+
NEGATIVE_CACHE_TTL_S = 10 * 60.0
|
|
43
48
|
|
|
44
49
|
_TRUTHY = {"1", "true", "yes", "on"}
|
|
45
50
|
_FALSEY = {"0", "false", "no", "off"}
|
|
@@ -146,7 +151,8 @@ async def is_assigned_vps(client: "BackendClient | None" = None) -> bool | None:
|
|
|
146
151
|
if forced is not None:
|
|
147
152
|
return forced
|
|
148
153
|
now = time.monotonic()
|
|
149
|
-
|
|
154
|
+
ttl = CACHE_TTL_S if _cache["value"] else NEGATIVE_CACHE_TTL_S
|
|
155
|
+
if _cache["value"] is not None and now - _cache["at"] < ttl:
|
|
150
156
|
return _cache["value"]
|
|
151
157
|
try:
|
|
152
158
|
if client is None:
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""An outdated AI Draftsman CLI is replaced, not left to fail every ticket.
|
|
2
|
+
|
|
3
|
+
Before 1.1.1 `ad` sent a tool schema Gemini rejects (an integer `enum` in
|
|
4
|
+
create_artifact), so every fallback run died on its first request with
|
|
5
|
+
"400 INVALID_ARGUMENT … function_declarations[5]". The fix shipped under a new npm
|
|
6
|
+
name, and machines kept the old CLI: the SDK used whatever was installed.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from sdk import local_exec, updater
|
|
18
|
+
|
|
19
|
+
_STALE = ("API error: INVALID_REQUEST: 400 INVALID_ARGUMENT Invalid value at "
|
|
20
|
+
"'tools[0].function_declarations[5].parameters.properties[8].value' (TYPE_STRING), 1")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture()
|
|
24
|
+
def npm(monkeypatch):
|
|
25
|
+
"""A scripted npm: `installed` is what `npm ls -g` reports; calls are recorded."""
|
|
26
|
+
state = {"installed": {}, "calls": [], "install_rc": 0}
|
|
27
|
+
monkeypatch.delenv("AGENTOS_FALLBACK_CLI", raising=False)
|
|
28
|
+
monkeypatch.setattr(local_exec, "_setting", lambda key: "")
|
|
29
|
+
monkeypatch.setattr(local_exec, "_npm_cmd", lambda *a: ["npm", *a])
|
|
30
|
+
|
|
31
|
+
def run(cmd, **kw):
|
|
32
|
+
args = cmd[1:]
|
|
33
|
+
state["calls"].append(args)
|
|
34
|
+
out, rc = "", 0
|
|
35
|
+
if args[:2] == ["ls", "-g"]:
|
|
36
|
+
out = json.dumps({"dependencies": {k: {"version": v} for k, v in state["installed"].items()}})
|
|
37
|
+
elif args[:2] == ["uninstall", "-g"]:
|
|
38
|
+
state["installed"].pop(args[2], None)
|
|
39
|
+
elif args[:2] == ["install", "-g"]:
|
|
40
|
+
rc = state["install_rc"]
|
|
41
|
+
if rc == 0:
|
|
42
|
+
state["installed"][local_exec.FALLBACK_PACKAGE] = "1.1.1"
|
|
43
|
+
return subprocess.CompletedProcess(cmd, rc, out, "EACCES" if rc else "")
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr(local_exec.subprocess, "run", run)
|
|
46
|
+
return state
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _changes(state):
|
|
50
|
+
return [c for c in state["calls"] if c[0] in ("install", "uninstall")]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestUpgrade:
|
|
54
|
+
def test_old_package_name_is_replaced(self, npm):
|
|
55
|
+
npm["installed"] = {"@aidraftsman/cli": "1.0.1"}
|
|
56
|
+
msg = local_exec.upgrade_fallback_cli()
|
|
57
|
+
assert _changes(npm) == [["uninstall", "-g", "@aidraftsman/cli"],
|
|
58
|
+
["install", "-g", f"{local_exec.FALLBACK_PACKAGE}@latest"]]
|
|
59
|
+
assert msg == "AI Draftsman CLI updated 1.0.1 → 1.1.1"
|
|
60
|
+
|
|
61
|
+
def test_an_old_release_is_upgraded(self, npm):
|
|
62
|
+
npm["installed"] = {local_exec.FALLBACK_PACKAGE: "1.1.0"}
|
|
63
|
+
assert local_exec.upgrade_fallback_cli().startswith("AI Draftsman CLI updated 1.1.0")
|
|
64
|
+
|
|
65
|
+
def test_a_current_release_is_left_alone(self, npm):
|
|
66
|
+
npm["installed"] = {local_exec.FALLBACK_PACKAGE: "1.2.0"}
|
|
67
|
+
assert local_exec.upgrade_fallback_cli() == ""
|
|
68
|
+
assert _changes(npm) == []
|
|
69
|
+
|
|
70
|
+
def test_never_installs_unasked(self, npm):
|
|
71
|
+
assert local_exec.upgrade_fallback_cli() == ""
|
|
72
|
+
assert _changes(npm) == []
|
|
73
|
+
|
|
74
|
+
def test_install_missing_installs(self, npm):
|
|
75
|
+
assert local_exec.upgrade_fallback_cli(install_missing=True).endswith("→ 1.1.1")
|
|
76
|
+
|
|
77
|
+
def test_a_failed_npm_says_how_to_do_it_by_hand(self, npm):
|
|
78
|
+
npm["installed"] = {"@aidraftsman/cli": "1.0.1"}
|
|
79
|
+
npm["install_rc"] = 1
|
|
80
|
+
msg = local_exec.upgrade_fallback_cli()
|
|
81
|
+
assert "could not update" in msg and local_exec.FALLBACK_INSTALL_HINT in msg
|
|
82
|
+
|
|
83
|
+
def test_an_owner_managed_cli_is_not_touched(self, npm, monkeypatch):
|
|
84
|
+
npm["installed"] = {"@aidraftsman/cli": "1.0.1"}
|
|
85
|
+
monkeypatch.setattr(local_exec, "_setting",
|
|
86
|
+
lambda key: "/opt/ad" if key == "AGENTOS_FALLBACK_CLI" else "")
|
|
87
|
+
assert local_exec.upgrade_fallback_cli() == ""
|
|
88
|
+
assert npm["calls"] == []
|
|
89
|
+
|
|
90
|
+
def test_start_up_check_runs_at_most_every_few_hours(self, npm, monkeypatch, tmp_path):
|
|
91
|
+
monkeypatch.setattr(local_exec.Path, "home", lambda: tmp_path)
|
|
92
|
+
npm["installed"] = {local_exec.FALLBACK_PACKAGE: "1.2.0"}
|
|
93
|
+
local_exec.maybe_upgrade_fallback_cli()
|
|
94
|
+
local_exec.maybe_upgrade_fallback_cli()
|
|
95
|
+
assert sum(1 for c in npm["calls"] if c[0] == "ls") == 1
|
|
96
|
+
|
|
97
|
+
def test_hint_names_the_new_package(self):
|
|
98
|
+
assert "@consultancy-outfit/ai-draftsman-cli" in local_exec.FALLBACK_INSTALL_HINT
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX script stand-in")
|
|
102
|
+
class TestSelfHeal:
|
|
103
|
+
def _cli(self, tmp_path):
|
|
104
|
+
"""Fails like an outdated `ad` on its first run, succeeds on the second."""
|
|
105
|
+
count = tmp_path / "runs"
|
|
106
|
+
script = tmp_path / "ad"
|
|
107
|
+
script.write_text(f"""#!{sys.executable}
|
|
108
|
+
import json, sys, pathlib
|
|
109
|
+
sys.stdin.read()
|
|
110
|
+
p = pathlib.Path({str(count)!r}); n = int(p.read_text()) if p.exists() else 0; p.write_text(str(n + 1))
|
|
111
|
+
if n == 0:
|
|
112
|
+
sys.stderr.write({_STALE!r}); sys.exit(1)
|
|
113
|
+
print(json.dumps({{"result": "done", "usage": {{}}}}))
|
|
114
|
+
""")
|
|
115
|
+
script.chmod(0o755)
|
|
116
|
+
return script, count
|
|
117
|
+
|
|
118
|
+
def test_upgrades_once_and_runs_again(self, tmp_path, monkeypatch):
|
|
119
|
+
cli, count = self._cli(tmp_path)
|
|
120
|
+
monkeypatch.setattr(local_exec, "fallback_cli_path", lambda: str(cli))
|
|
121
|
+
monkeypatch.setattr(local_exec, "upgrade_fallback_cli",
|
|
122
|
+
lambda **k: "AI Draftsman CLI updated 1.0.1 → 1.1.1")
|
|
123
|
+
res = asyncio.run(local_exec.run_fallback_cli("x", str(tmp_path / "p")))
|
|
124
|
+
assert res["ok"] is True and count.read_text() == "2"
|
|
125
|
+
|
|
126
|
+
def test_no_upgrade_means_no_retry_and_a_clear_error(self, tmp_path, monkeypatch):
|
|
127
|
+
cli, count = self._cli(tmp_path)
|
|
128
|
+
monkeypatch.setattr(local_exec, "fallback_cli_path", lambda: str(cli))
|
|
129
|
+
monkeypatch.setattr(local_exec, "upgrade_fallback_cli", lambda **k: "")
|
|
130
|
+
res = asyncio.run(local_exec.run_fallback_cli("x", str(tmp_path / "p")))
|
|
131
|
+
assert res["ok"] is False and count.read_text() == "1"
|
|
132
|
+
assert "outdated" in res["error"] and local_exec.FALLBACK_INSTALL_HINT in res["error"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_ctrl_c_while_waiting_for_the_restarted_run_exits_quietly(monkeypatch):
|
|
136
|
+
monkeypatch.delenv("AGENTOS_NO_UPDATE", raising=False)
|
|
137
|
+
monkeypatch.delenv("_AGENTOS_REEXEC", raising=False)
|
|
138
|
+
monkeypatch.setattr(updater, "auto_update", lambda quiet=False: (True, "1", "2"))
|
|
139
|
+
monkeypatch.setattr(updater, "_verify_install", lambda: True)
|
|
140
|
+
monkeypatch.setattr(local_exec, "maybe_upgrade_fallback_cli", lambda: "")
|
|
141
|
+
|
|
142
|
+
def interrupted(*a, **k):
|
|
143
|
+
raise KeyboardInterrupt
|
|
144
|
+
monkeypatch.setattr(updater.subprocess, "run", interrupted)
|
|
145
|
+
with pytest.raises(SystemExit) as exc:
|
|
146
|
+
updater.auto_update_and_reexec("sdk.run_auto", ["ticket", "WEL-9"])
|
|
147
|
+
assert exc.value.code == 130
|
|
@@ -257,6 +257,29 @@ class TestReporting:
|
|
|
257
257
|
assert "could not check" in text
|
|
258
258
|
assert "publickey" in text
|
|
259
259
|
|
|
260
|
+
def test_a_current_pypi_install_says_so(self):
|
|
261
|
+
_updated, said = self._run(installed="1.6.75", pypi="1.6.75", result=None,
|
|
262
|
+
is_github=False)
|
|
263
|
+
assert "already up to date (v1.6.75)" in "\n".join(said)
|
|
264
|
+
|
|
265
|
+
def test_unreachable_pypi_is_not_called_up_to_date(self):
|
|
266
|
+
"""A PyPI install that cannot reach PyPI never found out — say that."""
|
|
267
|
+
_updated, said = self._run(installed="1.6.73", pypi=None, result=None,
|
|
268
|
+
is_github=False)
|
|
269
|
+
text = "\n".join(said)
|
|
270
|
+
assert "already up to date" not in text
|
|
271
|
+
assert "could not reach PyPI" in text
|
|
272
|
+
|
|
273
|
+
def test_updates_switched_off_is_said_out_loud(self, monkeypatch):
|
|
274
|
+
"""AGENTOS_NO_UPDATE used to return silently, which the update command
|
|
275
|
+
then printed as "already up to date"."""
|
|
276
|
+
monkeypatch.setenv("AGENTOS_NO_UPDATE", "1")
|
|
277
|
+
said: list[str] = []
|
|
278
|
+
with patch.object(updater, "current_version", return_value="1.6.51"), \
|
|
279
|
+
patch.object(updater, "_say", said.append):
|
|
280
|
+
updater.auto_update(quiet=False)
|
|
281
|
+
assert "AGENTOS_NO_UPDATE" in "\n".join(said)
|
|
282
|
+
|
|
260
283
|
def test_quiet_says_nothing_at_all(self):
|
|
261
284
|
result = updater.UpgradeResult(False, "1.4.0", "1.4.0", True, "no-op")
|
|
262
285
|
said: list[str] = []
|
|
@@ -326,3 +349,37 @@ class TestPipInstall:
|
|
|
326
349
|
])
|
|
327
350
|
def test_failure_labels_name_the_source_without_leaking_it(url, label):
|
|
328
351
|
assert updater._source_label(url) == label
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ── `agentos-auto update`: the command must not override the report ───
|
|
355
|
+
class TestUpdateCommand:
|
|
356
|
+
"""The command called ``auto_update(quiet=True)`` and then printed its own
|
|
357
|
+
"already up to date" whenever the version had not moved — so a failed pip
|
|
358
|
+
(Mubashar's 1.6.73 box with 1.6.75 published, Tauqeer's 1.6.51 box) read as
|
|
359
|
+
current, and the report above was never shown to anyone."""
|
|
360
|
+
|
|
361
|
+
def _run_update(self, monkeypatch, capsys, result):
|
|
362
|
+
from sdk import run_auto
|
|
363
|
+
|
|
364
|
+
calls: list[bool] = []
|
|
365
|
+
|
|
366
|
+
def fake_auto_update(quiet=False):
|
|
367
|
+
calls.append(quiet)
|
|
368
|
+
return result
|
|
369
|
+
monkeypatch.setattr(updater, "auto_update", fake_auto_update)
|
|
370
|
+
monkeypatch.setattr(sys, "argv", ["agentos-auto", "update"])
|
|
371
|
+
monkeypatch.setattr(run_auto, "_daemon_running", lambda: None)
|
|
372
|
+
import sdk.github_ingest as gi
|
|
373
|
+
monkeypatch.setattr(gi, "run_backfill_once", lambda: {"ran": False})
|
|
374
|
+
import sdk.vps_health as vh
|
|
375
|
+
monkeypatch.setattr(vh, "enabled", lambda: False)
|
|
376
|
+
run_auto.main()
|
|
377
|
+
return calls, capsys.readouterr().out
|
|
378
|
+
|
|
379
|
+
def test_it_asks_for_the_full_report(self, monkeypatch, capsys):
|
|
380
|
+
calls, _ = self._run_update(monkeypatch, capsys, (False, "1.6.73", None))
|
|
381
|
+
assert calls == [False]
|
|
382
|
+
|
|
383
|
+
def test_it_never_prints_its_own_up_to_date(self, monkeypatch, capsys):
|
|
384
|
+
_, out = self._run_update(monkeypatch, capsys, (False, "1.6.73", None))
|
|
385
|
+
assert "already up to date" not in out
|
|
@@ -91,6 +91,33 @@ class TestIsAssignedVps:
|
|
|
91
91
|
assert asyncio.run(vps_identity.is_assigned_vps(client)) is True
|
|
92
92
|
assert client.profile_calls == 1
|
|
93
93
|
|
|
94
|
+
def test_a_newly_assigned_vps_is_picked_up_within_minutes(self, monkeypatch):
|
|
95
|
+
"""agentos installed BEFORE the allocation existed: the first answer is
|
|
96
|
+
"no VPS assigned". Cached for 6 h, that kept a freshly assigned VPS silent
|
|
97
|
+
— no health, activity discarded — for the rest of the day."""
|
|
98
|
+
_holds(monkeypatch, "217.160.157.148")
|
|
99
|
+
clock = {"t": 1000.0}
|
|
100
|
+
monkeypatch.setattr(vps_identity.time, "monotonic", lambda: clock["t"])
|
|
101
|
+
|
|
102
|
+
before = _Client(assigned="")
|
|
103
|
+
assert asyncio.run(vps_identity.is_assigned_vps(before)) is False
|
|
104
|
+
after = _Client(assigned="217.160.157.148")
|
|
105
|
+
clock["t"] += 60
|
|
106
|
+
assert asyncio.run(vps_identity.is_assigned_vps(after)) is False # still cached
|
|
107
|
+
clock["t"] += vps_identity.NEGATIVE_CACHE_TTL_S
|
|
108
|
+
assert asyncio.run(vps_identity.is_assigned_vps(after)) is True # re-asked
|
|
109
|
+
assert vps_identity.NEGATIVE_CACHE_TTL_S <= 15 * 60
|
|
110
|
+
|
|
111
|
+
def test_a_yes_is_still_cached_for_hours(self, monkeypatch):
|
|
112
|
+
_holds(monkeypatch, "77.68.23.23")
|
|
113
|
+
clock = {"t": 1000.0}
|
|
114
|
+
monkeypatch.setattr(vps_identity.time, "monotonic", lambda: clock["t"])
|
|
115
|
+
client = _Client()
|
|
116
|
+
asyncio.run(vps_identity.is_assigned_vps(client))
|
|
117
|
+
clock["t"] += vps_identity.NEGATIVE_CACHE_TTL_S * 3
|
|
118
|
+
asyncio.run(vps_identity.is_assigned_vps(client))
|
|
119
|
+
assert client.profile_calls == 1
|
|
120
|
+
|
|
94
121
|
@pytest.mark.parametrize("raw, want", [("1", True), ("true", True), ("0", False), ("off", False)])
|
|
95
122
|
def test_override_wins_without_a_profile_call(self, monkeypatch, raw, want):
|
|
96
123
|
monkeypatch.setenv("AGENTOS_IS_VPS", raw)
|
|
File without changes
|
{devops_bot_sdk-1.6.76 → devops_bot_sdk-1.6.79}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|