devops-bot-sdk 1.6.57__tar.gz → 1.6.60__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.57 → devops_bot_sdk-1.6.60}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/cli.py +168 -1
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/client.py +85 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ticket_fields.py +51 -0
- devops_bot_sdk-1.6.60/sdk/tickets.py +177 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/api.py +58 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/models.py +32 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/app.js +122 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/styles.css +4 -0
- devops_bot_sdk-1.6.60/sdk/ui/static/tickets.html +119 -0
- devops_bot_sdk-1.6.60/tests/test_ticket_cli.py +342 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ticket_fields.py +6 -1
- devops_bot_sdk-1.6.57/sdk/ui/static/tickets.html +0 -47
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/README.md +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.57 → devops_bot_sdk-1.6.60}/tests/test_write_hook.py +0 -0
|
@@ -47,6 +47,7 @@ sdk/task_types.py
|
|
|
47
47
|
sdk/test.py
|
|
48
48
|
sdk/test_pipeline.py
|
|
49
49
|
sdk/ticket_fields.py
|
|
50
|
+
sdk/tickets.py
|
|
50
51
|
sdk/updater.py
|
|
51
52
|
sdk/url_access.py
|
|
52
53
|
sdk/vps_health.py
|
|
@@ -207,6 +208,7 @@ tests/test_stale_panel_restart.py
|
|
|
207
208
|
tests/test_swagger_command.py
|
|
208
209
|
tests/test_task_file_and_plan_run.py
|
|
209
210
|
tests/test_task_file_format_doc.py
|
|
211
|
+
tests/test_ticket_cli.py
|
|
210
212
|
tests/test_ticket_fields.py
|
|
211
213
|
tests/test_ticket_run_resolve_by_id.py
|
|
212
214
|
tests/test_ticket_speed_fixes.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.60
|
|
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.60" # 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
|
|
@@ -49,6 +49,21 @@ Command surface:
|
|
|
49
49
|
agentos agent rm <name>
|
|
50
50
|
De-register the agent from the backend and delete its local manifest.
|
|
51
51
|
|
|
52
|
+
agentos ticket create --project KEY --summary TEXT [--type T] [--priority P]
|
|
53
|
+
[--assignee EMAIL] [--description[-file] …] [--instructions[-file] …]
|
|
54
|
+
[--parent ID] [--status S] [--points N]
|
|
55
|
+
File one ticket (POST /tasks-ml) and print its Jira key. Type defaults to
|
|
56
|
+
Task, priority to Medium, assignee to the token owner. Validated locally
|
|
57
|
+
first; a gateway rejection prints the gateway's own messages.
|
|
58
|
+
agentos ticket update KEY_OR_ID [--summary …] [--description[-file] …]
|
|
59
|
+
[--instructions[-file] …] [--priority P] [--status S] [--points N]
|
|
60
|
+
[--assignee EMAIL] [--type T]
|
|
61
|
+
Change only the given fields (PUT /tasks-ml/{id}). Takes a Jira key or
|
|
62
|
+
the platform id. See sdk.tickets — the Tickets page uses the same code.
|
|
63
|
+
agentos ticket delete KEY_OR_ID [--yes]
|
|
64
|
+
Show the ticket and, after a y/N confirmation (skipped by --yes), delete it
|
|
65
|
+
(DELETE /tasks-ml/{id}). Cannot be undone.
|
|
66
|
+
|
|
52
67
|
agentos integration add <path>
|
|
53
68
|
Register an integration (MCP server, etc.) from a JSON file.
|
|
54
69
|
agentos integration test <name>
|
|
@@ -162,6 +177,10 @@ def main() -> None:
|
|
|
162
177
|
_integration_command(rest)
|
|
163
178
|
return
|
|
164
179
|
|
|
180
|
+
if noun == "ticket":
|
|
181
|
+
_ticket_command(rest)
|
|
182
|
+
return
|
|
183
|
+
|
|
165
184
|
if noun == "agentd":
|
|
166
185
|
_agentd_command(rest)
|
|
167
186
|
return
|
|
@@ -202,12 +221,13 @@ def main() -> None:
|
|
|
202
221
|
|
|
203
222
|
|
|
204
223
|
def _print_usage() -> None:
|
|
205
|
-
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser|fallback|firewall|activity-setup|swagger> ...")
|
|
224
|
+
print("usage: agentos <configure|chat|agent|ticket|integration|default-agents|agentd|github|report|browser|fallback|firewall|activity-setup|swagger> ...")
|
|
206
225
|
print()
|
|
207
226
|
print(" agentos configure [--rotate]")
|
|
208
227
|
print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
|
|
209
228
|
print(" agentos agent <list|run NAME|chat NAME MSG|logs NAME|enable NAME|disable NAME|rm NAME>")
|
|
210
229
|
print(" agentos agent <set-key NAME KEY[=VALUE]…|keys NAME|rm-key NAME KEY…> # persistent API keys (custom agents)")
|
|
230
|
+
print(" agentos ticket <create|update KEY_OR_ID|delete KEY_OR_ID|help>")
|
|
211
231
|
print(" agentos integration <add PATH|test NAME|list|rm NAME>")
|
|
212
232
|
print(" agentos default-agents list [--department-id UUID] # read-only; agents are created in the web app")
|
|
213
233
|
print(" agentos agentd <start [--foreground]|status>")
|
|
@@ -625,6 +645,153 @@ def _pop_department_id(args: list[str]) -> tuple[str | None, list[str]]:
|
|
|
625
645
|
return dept_id, out
|
|
626
646
|
|
|
627
647
|
|
|
648
|
+
# `agentos ticket` flag → POST/PUT /tasks-ml wire name. Text flags have a
|
|
649
|
+
# `-file` twin that reads the value from a file instead.
|
|
650
|
+
_TICKET_FLAGS = {
|
|
651
|
+
"--project": "projectKey", "--type": "type", "--summary": "summary",
|
|
652
|
+
"--priority": "priority", "--assignee": "assigneeEmail", "--status": "status",
|
|
653
|
+
"--points": "points", "--parent": "parentTaskId",
|
|
654
|
+
"--description": "description", "--instructions": "humanInstructions",
|
|
655
|
+
}
|
|
656
|
+
_TICKET_FILE_FLAGS = {"--description-file": "--description",
|
|
657
|
+
"--instructions-file": "--instructions"}
|
|
658
|
+
# Create-only: neither is editable through PUT /tasks-ml/{id}.
|
|
659
|
+
_TICKET_CREATE_ONLY = {"--project", "--parent"}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _ticket_usage() -> None:
|
|
663
|
+
print("usage: agentos ticket <create|update KEY_OR_ID|delete KEY_OR_ID|help>")
|
|
664
|
+
print()
|
|
665
|
+
print(" agentos ticket create --project KEY --summary TEXT [--type TYPE] [--priority P]")
|
|
666
|
+
print(" [--assignee EMAIL] [--description TEXT | --description-file PATH]")
|
|
667
|
+
print(" [--instructions TEXT | --instructions-file PATH] [--parent ID] [--status S] [--points N]")
|
|
668
|
+
print(" agentos ticket update KEY_OR_ID [--summary TEXT] [--description TEXT | --description-file PATH]")
|
|
669
|
+
print(" [--instructions TEXT | --instructions-file PATH] [--priority P] [--status S]")
|
|
670
|
+
print(" [--points N] [--assignee EMAIL] [--type TYPE]")
|
|
671
|
+
print(" agentos ticket delete KEY_OR_ID [--yes] # asks to confirm unless --yes")
|
|
672
|
+
print()
|
|
673
|
+
print(" TYPE: Epic|Story|Task|Bug|Sub-task (default Task) P: Lowest|Low|Medium|High|Highest (default Medium)")
|
|
674
|
+
print(" --assignee defaults to the configured token's owner.")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _parse_ticket_flags(args: list[str], *, creating: bool) -> dict[str, str]:
|
|
678
|
+
"""Flags → wire fields. Exits 1 on an unknown flag, a missing value, both a
|
|
679
|
+
text flag and its -file twin, or an unreadable file."""
|
|
680
|
+
fields: dict[str, str] = {}
|
|
681
|
+
seen: set[str] = set()
|
|
682
|
+
i = 0
|
|
683
|
+
while i < len(args):
|
|
684
|
+
flag = args[i]
|
|
685
|
+
known = flag in _TICKET_FLAGS or flag in _TICKET_FILE_FLAGS
|
|
686
|
+
if not known or (not creating and flag in _TICKET_CREATE_ONLY):
|
|
687
|
+
print(f"error: unknown option for `agentos ticket {'create' if creating else 'update'}`: {flag}")
|
|
688
|
+
sys.exit(1)
|
|
689
|
+
if i + 1 >= len(args):
|
|
690
|
+
print(f"error: expected a value after {flag}")
|
|
691
|
+
sys.exit(1)
|
|
692
|
+
value = args[i + 1]
|
|
693
|
+
text_flag = _TICKET_FILE_FLAGS.get(flag, flag)
|
|
694
|
+
if text_flag in seen:
|
|
695
|
+
print(f"error: pass {text_flag} or {text_flag}-file once, not both")
|
|
696
|
+
sys.exit(1)
|
|
697
|
+
seen.add(text_flag)
|
|
698
|
+
if flag in _TICKET_FILE_FLAGS:
|
|
699
|
+
path = Path(value).expanduser()
|
|
700
|
+
try:
|
|
701
|
+
value = path.read_text(encoding="utf-8")
|
|
702
|
+
except OSError as exc:
|
|
703
|
+
print(f"error: {flag}: cannot read {path}: {exc.strerror or exc}")
|
|
704
|
+
sys.exit(1)
|
|
705
|
+
fields[_TICKET_FLAGS[text_flag]] = value
|
|
706
|
+
i += 2
|
|
707
|
+
return fields
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _ticket_command(args: list[str]) -> None:
|
|
711
|
+
"""`agentos ticket create|update` — see sdk.tickets for the rules."""
|
|
712
|
+
verb = args[0] if args else "help"
|
|
713
|
+
if verb in {"help", "-h", "--help"}:
|
|
714
|
+
_ticket_usage()
|
|
715
|
+
sys.exit(0 if args else 1)
|
|
716
|
+
if verb not in {"create", "update", "delete"}:
|
|
717
|
+
_ticket_usage()
|
|
718
|
+
sys.exit(1)
|
|
719
|
+
if verb in {"update", "delete"} and (len(args) < 2 or args[1].startswith("--")):
|
|
720
|
+
_ticket_usage()
|
|
721
|
+
sys.exit(1)
|
|
722
|
+
|
|
723
|
+
from sdk import tickets
|
|
724
|
+
from sdk.client import BackendClient
|
|
725
|
+
|
|
726
|
+
if verb == "delete":
|
|
727
|
+
_ticket_delete(args[1], args[2:])
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
if verb == "create":
|
|
731
|
+
fields = _parse_ticket_flags(args[1:], creating=True)
|
|
732
|
+
if not fields.get("projectKey") or not fields.get("summary"):
|
|
733
|
+
print("error: --project and --summary are required")
|
|
734
|
+
sys.exit(1)
|
|
735
|
+
else:
|
|
736
|
+
fields = _parse_ticket_flags(args[2:], creating=False)
|
|
737
|
+
if not fields:
|
|
738
|
+
print("error: nothing to update: pass at least one field")
|
|
739
|
+
_ticket_usage()
|
|
740
|
+
sys.exit(1)
|
|
741
|
+
# Validate before building a client, so a bad flag never needs a token.
|
|
742
|
+
err = tickets.validate_fields(fields)
|
|
743
|
+
if err:
|
|
744
|
+
print(f"error: {err}")
|
|
745
|
+
sys.exit(1)
|
|
746
|
+
|
|
747
|
+
client = BackendClient.from_config()
|
|
748
|
+
if verb == "create":
|
|
749
|
+
res = asyncio.run(tickets.create_ticket(client, fields))
|
|
750
|
+
else:
|
|
751
|
+
res = asyncio.run(tickets.update_ticket(client, args[1], fields))
|
|
752
|
+
if not res["ok"]:
|
|
753
|
+
print(f"error: {res['error']}")
|
|
754
|
+
sys.exit(1)
|
|
755
|
+
label = res["key"] or res["id"]
|
|
756
|
+
if verb == "create":
|
|
757
|
+
print(f"created {label} (id {res['id']})")
|
|
758
|
+
else:
|
|
759
|
+
print(f"updated {label}: {', '.join(sorted(fields))}")
|
|
760
|
+
sys.exit(0)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _ticket_delete(target: str, flags: list[str]) -> None:
|
|
764
|
+
"""`agentos ticket delete KEY_OR_ID [--yes]` — shows the ticket, then asks."""
|
|
765
|
+
from sdk import tickets
|
|
766
|
+
from sdk.client import BackendClient
|
|
767
|
+
|
|
768
|
+
unknown = [f for f in flags if f not in {"--yes", "-y"}]
|
|
769
|
+
if unknown:
|
|
770
|
+
print(f"error: unknown option for `agentos ticket delete`: {unknown[0]}")
|
|
771
|
+
sys.exit(1)
|
|
772
|
+
client = BackendClient.from_config()
|
|
773
|
+
rec = asyncio.run(tickets.get_ticket(client, target))
|
|
774
|
+
if rec is None:
|
|
775
|
+
print(f"error: ticket not found: {target}")
|
|
776
|
+
sys.exit(1)
|
|
777
|
+
label = rec.get("key") or rec.get("id")
|
|
778
|
+
if not flags:
|
|
779
|
+
print(f"{label} [{rec.get('status') or '?'}] {rec.get('summary') or ''}")
|
|
780
|
+
try:
|
|
781
|
+
answer = input(f"Delete {label}? This cannot be undone. [y/N] ")
|
|
782
|
+
except EOFError:
|
|
783
|
+
answer = ""
|
|
784
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
785
|
+
print("not deleted")
|
|
786
|
+
sys.exit(1)
|
|
787
|
+
res = asyncio.run(tickets.delete_ticket(client, str(rec.get("id") or target)))
|
|
788
|
+
if not res["ok"]:
|
|
789
|
+
print(f"error: {res['error']}")
|
|
790
|
+
sys.exit(1)
|
|
791
|
+
print(f"deleted {label}")
|
|
792
|
+
sys.exit(0)
|
|
793
|
+
|
|
794
|
+
|
|
628
795
|
def _resolve_department_id(explicit: str | None) -> str:
|
|
629
796
|
"""Use the explicit --department-id, else the caller's own from bootstrap()."""
|
|
630
797
|
if explicit:
|
|
@@ -249,6 +249,7 @@ class BackendClient:
|
|
|
249
249
|
# busy backend (retry in 30 min) from a rejected request (wait for the
|
|
250
250
|
# next slot). Empty dict means "no failure recorded".
|
|
251
251
|
self.last_ingest_error: dict = {}
|
|
252
|
+
self.last_task_ml_error: dict = {}
|
|
252
253
|
self._headers = {
|
|
253
254
|
"Authorization": token, # co_<uuid>_<hex> — no Bearer prefix
|
|
254
255
|
"X-SDK-Version": SDK_VERSION,
|
|
@@ -1274,6 +1275,48 @@ class BackendClient:
|
|
|
1274
1275
|
logger.warning("client.get_task_status list failed task=%s err=%s", task_id, exc)
|
|
1275
1276
|
return None
|
|
1276
1277
|
|
|
1278
|
+
async def get_task_ml(self, task_id: str) -> dict | None:
|
|
1279
|
+
"""GET {BE}/tasks-ml/{id} → the full RAW record (jiraIssueKey, summary, …), or None.
|
|
1280
|
+
|
|
1281
|
+
Same read as ``get_task_status`` — direct single-record read, then a scan
|
|
1282
|
+
of the list endpoint for a backend that doesn't serve it — but returns the
|
|
1283
|
+
whole record. Matches `id` or the Jira key. Best-effort: never raises.
|
|
1284
|
+
"""
|
|
1285
|
+
from sdk.config import BE_DEFAULT_URL
|
|
1286
|
+
|
|
1287
|
+
if not task_id:
|
|
1288
|
+
return None
|
|
1289
|
+
base = BE_DEFAULT_URL.rstrip("/")
|
|
1290
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1291
|
+
try:
|
|
1292
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
1293
|
+
resp = await client.get(f"{base}/tasks-ml/{task_id}", headers=headers)
|
|
1294
|
+
if resp.status_code < 400:
|
|
1295
|
+
data = (resp.json() or {}).get("data") or {}
|
|
1296
|
+
if isinstance(data, dict):
|
|
1297
|
+
rec = data.get("record") if isinstance(data.get("record"), dict) else data
|
|
1298
|
+
if rec.get("id"):
|
|
1299
|
+
return rec
|
|
1300
|
+
except Exception as exc: # noqa: BLE001 — fall through to the list scan
|
|
1301
|
+
logger.warning("client.get_task_ml direct failed task=%s err=%s", task_id, exc)
|
|
1302
|
+
try:
|
|
1303
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
1304
|
+
resp = await client.get(f"{base}/tasks-ml", params={"limit": 10000},
|
|
1305
|
+
headers=headers)
|
|
1306
|
+
if resp.status_code >= 400:
|
|
1307
|
+
return None
|
|
1308
|
+
records = ((resp.json() or {}).get("data") or {}).get("records") or []
|
|
1309
|
+
want = str(task_id).strip().upper()
|
|
1310
|
+
for rec in records:
|
|
1311
|
+
if isinstance(rec, dict) and want in {
|
|
1312
|
+
str(rec.get(k) or "").strip().upper()
|
|
1313
|
+
for k in ("id", "jiraIssueKey", "jiraKey")
|
|
1314
|
+
}:
|
|
1315
|
+
return rec
|
|
1316
|
+
except Exception as exc: # noqa: BLE001
|
|
1317
|
+
logger.warning("client.get_task_ml list failed task=%s err=%s", task_id, exc)
|
|
1318
|
+
return None
|
|
1319
|
+
|
|
1277
1320
|
async def set_task_status(self, task_id: str, status: str) -> bool:
|
|
1278
1321
|
"""PUT {BE}/tasks-ml/{id} status=<status> (best-effort, arbitrary target).
|
|
1279
1322
|
|
|
@@ -1435,11 +1478,17 @@ class BackendClient:
|
|
|
1435
1478
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
1436
1479
|
resp = await client.put(url, files=files, headers=headers)
|
|
1437
1480
|
if resp.status_code >= 400:
|
|
1481
|
+
try:
|
|
1482
|
+
self.last_task_ml_error = resp.json() or {}
|
|
1483
|
+
except Exception: # noqa: BLE001
|
|
1484
|
+
self.last_task_ml_error = {"error": resp.text or "request failed"}
|
|
1438
1485
|
logger.warning("client.update_task_ml failed task=%s status=%s body=%s",
|
|
1439
1486
|
task_id, resp.status_code, resp.text[:200])
|
|
1440
1487
|
return False
|
|
1488
|
+
self.last_task_ml_error = {}
|
|
1441
1489
|
return True
|
|
1442
1490
|
except Exception as exc: # noqa: BLE001
|
|
1491
|
+
self.last_task_ml_error = {"error": str(exc)}
|
|
1443
1492
|
logger.warning("client.update_task_ml error task=%s err=%s", task_id, exc)
|
|
1444
1493
|
return False
|
|
1445
1494
|
|
|
@@ -1458,6 +1507,7 @@ class BackendClient:
|
|
|
1458
1507
|
from sdk.config import BE_DEFAULT_URL
|
|
1459
1508
|
from sdk.ticket_fields import to_multipart
|
|
1460
1509
|
|
|
1510
|
+
self.last_task_ml_error = {}
|
|
1461
1511
|
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml"
|
|
1462
1512
|
headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
|
|
1463
1513
|
headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
|
|
@@ -1468,9 +1518,14 @@ class BackendClient:
|
|
|
1468
1518
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
1469
1519
|
resp = await client.post(url, files=files, headers=headers)
|
|
1470
1520
|
if resp.status_code >= 400:
|
|
1521
|
+
try:
|
|
1522
|
+
self.last_task_ml_error = resp.json() or {}
|
|
1523
|
+
except Exception: # noqa: BLE001
|
|
1524
|
+
self.last_task_ml_error = {"error": resp.text or "request failed"}
|
|
1471
1525
|
logger.warning("client.create_task_ml failed status=%s body=%s",
|
|
1472
1526
|
resp.status_code, resp.text[:300])
|
|
1473
1527
|
return None
|
|
1528
|
+
self.last_task_ml_error = {}
|
|
1474
1529
|
body = resp.json() or {}
|
|
1475
1530
|
rec = body.get("data") or body
|
|
1476
1531
|
# The created record's id may sit at data.id or data.record.id.
|
|
@@ -1478,9 +1533,39 @@ class BackendClient:
|
|
|
1478
1533
|
return rec.get("id") or ((rec.get("record") or {}).get("id"))
|
|
1479
1534
|
return None
|
|
1480
1535
|
except Exception as exc: # noqa: BLE001
|
|
1536
|
+
self.last_task_ml_error = {"error": str(exc)}
|
|
1481
1537
|
logger.warning("client.create_task_ml error err=%s", exc)
|
|
1482
1538
|
return None
|
|
1483
1539
|
|
|
1540
|
+
async def delete_task_ml(self, task_id: str) -> bool:
|
|
1541
|
+
"""DELETE {BE}/tasks-ml/{id} — remove a ticket. True on 2xx.
|
|
1542
|
+
|
|
1543
|
+
On failure the gateway's JSON body (e.g. ``{"message": "Task not found"}``)
|
|
1544
|
+
is kept in ``last_task_ml_error``, as for create/update. Never raises.
|
|
1545
|
+
"""
|
|
1546
|
+
from sdk.config import BE_DEFAULT_URL
|
|
1547
|
+
|
|
1548
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}"
|
|
1549
|
+
headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
|
|
1550
|
+
headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
|
|
1551
|
+
try:
|
|
1552
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
1553
|
+
resp = await client.delete(url, headers=headers)
|
|
1554
|
+
if resp.status_code >= 400:
|
|
1555
|
+
try:
|
|
1556
|
+
self.last_task_ml_error = resp.json() or {}
|
|
1557
|
+
except Exception: # noqa: BLE001
|
|
1558
|
+
self.last_task_ml_error = {"error": resp.text or "request failed"}
|
|
1559
|
+
logger.warning("client.delete_task_ml failed task=%s status=%s body=%s",
|
|
1560
|
+
task_id, resp.status_code, resp.text[:200])
|
|
1561
|
+
return False
|
|
1562
|
+
self.last_task_ml_error = {}
|
|
1563
|
+
return True
|
|
1564
|
+
except Exception as exc: # noqa: BLE001
|
|
1565
|
+
self.last_task_ml_error = {"error": str(exc)}
|
|
1566
|
+
logger.warning("client.delete_task_ml error task=%s err=%s", task_id, exc)
|
|
1567
|
+
return False
|
|
1568
|
+
|
|
1484
1569
|
# ── Jira projects ──────────────────────────────────────────────────
|
|
1485
1570
|
# A Jira project is the container a ticket's `projectKey` points at, so these
|
|
1486
1571
|
# sit next to create_task_ml: a flow that has no projectKey yet resolves one
|
|
@@ -23,6 +23,10 @@ from __future__ import annotations
|
|
|
23
23
|
MultipartPart = tuple[str, tuple[None, str]]
|
|
24
24
|
MultipartBody = list[MultipartPart]
|
|
25
25
|
|
|
26
|
+
VALID_TICKET_TYPES = frozenset({"Epic", "Story", "Task", "Bug", "Sub-task"})
|
|
27
|
+
VALID_PRIORITIES = frozenset({"Lowest", "Low", "Medium", "High", "Highest"})
|
|
28
|
+
MAX_SUMMARY_LENGTH = 255
|
|
29
|
+
|
|
26
30
|
# The field names the SDK sends to POST /tasks-ml, from the real call sites in
|
|
27
31
|
# sdk/ipc/handlers.py (_epic_inherited_fields, and the child-story `fields` dict
|
|
28
32
|
# in the Epic decomposition loop). Documentation and an assertion target for
|
|
@@ -90,6 +94,46 @@ def is_sla_field(key: str) -> bool:
|
|
|
90
94
|
return _normalise(key) in SLA_FIELDS
|
|
91
95
|
|
|
92
96
|
|
|
97
|
+
def validate_ticket_type(value: str) -> str | None:
|
|
98
|
+
"""Return an error string when `value` is not an allowed ticket type."""
|
|
99
|
+
if value in VALID_TICKET_TYPES:
|
|
100
|
+
return None
|
|
101
|
+
return (
|
|
102
|
+
"invalid type: "
|
|
103
|
+
f"{value!r}. Expected one of: {', '.join(sorted(VALID_TICKET_TYPES))}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def validate_priority(value: str) -> str | None:
|
|
108
|
+
"""Return an error string when `value` is not an allowed priority."""
|
|
109
|
+
if value in VALID_PRIORITIES:
|
|
110
|
+
return None
|
|
111
|
+
return (
|
|
112
|
+
"invalid priority: "
|
|
113
|
+
f"{value!r}. Expected one of: {', '.join(sorted(VALID_PRIORITIES))}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_email(value: str) -> str | None:
|
|
118
|
+
"""Return an error string when `value` is not shaped like an email."""
|
|
119
|
+
text = str(value or "").strip()
|
|
120
|
+
if text and "@" in text and not text.startswith("@") and not text.endswith("@"):
|
|
121
|
+
local, _, domain = text.partition("@")
|
|
122
|
+
if local and domain and "." in domain and not domain.startswith(".") and not domain.endswith("."):
|
|
123
|
+
return None
|
|
124
|
+
return f"invalid assignee email: {value!r}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def validate_summary_length(value: str) -> str | None:
|
|
128
|
+
"""Return an error string when `summary` exceeds the backend limit."""
|
|
129
|
+
if len(value) <= MAX_SUMMARY_LENGTH:
|
|
130
|
+
return None
|
|
131
|
+
return (
|
|
132
|
+
f"summary must be {MAX_SUMMARY_LENGTH} characters or fewer "
|
|
133
|
+
f"(got {len(value)})"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
93
137
|
def to_multipart(
|
|
94
138
|
fields: dict[str, object],
|
|
95
139
|
model_used: list[str] | None = None,
|
|
@@ -141,9 +185,16 @@ def to_multipart(
|
|
|
141
185
|
# The field contract itself. The MultipartPart/MultipartBody aliases above are
|
|
142
186
|
# importable annotation helpers, not part of the contract, so they stay out.
|
|
143
187
|
__all__ = [
|
|
188
|
+
"MAX_SUMMARY_LENGTH",
|
|
144
189
|
"OMITTED_FIELDS",
|
|
145
190
|
"SLA_FIELDS",
|
|
146
191
|
"TASKS_ML_FIELDS",
|
|
192
|
+
"VALID_PRIORITIES",
|
|
193
|
+
"VALID_TICKET_TYPES",
|
|
147
194
|
"is_sla_field",
|
|
148
195
|
"to_multipart",
|
|
196
|
+
"validate_email",
|
|
197
|
+
"validate_priority",
|
|
198
|
+
"validate_summary_length",
|
|
199
|
+
"validate_ticket_type",
|
|
149
200
|
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Create, edit and delete tickets — the one implementation behind `agentos ticket` and
|
|
2
|
+
the control panel's Tickets page.
|
|
3
|
+
|
|
4
|
+
Both front ends gather the same flags/fields, so the rules live here once:
|
|
5
|
+
validation against the gateway's POST /tasks-ml contract (``sdk.ticket_fields``),
|
|
6
|
+
the assignee default (the token owner's email), Jira-key → platform-id
|
|
7
|
+
resolution, and reading the created ticket's Jira key back. Every function
|
|
8
|
+
returns a result dict instead of printing or raising, so the CLI can print it
|
|
9
|
+
and the HTTP layer can turn it into a status code.
|
|
10
|
+
|
|
11
|
+
Result shape: ``{"ok": bool, "id": str, "key": str, "error": str}``, plus
|
|
12
|
+
``"details"`` (the gateway's JSON error body) when the backend rejected the call.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from sdk.ticket_fields import (
|
|
20
|
+
validate_email,
|
|
21
|
+
validate_priority,
|
|
22
|
+
validate_summary_length,
|
|
23
|
+
validate_ticket_type,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# PROJ-123. A platform id is a UUID, which also contains dashes, so "has a dash"
|
|
27
|
+
# is not enough to tell the two apart.
|
|
28
|
+
_JIRA_KEY_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*-\d+$")
|
|
29
|
+
|
|
30
|
+
# Editable fields, by wire name. `None` drops a field; anything else is sent.
|
|
31
|
+
EDITABLE_FIELDS = (
|
|
32
|
+
"summary", "description", "humanInstructions", "priority", "status",
|
|
33
|
+
"points", "assigneeEmail", "type",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_jira_key(value: str) -> bool:
|
|
38
|
+
return bool(_JIRA_KEY_RE.match(str(value or "").strip()))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _fail(error: str, **extra: Any) -> dict:
|
|
42
|
+
return {"ok": False, "id": "", "key": "", "error": error, **extra}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_fields(fields: dict[str, Any]) -> str | None:
|
|
46
|
+
"""The first validation error in `fields` (only the fields present), or None."""
|
|
47
|
+
if fields.get("summary") is not None:
|
|
48
|
+
if not str(fields["summary"]).strip():
|
|
49
|
+
return "summary must not be empty"
|
|
50
|
+
err = validate_summary_length(str(fields["summary"]))
|
|
51
|
+
if err:
|
|
52
|
+
return err
|
|
53
|
+
if fields.get("type") is not None and (err := validate_ticket_type(str(fields["type"]))):
|
|
54
|
+
return err
|
|
55
|
+
if fields.get("priority") is not None and (err := validate_priority(str(fields["priority"]))):
|
|
56
|
+
return err
|
|
57
|
+
if fields.get("assigneeEmail") is not None and (err := validate_email(str(fields["assigneeEmail"]))):
|
|
58
|
+
return err
|
|
59
|
+
if fields.get("projectKey") is not None and len(str(fields["projectKey"])) > 50:
|
|
60
|
+
return "project key must be 50 characters or fewer"
|
|
61
|
+
points = fields.get("points")
|
|
62
|
+
if points is not None and str(points).strip() != "":
|
|
63
|
+
try:
|
|
64
|
+
if int(str(points)) < 0:
|
|
65
|
+
raise ValueError
|
|
66
|
+
except ValueError:
|
|
67
|
+
return f"points must be a whole number of 0 or more (got {points!r})"
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def owner_email(client) -> str:
|
|
72
|
+
"""The configured token owner's email, or "" when it can't be read."""
|
|
73
|
+
try:
|
|
74
|
+
return str((await client.profile()).email or "").strip()
|
|
75
|
+
except Exception: # noqa: BLE001
|
|
76
|
+
return ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def resolve_id(client, key_or_id: str) -> str | None:
|
|
80
|
+
"""Platform task id for a Jira key (AP2-123) or an id; None when not found."""
|
|
81
|
+
target = str(key_or_id or "").strip()
|
|
82
|
+
if not target:
|
|
83
|
+
return None
|
|
84
|
+
if not is_jira_key(target):
|
|
85
|
+
return target
|
|
86
|
+
rows = await client.list_tasks_by_keys([target])
|
|
87
|
+
if not rows:
|
|
88
|
+
return None
|
|
89
|
+
return str(rows[0].get("id") or "") or None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def create_ticket(client, fields: dict[str, Any]) -> dict:
|
|
93
|
+
"""POST /tasks-ml. `fields` uses wire names; type/priority default to Task/Medium
|
|
94
|
+
and assigneeEmail to the token owner. Validates before any HTTP call."""
|
|
95
|
+
body = {k: v for k, v in fields.items() if v is not None and v != ""}
|
|
96
|
+
body.setdefault("type", "Task")
|
|
97
|
+
body.setdefault("priority", "Medium")
|
|
98
|
+
if not str(body.get("projectKey") or "").strip():
|
|
99
|
+
return _fail("project is required")
|
|
100
|
+
if not str(body.get("summary") or "").strip():
|
|
101
|
+
return _fail("summary is required")
|
|
102
|
+
err = validate_fields(body)
|
|
103
|
+
if err:
|
|
104
|
+
return _fail(err)
|
|
105
|
+
if not body.get("assigneeEmail"):
|
|
106
|
+
email = await owner_email(client)
|
|
107
|
+
if not email:
|
|
108
|
+
return _fail("assignee is required: the token owner's email could not be read")
|
|
109
|
+
body["assigneeEmail"] = email
|
|
110
|
+
|
|
111
|
+
task_id = await client.create_task_ml(body)
|
|
112
|
+
if not task_id:
|
|
113
|
+
details = getattr(client, "last_task_ml_error", None) or {}
|
|
114
|
+
return _fail(_error_text(details) or "ticket create failed", details=details)
|
|
115
|
+
record = await client.get_task_ml(task_id) or {}
|
|
116
|
+
return {"ok": True, "id": str(task_id), "key": str(record.get("jiraIssueKey") or ""),
|
|
117
|
+
"error": ""}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def update_ticket(client, key_or_id: str, fields: dict[str, Any]) -> dict:
|
|
121
|
+
"""PUT /tasks-ml/{id} with only the given fields. Validates before any HTTP call."""
|
|
122
|
+
body = {k: v for k, v in fields.items() if k in EDITABLE_FIELDS and v is not None}
|
|
123
|
+
if not body:
|
|
124
|
+
return _fail("nothing to update: pass at least one field")
|
|
125
|
+
err = validate_fields(body)
|
|
126
|
+
if err:
|
|
127
|
+
return _fail(err)
|
|
128
|
+
task_id = await resolve_id(client, key_or_id)
|
|
129
|
+
if not task_id:
|
|
130
|
+
return _fail(f"ticket not found: {key_or_id}")
|
|
131
|
+
ok = await client.update_task_ml(task_id, body)
|
|
132
|
+
if not ok:
|
|
133
|
+
details = getattr(client, "last_task_ml_error", None) or {}
|
|
134
|
+
return _fail(_error_text(details) or "ticket update failed", details=details)
|
|
135
|
+
return {"ok": True, "id": task_id,
|
|
136
|
+
"key": key_or_id if is_jira_key(key_or_id) else "", "error": ""}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def delete_ticket(client, key_or_id: str) -> dict:
|
|
140
|
+
"""DELETE /tasks-ml/{id}. Irreversible — callers confirm with the person first."""
|
|
141
|
+
task_id = await resolve_id(client, key_or_id)
|
|
142
|
+
if not task_id:
|
|
143
|
+
return _fail(f"ticket not found: {key_or_id}")
|
|
144
|
+
if not await client.delete_task_ml(task_id):
|
|
145
|
+
details = getattr(client, "last_task_ml_error", None) or {}
|
|
146
|
+
error = _error_text(details) or "ticket delete failed"
|
|
147
|
+
if error.lower() == "task not found":
|
|
148
|
+
error = f"ticket not found: {key_or_id}"
|
|
149
|
+
return _fail(error, details=details)
|
|
150
|
+
return {"ok": True, "id": task_id,
|
|
151
|
+
"key": key_or_id if is_jira_key(key_or_id) else "", "error": ""}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def get_ticket(client, key_or_id: str) -> dict | None:
|
|
155
|
+
"""The editable view of one ticket, or None when it can't be found."""
|
|
156
|
+
task_id = await resolve_id(client, key_or_id)
|
|
157
|
+
record = await client.get_task_ml(task_id) if task_id else None
|
|
158
|
+
if not record:
|
|
159
|
+
return None
|
|
160
|
+
out = {k: record.get(k) for k in EDITABLE_FIELDS if k != "assigneeEmail"}
|
|
161
|
+
out.update({"id": record.get("id") or task_id,
|
|
162
|
+
"key": record.get("jiraIssueKey") or record.get("jiraKey") or "",
|
|
163
|
+
"projectKey": record.get("projectKey") or ""})
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _error_text(details: Any) -> str:
|
|
168
|
+
"""One line from the gateway's error body: `{"message": [...]}` / `{"errors": ...}`."""
|
|
169
|
+
if not isinstance(details, dict):
|
|
170
|
+
return str(details or "")
|
|
171
|
+
for key in ("message", "errors", "error", "detail"):
|
|
172
|
+
val = details.get(key)
|
|
173
|
+
if isinstance(val, list) and val:
|
|
174
|
+
return "; ".join(str(v) for v in val)
|
|
175
|
+
if val:
|
|
176
|
+
return str(val)
|
|
177
|
+
return ""
|