devops-bot-sdk 1.6.24__tar.gz → 1.6.25__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.24 → devops_bot_sdk-1.6.25}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/devops_bot_sdk.egg-info/SOURCES.txt +4 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/client.py +43 -118
- devops_bot_sdk-1.6.25/sdk/github_ingest.py +161 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/intake.py +182 -8
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/plan_run.py +106 -18
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/run_auto.py +105 -14
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/api.py +23 -66
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/security.py +5 -9
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/server.py +14 -74
- devops_bot_sdk-1.6.25/sdk/vps_health.py +111 -0
- devops_bot_sdk-1.6.25/tests/test_intake_plan_review.py +177 -0
- devops_bot_sdk-1.6.25/tests/test_plan_new_command.py +140 -0
- devops_bot_sdk-1.6.25/tests/test_plan_run_activity.py +210 -0
- devops_bot_sdk-1.6.25/tests/test_task_file_format_doc.py +283 -0
- devops_bot_sdk-1.6.24/sdk/github_ingest.py +0 -570
- devops_bot_sdk-1.6.24/sdk/vps_health.py +0 -264
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/README.md +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.24 → devops_bot_sdk-1.6.25}/tests/test_vps_health.py +0 -0
|
@@ -130,8 +130,11 @@ tests/test_graph_recon.py
|
|
|
130
130
|
tests/test_graphify_build.py
|
|
131
131
|
tests/test_graphify_hook_install.py
|
|
132
132
|
tests/test_health.py
|
|
133
|
+
tests/test_intake_plan_review.py
|
|
133
134
|
tests/test_otp_login.py
|
|
134
135
|
tests/test_phase_timer.py
|
|
136
|
+
tests/test_plan_new_command.py
|
|
137
|
+
tests/test_plan_run_activity.py
|
|
135
138
|
tests/test_recon_session_isolation.py
|
|
136
139
|
tests/test_run_auto_agent_only.py
|
|
137
140
|
tests/test_run_outputs.py
|
|
@@ -139,6 +142,7 @@ tests/test_schedule_api.py
|
|
|
139
142
|
tests/test_scheduler.py
|
|
140
143
|
tests/test_service_token_warning.py
|
|
141
144
|
tests/test_task_file_and_plan_run.py
|
|
145
|
+
tests/test_task_file_format_doc.py
|
|
142
146
|
tests/test_ticket_fields.py
|
|
143
147
|
tests/test_ticket_run_resolve_by_id.py
|
|
144
148
|
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.25
|
|
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.25" # 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
|
|
@@ -896,17 +896,11 @@ class BackendClient:
|
|
|
896
896
|
return "pending"
|
|
897
897
|
|
|
898
898
|
async def github_token_record(self) -> dict | None:
|
|
899
|
-
"""GET /ml-api/github/token → the caller's
|
|
899
|
+
"""GET /ml-api/github/token → the full record for the caller's GitHub link.
|
|
900
900
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
ML backend resolves WHICH link to return from the ``co_`` token, so there
|
|
905
|
-
is no list-all form.
|
|
906
|
-
|
|
907
|
-
``github_token()`` below is the accessToken-only view of this. The daily
|
|
908
|
-
PR-ingest cron (``sdk.github_ingest``) needs ``username`` too, which is why
|
|
909
|
-
the full record is exposed. Best-effort — never raises.
|
|
901
|
+
``{id, userId, githubId, username, accessToken, scope, createdAt, updatedAt}``
|
|
902
|
+
or None when the user has no GitHub link, the token is rejected, or the
|
|
903
|
+
backend is unreachable. Best-effort: never raises.
|
|
910
904
|
"""
|
|
911
905
|
from sdk.config import BE_DEFAULT_URL
|
|
912
906
|
|
|
@@ -923,7 +917,7 @@ class BackendClient:
|
|
|
923
917
|
if resp.status_code >= 400:
|
|
924
918
|
return None
|
|
925
919
|
return (resp.json() or {}).get("data") or None
|
|
926
|
-
except Exception:
|
|
920
|
+
except Exception:
|
|
927
921
|
return None
|
|
928
922
|
|
|
929
923
|
async def github_token(self) -> str | None:
|
|
@@ -933,121 +927,52 @@ class BackendClient:
|
|
|
933
927
|
endpoint lives on the ML backend (devopsbot-be), authorised by the same
|
|
934
928
|
co_ token. Best-effort: returns None on any failure so a run without a
|
|
935
929
|
connected GitHub account still proceeds (clone/PR steps just won't auth).
|
|
936
|
-
|
|
937
|
-
Memoized for _GITHUB_TOKEN_TTL_S. The auto-loop asks for this once per
|
|
938
|
-
ticket and again inside epic/story handling, so a long queue made the same
|
|
939
|
-
round-trip dozens of times for a value that does not change between
|
|
940
|
-
tickets. A TTL rather than a permanent cache so a token refreshed on the
|
|
941
|
-
backend is picked up without restarting the daemon; a failed fetch is not
|
|
942
|
-
cached, so a transient outage cannot pin None for the whole window.
|
|
943
930
|
"""
|
|
944
|
-
import time as _time
|
|
945
|
-
|
|
946
|
-
now = _time.monotonic()
|
|
947
|
-
cached = _GITHUB_TOKEN_CACHE.get(self._token)
|
|
948
|
-
if cached and now < cached[1]:
|
|
949
|
-
return cached[0]
|
|
950
931
|
record = await self.github_token_record()
|
|
951
|
-
|
|
952
|
-
if token:
|
|
953
|
-
_GITHUB_TOKEN_CACHE[self._token] = (token, now + _GITHUB_TOKEN_TTL_S)
|
|
954
|
-
return token
|
|
932
|
+
return (record or {}).get("accessToken") or None
|
|
955
933
|
|
|
956
|
-
async def ingest_github_prs(
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
``/api/v1`` call uses), NOT the ML gateway: the backend owns the LLM
|
|
969
|
-
summarise, the downstream daily-summary forward, and the Qdrant upsert.
|
|
970
|
-
The SDK only decides WHEN, because only this machine knows this user's
|
|
971
|
-
schedule and its own last-success watermark.
|
|
972
|
-
|
|
973
|
-
``since`` is an ISO date (``YYYY-MM-DD``) → the backend's
|
|
974
|
-
``GetUserPRsRequest.date_from``. Omitted means "today", which is what a
|
|
975
|
-
never-missed daily run wants; ``sdk.github_ingest`` sends the catch-up
|
|
976
|
-
watermark instead so a machine that was off does not silently lose days.
|
|
977
|
-
|
|
978
|
-
The credentials are sent in the body because that is what the endpoint
|
|
979
|
-
accepts today. The ``Authorization: co_`` header goes with every request
|
|
980
|
-
regardless, so once the backend resolves the GitHub link server-side from
|
|
981
|
-
that token (see docs/GITHUB_SCHEDULING_SDK_MIGRATION.md §3 step 1) it can
|
|
982
|
-
ignore the body fields without breaking this client.
|
|
983
|
-
|
|
984
|
-
Returns the response JSON (``{job_id, …}``) or None on any failure —
|
|
985
|
-
never raises into the cron.
|
|
934
|
+
async def ingest_github_prs(self, since: str | None = None) -> dict:
|
|
935
|
+
"""POST {AI}/api/v1/github/prs — trigger this user's daily PR ingestion.
|
|
936
|
+
|
|
937
|
+
The backend resolves the caller's GitHub link server-side from the co_
|
|
938
|
+
token in ``Authorization`` (see ``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md``);
|
|
939
|
+
``githubUsername``/``githubAccessToken`` below are placeholders kept only
|
|
940
|
+
for backends that predate that resolution and are otherwise ignored.
|
|
941
|
+
``since`` (ISO-8601 date) scopes the fetch — omit for "today".
|
|
942
|
+
|
|
943
|
+
Returns the backend's ``{status, job_id, status_url, ...}`` envelope.
|
|
944
|
+
Raises BackendUnreachable / HTTP errors — the github_ingest cron catches
|
|
945
|
+
and logs these itself rather than treating them as success.
|
|
986
946
|
"""
|
|
987
|
-
|
|
988
|
-
"userId":
|
|
989
|
-
"githubUsername":
|
|
990
|
-
"githubAccessToken":
|
|
991
|
-
"apiTokenHash": self._token,
|
|
947
|
+
body = [{
|
|
948
|
+
"userId": self._user_id_from_token(),
|
|
949
|
+
"githubUsername": "",
|
|
950
|
+
"githubAccessToken": "",
|
|
951
|
+
"apiTokenHash": self._token,
|
|
952
|
+
"date_from": since,
|
|
992
953
|
}]
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
|
|
1000
|
-
)
|
|
1001
|
-
if resp.status_code >= 400:
|
|
1002
|
-
body = resp.text[:500]
|
|
1003
|
-
logger.warning("client.ingest_github_prs failed status=%s body=%s",
|
|
1004
|
-
resp.status_code, body[:200])
|
|
1005
|
-
self.last_ingest_error = {
|
|
1006
|
-
"status": resp.status_code,
|
|
1007
|
-
"body": body,
|
|
1008
|
-
"retryable": is_retryable_backend_error(resp.status_code, body),
|
|
1009
|
-
}
|
|
1010
|
-
return None
|
|
1011
|
-
return resp.json()
|
|
1012
|
-
except Exception as exc: # noqa: BLE001
|
|
1013
|
-
logger.warning("client.ingest_github_prs error err=%s", exc)
|
|
1014
|
-
# A transport failure (timeout, connection reset, DNS blip) is as
|
|
1015
|
-
# retryable as an exhausted pool — the backend may simply be busy.
|
|
1016
|
-
self.last_ingest_error = {"status": 0, "body": str(exc)[:500], "retryable": True}
|
|
1017
|
-
return None
|
|
1018
|
-
|
|
1019
|
-
async def get_task_status(self, task_id: str) -> str | None:
|
|
1020
|
-
"""GET {BE}/tasks-ml/{id} → the ticket's CURRENT board status, or None.
|
|
954
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
955
|
+
resp = await client.post(
|
|
956
|
+
self._url("/api/v1/github/prs"), json=body, headers=self._headers,
|
|
957
|
+
)
|
|
958
|
+
_check_status(resp.status_code, self._url("/api/v1/github/prs"))
|
|
959
|
+
return resp.json()
|
|
1021
960
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
``completed`` is not evidence that the BOARD transitioned — the two are
|
|
1025
|
-
separate systems and the Jira-side transition can lag or fail on its own.
|
|
1026
|
-
Reading the ticket is the only way to tell them apart.
|
|
961
|
+
async def report_vps_health(self, payload: dict) -> dict:
|
|
962
|
+
"""PATCH {AI}/api/v1/vps/health — heartbeat this machine's resource usage.
|
|
1027
963
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
964
|
+
Feeds the backend's ``contabo_allocation`` row (VPS health shown in chat /
|
|
965
|
+
activity reports) — a live GET of the same path answers on-demand checks,
|
|
966
|
+
this PATCH is what keeps that data from going stale. Raises on failure
|
|
967
|
+
(BackendUnreachable / HTTP error); the vps_health cron logs and continues
|
|
968
|
+
rather than crashing the loop.
|
|
1031
969
|
"""
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
resp = await client.get(url, headers=headers)
|
|
1039
|
-
if resp.status_code >= 400:
|
|
1040
|
-
logger.warning("client.get_task_status failed task=%s status=%s",
|
|
1041
|
-
task_id, resp.status_code)
|
|
1042
|
-
return None
|
|
1043
|
-
rec = (resp.json() or {}).get("data") or {}
|
|
1044
|
-
if isinstance(rec, dict):
|
|
1045
|
-
rec = rec.get("record") or rec
|
|
1046
|
-
status = str(rec.get("status") or "").strip()
|
|
1047
|
-
return status or None
|
|
1048
|
-
except Exception as exc: # noqa: BLE001
|
|
1049
|
-
logger.warning("client.get_task_status error task=%s err=%s", task_id, exc)
|
|
1050
|
-
return None
|
|
970
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
971
|
+
resp = await client.request(
|
|
972
|
+
"PATCH", self._url("/api/v1/vps/health"), json=payload, headers=self._headers,
|
|
973
|
+
)
|
|
974
|
+
_check_status(resp.status_code, self._url("/api/v1/vps/health"))
|
|
975
|
+
return resp.json()
|
|
1051
976
|
|
|
1052
977
|
async def move_to_backlog(self, task_id: str) -> bool:
|
|
1053
978
|
"""PUT {BE}/tasks-ml/{id} status=backlog (best-effort).
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Per-machine daily GitHub PR ingestion trigger.
|
|
2
|
+
|
|
3
|
+
See ``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md`` (Option A) in ``devops-bot-ai``:
|
|
4
|
+
the SDK owns the trigger only — one authenticated POST a day to
|
|
5
|
+
``/api/v1/github/prs``; the backend keeps the LLM summarise + SQL + Qdrant work.
|
|
6
|
+
|
|
7
|
+
Slot-based, not interval-based: ticks every 15 min and fires once the UTC clock
|
|
8
|
+
has passed ``AGENTOS_GITHUB_INGEST_AT`` (default ``13:30``) *and* no success is
|
|
9
|
+
recorded for today. A machine that's asleep at the slot catches up on wake
|
|
10
|
+
instead of silently losing that day's run (a central Celery-beat schedule fires
|
|
11
|
+
regardless of machine state; a per-VPS cron does not — see the doc's §6).
|
|
12
|
+
|
|
13
|
+
Catch-up watermark: ``~/.agentos/github_ingest.json`` holds ``last_success_at``.
|
|
14
|
+
The request sends ``since = last_success_at`` (capped by
|
|
15
|
+
``AGENTOS_GITHUB_INGEST_LOOKBACK_DAYS``, default 7), never a bare "today". A
|
|
16
|
+
failed run does NOT advance the watermark, so the window is re-covered next tick.
|
|
17
|
+
|
|
18
|
+
Disable with ``AGENTOS_GITHUB_INGEST=0``.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
from datetime import date, datetime, timedelta, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
_ENABLED_ENV = "AGENTOS_GITHUB_INGEST"
|
|
32
|
+
_AT_ENV = "AGENTOS_GITHUB_INGEST_AT"
|
|
33
|
+
_LOOKBACK_ENV = "AGENTOS_GITHUB_INGEST_LOOKBACK_DAYS"
|
|
34
|
+
_DEFAULT_AT = "13:30" # UTC — the old beat slot (18:30 PKT)
|
|
35
|
+
_DEFAULT_LOOKBACK = 7
|
|
36
|
+
_TICK_SECONDS = 15 * 60 # check the slot every 15 min
|
|
37
|
+
|
|
38
|
+
_STATE_FILE = Path.home() / ".agentos" / "github_ingest.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def enabled() -> bool:
|
|
42
|
+
return os.environ.get(_ENABLED_ENV, "1").strip() not in ("0", "false", "False")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _target_time() -> tuple[int, int]:
|
|
46
|
+
raw = os.environ.get(_AT_ENV, "") or _DEFAULT_AT
|
|
47
|
+
try:
|
|
48
|
+
hh, mm = raw.split(":", 1)
|
|
49
|
+
return int(hh) % 24, int(mm) % 60
|
|
50
|
+
except (ValueError, TypeError):
|
|
51
|
+
return 13, 30
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def lookback_days() -> int:
|
|
55
|
+
try:
|
|
56
|
+
return max(1, int(os.environ.get(_LOOKBACK_ENV, "") or _DEFAULT_LOOKBACK))
|
|
57
|
+
except ValueError:
|
|
58
|
+
return _DEFAULT_LOOKBACK
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_state() -> dict:
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(_STATE_FILE.read_text(encoding="utf-8"))
|
|
64
|
+
except Exception: # noqa: BLE001 — first run, corrupt file, etc.
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _save_state(state: dict) -> None:
|
|
69
|
+
try:
|
|
70
|
+
_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
_STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
|
|
72
|
+
except Exception as exc: # noqa: BLE001 — a failed write just costs one retry
|
|
73
|
+
logger.warning("github_ingest.state_write_failed: %s", exc)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _since(state: dict) -> str:
|
|
77
|
+
"""The ``date_from`` to send — last success, capped, never a bare 'today'."""
|
|
78
|
+
last = state.get("last_success_at")
|
|
79
|
+
floor = date.today() - timedelta(days=lookback_days())
|
|
80
|
+
if not last:
|
|
81
|
+
return floor.isoformat()
|
|
82
|
+
try:
|
|
83
|
+
last_date = datetime.fromisoformat(last).date()
|
|
84
|
+
except ValueError:
|
|
85
|
+
return floor.isoformat()
|
|
86
|
+
return max(last_date, floor).isoformat()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _due_now(state: dict, now: datetime) -> bool:
|
|
90
|
+
last = state.get("last_success_at")
|
|
91
|
+
if last:
|
|
92
|
+
try:
|
|
93
|
+
if datetime.fromisoformat(last).date() >= now.date():
|
|
94
|
+
return False # already succeeded today
|
|
95
|
+
except ValueError:
|
|
96
|
+
pass
|
|
97
|
+
hh, mm = _target_time()
|
|
98
|
+
return (now.hour, now.minute) >= (hh, mm)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def run_once(force: bool = False) -> dict:
|
|
102
|
+
"""Trigger ingestion now (unconditionally if ``force``). Updates the watermark
|
|
103
|
+
on success only. Never raises — the outcome is always returned, never thrown.
|
|
104
|
+
"""
|
|
105
|
+
from sdk.client import BackendClient
|
|
106
|
+
|
|
107
|
+
state = _load_state()
|
|
108
|
+
if not force and not _due_now(state, datetime.now(timezone.utc)):
|
|
109
|
+
return {"ok": True, "skipped": "not_due", **state}
|
|
110
|
+
|
|
111
|
+
since = _since(state)
|
|
112
|
+
try:
|
|
113
|
+
client = BackendClient.from_config()
|
|
114
|
+
result = await client.ingest_github_prs(since=since)
|
|
115
|
+
# BackendClient._check_status only raises on 401/426/5xx — a 4xx business
|
|
116
|
+
# error (no linked GitHub account, validation failure, ...) comes back as
|
|
117
|
+
# a normal JSON body with no exception. Only a genuine accepted-job
|
|
118
|
+
# response advances the watermark; anything else is a failure that must
|
|
119
|
+
# be retried, not silently marked done.
|
|
120
|
+
if str(result.get("status", "")).lower() != "accepted" or not result.get("job_id"):
|
|
121
|
+
raise RuntimeError(result.get("detail") or f"unexpected response: {result}")
|
|
122
|
+
state["last_success_at"] = datetime.now(timezone.utc).date().isoformat()
|
|
123
|
+
state["last_outcome"] = {"ok": True, "since": since, "result": result}
|
|
124
|
+
_save_state(state)
|
|
125
|
+
logger.info("github_ingest.triggered since=%s", since)
|
|
126
|
+
return state["last_outcome"]
|
|
127
|
+
except Exception as exc: # noqa: BLE001 — failure must not advance the watermark
|
|
128
|
+
outcome = {"ok": False, "since": since, "error": str(exc)}
|
|
129
|
+
state["last_outcome"] = outcome
|
|
130
|
+
_save_state(state)
|
|
131
|
+
logger.warning("github_ingest.failed: %s", exc)
|
|
132
|
+
return outcome
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def status() -> dict:
|
|
136
|
+
"""Config + watermark + last outcome — the ``GET /api/ui/github-ingest`` body."""
|
|
137
|
+
hh, mm = _target_time()
|
|
138
|
+
state = _load_state()
|
|
139
|
+
return {
|
|
140
|
+
"enabled": enabled(),
|
|
141
|
+
"at_utc": f"{hh:02d}:{mm:02d}",
|
|
142
|
+
"lookback_days": lookback_days(),
|
|
143
|
+
"last_success_at": state.get("last_success_at"),
|
|
144
|
+
"last_outcome": state.get("last_outcome"),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def run_forever() -> None:
|
|
149
|
+
while True:
|
|
150
|
+
try:
|
|
151
|
+
await run_once()
|
|
152
|
+
except Exception as exc: # noqa: BLE001 — the tick loop must never die
|
|
153
|
+
logger.warning("github_ingest.tick_failed: %s", exc)
|
|
154
|
+
await asyncio.sleep(_TICK_SECONDS)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def start() -> "asyncio.Task | None":
|
|
158
|
+
if not enabled():
|
|
159
|
+
logger.info("github_ingest.disabled")
|
|
160
|
+
return None
|
|
161
|
+
return asyncio.create_task(run_forever(), name="github-ingest-cron")
|
|
@@ -115,8 +115,124 @@ async def finalize(conversation_id: str, *, requirements: dict[str, Any],
|
|
|
115
115
|
)
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
async def build_plan(conversation_id: str, *, requirements: dict[str, Any],
|
|
119
|
+
history: list[dict[str, Any]] | None = None,
|
|
120
|
+
plan: dict[str, Any] | None = None,
|
|
121
|
+
feedback: str | None = None) -> dict[str, Any]:
|
|
122
|
+
"""Draft — or revise — the plan WITHOUT creating anything.
|
|
123
|
+
|
|
124
|
+
The step `finalize` skips. `finalize` builds a plan and commits it in one
|
|
125
|
+
call, which is right for "just make the tickets" but leaves no way to read
|
|
126
|
+
the plan first; the control panel has always had the review step and the CLI
|
|
127
|
+
had not. Pass BOTH `plan` and `feedback` to revise: the revise prompt requires
|
|
128
|
+
that anything the feedback does not mention comes back unchanged.
|
|
129
|
+
"""
|
|
130
|
+
payload: dict[str, Any] = {
|
|
131
|
+
"conversation_id": conversation_id,
|
|
132
|
+
"requirements": requirements,
|
|
133
|
+
"history": history or [],
|
|
134
|
+
}
|
|
135
|
+
if plan is not None:
|
|
136
|
+
payload["plan"] = plan
|
|
137
|
+
if feedback:
|
|
138
|
+
payload["feedback"] = feedback
|
|
139
|
+
return await _post(
|
|
140
|
+
f"/api/v1/tickets/intake/{conversation_id}/plan",
|
|
141
|
+
payload, _FINALIZE_TIMEOUT_S,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def create(conversation_id: str, *, plan: dict[str, Any],
|
|
146
|
+
product_id: str, requirements: dict[str, Any] | None = None,
|
|
147
|
+
pv_id: str | None = None,
|
|
148
|
+
project: dict[str, Any] | None = None,
|
|
149
|
+
assignee_email: str | None = None,
|
|
150
|
+
autorun: bool = True) -> dict[str, Any]:
|
|
151
|
+
"""Commit a REVIEWED plan — the plan is sent back rather than rebuilt.
|
|
152
|
+
|
|
153
|
+
That is deliberate and matches the panel: what a person approved on screen is
|
|
154
|
+
exactly what gets created, edits included. Rebuilding it here would quietly
|
|
155
|
+
create something the reviewer never saw.
|
|
156
|
+
"""
|
|
157
|
+
payload: dict[str, Any] = {
|
|
158
|
+
"conversation_id": conversation_id,
|
|
159
|
+
"plan": plan,
|
|
160
|
+
"requirements": requirements or {},
|
|
161
|
+
"product_id": product_id,
|
|
162
|
+
"autorun": autorun,
|
|
163
|
+
}
|
|
164
|
+
if pv_id:
|
|
165
|
+
payload["pv_id"] = pv_id
|
|
166
|
+
if project:
|
|
167
|
+
payload["project"] = project
|
|
168
|
+
if assignee_email:
|
|
169
|
+
payload["assignee_email"] = assignee_email
|
|
170
|
+
return await _post(
|
|
171
|
+
f"/api/v1/tickets/intake/{conversation_id}/create",
|
|
172
|
+
payload, _FINALIZE_TIMEOUT_S,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
118
176
|
# ── Terminal loop ─────────────────────────────────────────────────────────
|
|
119
177
|
|
|
178
|
+
def render_plan(plan: dict[str, Any]) -> str:
|
|
179
|
+
"""The plan as text, laid out the way the User Story format reads.
|
|
180
|
+
|
|
181
|
+
What the plan step returns is the SHAPE of the work — per epic an overview,
|
|
182
|
+
scope in/out, dependencies and acceptance criteria; per story a title, a
|
|
183
|
+
summary and points. The full four-section `## User Story` body is written at
|
|
184
|
+
CREATE time by the backend's generator (which injects the same
|
|
185
|
+
`USER_STORY_FORMAT` this SDK ships). So this renders the review copy under the
|
|
186
|
+
same headings, and says where the rest arrives — rather than printing a raw
|
|
187
|
+
dict and calling it a user story.
|
|
188
|
+
"""
|
|
189
|
+
out: list[str] = []
|
|
190
|
+
title = str(plan.get("title") or "(untitled plan)")
|
|
191
|
+
out.append(f"# {title}")
|
|
192
|
+
if plan.get("summary"):
|
|
193
|
+
out.append(f"\n{plan['summary']}")
|
|
194
|
+
|
|
195
|
+
for dept in plan.get("departments") or []:
|
|
196
|
+
out.append(f"\n\n── Department: {dept.get('tag') or '—'} ──")
|
|
197
|
+
if dept.get("objective"):
|
|
198
|
+
out.append(f"Objective: {dept['objective']}")
|
|
199
|
+
for i, epic in enumerate(dept.get("epics") or [], 1):
|
|
200
|
+
out.append(f"\nEPIC {i}: {epic.get('title') or '—'}")
|
|
201
|
+
if epic.get("overview"):
|
|
202
|
+
out.append(f" {epic['overview']}")
|
|
203
|
+
for label, key in (("Scope IN", "scope_in"), ("Scope OUT", "scope_out"),
|
|
204
|
+
("Dependencies", "dependencies")):
|
|
205
|
+
vals = epic.get(key) or []
|
|
206
|
+
if vals:
|
|
207
|
+
out.append(f" {label}: " + "; ".join(str(v) for v in vals))
|
|
208
|
+
crit = epic.get("acceptance_criteria") or []
|
|
209
|
+
if crit:
|
|
210
|
+
out.append(" ## Acceptance Criteria")
|
|
211
|
+
out.extend(f" - {c}" for c in crit)
|
|
212
|
+
stories = epic.get("stories") or []
|
|
213
|
+
out.append(f" ## User Stories ({len(stories)})")
|
|
214
|
+
for j, st in enumerate(stories, 1):
|
|
215
|
+
pts = st.get("points")
|
|
216
|
+
out.append(f" {i}.{j} {st.get('title') or st.get('summary') or '—'}"
|
|
217
|
+
+ (f" [{pts} pts]" if pts else ""))
|
|
218
|
+
if st.get("summary") and st.get("title"):
|
|
219
|
+
out.append(f" {st['summary']}")
|
|
220
|
+
out.append("\n\nEach story's full `## User Story / ## Acceptance Criteria / "
|
|
221
|
+
"## Notes & Constraints / ## Definition of Done` body is written when "
|
|
222
|
+
"the tickets are created.")
|
|
223
|
+
return "\n".join(out)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _plan_counts(plan: dict[str, Any]) -> tuple[int, int]:
|
|
227
|
+
"""(epics, stories) across every department in a plan."""
|
|
228
|
+
epics = stories = 0
|
|
229
|
+
for dept in plan.get("departments") or []:
|
|
230
|
+
eps = dept.get("epics") or []
|
|
231
|
+
epics += len(eps)
|
|
232
|
+
stories += sum(len(e.get("stories") or []) for e in eps)
|
|
233
|
+
return epics, stories
|
|
234
|
+
|
|
235
|
+
|
|
120
236
|
def _print_status(requirements: dict[str, Any], missing: list[str]) -> None:
|
|
121
237
|
"""Show what is known and what is still needed.
|
|
122
238
|
|
|
@@ -156,10 +272,16 @@ def chat_cli(argv: list[str]) -> None:
|
|
|
156
272
|
autorun = "--no-autorun" not in argv
|
|
157
273
|
|
|
158
274
|
print("agentos chat — describe the work you want done.")
|
|
159
|
-
print(" Answer the questions
|
|
275
|
+
print(" Answer the questions, then:")
|
|
276
|
+
print(" plan build the plan and read it (creates nothing)")
|
|
277
|
+
print(" revise <change> adjust the plan")
|
|
278
|
+
print(" show re-print the current plan")
|
|
279
|
+
print(" create / done create the tickets")
|
|
280
|
+
print(" quit abandon; nothing is created\n")
|
|
160
281
|
|
|
161
282
|
history: list[dict[str, Any]] = []
|
|
162
283
|
requirements: dict[str, Any] = {}
|
|
284
|
+
plan: dict[str, Any] | None = None
|
|
163
285
|
conversation_id: str | None = None
|
|
164
286
|
index = 0
|
|
165
287
|
|
|
@@ -177,7 +299,46 @@ def chat_cli(argv: list[str]) -> None:
|
|
|
177
299
|
print("abandoned — nothing was created.")
|
|
178
300
|
return
|
|
179
301
|
|
|
180
|
-
|
|
302
|
+
# ── plan / revise / show: read the plan BEFORE anything is created ──
|
|
303
|
+
verb, _, rest = message.partition(" ")
|
|
304
|
+
if verb.lower() in ("plan", "revise", "show"):
|
|
305
|
+
if verb.lower() == "show":
|
|
306
|
+
if not plan:
|
|
307
|
+
print(" no plan yet — type 'plan' to build one.")
|
|
308
|
+
else:
|
|
309
|
+
print("\n" + render_plan(plan) + "\n")
|
|
310
|
+
continue
|
|
311
|
+
missing = [k for k, v in requirements.items() if not str(v or "").strip()]
|
|
312
|
+
if not requirements or missing:
|
|
313
|
+
print(f" not yet — still needed: "
|
|
314
|
+
f"{', '.join(missing) or 'everything'}")
|
|
315
|
+
continue
|
|
316
|
+
if verb.lower() == "revise" and not plan:
|
|
317
|
+
print(" nothing to revise yet — type 'plan' first.")
|
|
318
|
+
continue
|
|
319
|
+
if verb.lower() == "revise" and not rest.strip():
|
|
320
|
+
print(' say what to change, e.g. revise split the first epic in two')
|
|
321
|
+
continue
|
|
322
|
+
print("\n building the plan … (nothing is created by this)")
|
|
323
|
+
try:
|
|
324
|
+
res = await build_plan(
|
|
325
|
+
conversation_id or "", requirements=requirements,
|
|
326
|
+
history=history,
|
|
327
|
+
plan=plan if verb.lower() == "revise" else None,
|
|
328
|
+
feedback=rest.strip() if verb.lower() == "revise" else None,
|
|
329
|
+
)
|
|
330
|
+
except IntakeError as exc:
|
|
331
|
+
print(f" ✗ {exc}")
|
|
332
|
+
continue
|
|
333
|
+
plan = res.get("plan") or plan
|
|
334
|
+
epics, stories = _plan_counts(plan or {})
|
|
335
|
+
print("\n" + render_plan(plan or {}))
|
|
336
|
+
print(f"\n {epics} epic(s), {stories} story(ies) — nothing created yet.")
|
|
337
|
+
print(" 'revise <what to change>' to adjust · 'create' to make the "
|
|
338
|
+
"tickets · 'quit' to abandon.\n")
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
if message.lower() in ("done", "create"):
|
|
181
342
|
missing = [k for k, v in requirements.items() if not str(v or "").strip()]
|
|
182
343
|
if not requirements or missing:
|
|
183
344
|
print(f" not yet — still needed: "
|
|
@@ -187,13 +348,26 @@ def chat_cli(argv: list[str]) -> None:
|
|
|
187
348
|
print(" a product id is required to create tickets. Re-run with "
|
|
188
349
|
"`agentos chat --product-id <ID>`.")
|
|
189
350
|
return
|
|
190
|
-
print("\n building the plan and creating tickets … (this takes a minute)")
|
|
191
351
|
try:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
352
|
+
if plan:
|
|
353
|
+
# A reviewed plan is committed AS REVIEWED — sent back
|
|
354
|
+
# rather than rebuilt, so what was on screen is what gets
|
|
355
|
+
# created. Rebuilding here would create something the
|
|
356
|
+
# reviewer never saw.
|
|
357
|
+
print("\n creating tickets from the reviewed plan …")
|
|
358
|
+
result = await create(
|
|
359
|
+
conversation_id or "", plan=plan, product_id=product_id,
|
|
360
|
+
requirements=requirements, pv_id=pv_id, autorun=autorun,
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
print("\n building the plan and creating tickets … "
|
|
364
|
+
"(this takes a minute; type 'plan' first if you want "
|
|
365
|
+
"to read it before it is committed)")
|
|
366
|
+
result = await finalize(
|
|
367
|
+
conversation_id or "", requirements=requirements,
|
|
368
|
+
product_id=product_id, history=history, pv_id=pv_id,
|
|
369
|
+
autorun=autorun,
|
|
370
|
+
)
|
|
197
371
|
except IntakeError as exc:
|
|
198
372
|
print(f" ✗ {exc}")
|
|
199
373
|
return
|