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