devops-bot-sdk 1.6.25__tar.gz → 1.6.26__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.
Files changed (156) hide show
  1. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/PKG-INFO +3 -1
  2. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/devops_bot_sdk.egg-info/PKG-INFO +3 -1
  3. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/devops_bot_sdk.egg-info/SOURCES.txt +3 -0
  4. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/devops_bot_sdk.egg-info/requires.txt +2 -0
  5. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/pyproject.toml +5 -0
  6. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/__init__.py +2 -2
  7. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/activity.py +47 -14
  8. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/cli.py +228 -1
  9. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/client.py +146 -0
  10. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/models/responses.py +73 -0
  11. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/run_auto.py +89 -1
  12. devops_bot_sdk-1.6.26/sdk/self_report.py +597 -0
  13. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/api.py +201 -1
  14. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/models.py +29 -0
  15. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/app.js +194 -1
  16. devops_bot_sdk-1.6.26/sdk/ui/static/report.html +97 -0
  17. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/styles.css +30 -0
  18. devops_bot_sdk-1.6.26/tests/test_self_report.py +644 -0
  19. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/README.md +0 -0
  20. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  21. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  22. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  23. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agentd.py +0 -0
  24. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/__init__.py +0 -0
  25. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/builder.py +0 -0
  26. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/chat.py +0 -0
  27. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/connectors.py +0 -0
  28. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/engines/__init__.py +0 -0
  29. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/engines/claude_code.py +0 -0
  30. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/engines/http.py +0 -0
  31. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/engines/mcp.py +0 -0
  32. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/engines/n8n.py +0 -0
  33. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/engines/shell.py +0 -0
  34. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/integrations.py +0 -0
  35. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/inventory.py +0 -0
  36. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/library/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/manifest.py +0 -0
  38. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/prompt_studio.py +0 -0
  39. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/prompt_template.py +0 -0
  40. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/prompt_vars.py +0 -0
  41. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/run_log.py +0 -0
  42. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/schedules.py +0 -0
  43. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/secrets.py +0 -0
  44. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/ticket_run.py +0 -0
  45. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/usage.py +0 -0
  46. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/agents/varstore.py +0 -0
  47. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/collectors/__init__.py +0 -0
  48. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/collectors/files.py +0 -0
  49. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/collectors/process.py +0 -0
  50. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/collectors/screenshot.py +0 -0
  51. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/config.py +0 -0
  52. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/crucial.py +0 -0
  53. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/departments.py +0 -0
  54. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/design_verify/__init__.py +0 -0
  55. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/design_verify/assets.py +0 -0
  56. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/design_verify/bootstrap.py +0 -0
  57. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/design_verify/browser.py +0 -0
  58. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/design_verify/compare.py +0 -0
  59. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/design_verify/loop.py +0 -0
  60. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/exceptions.py +0 -0
  61. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/formats.py +0 -0
  62. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/git_ops.py +0 -0
  63. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/github_ingest.py +0 -0
  64. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/graph_recon.py +0 -0
  65. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/graphify.py +0 -0
  66. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/hooks/__init__.py +0 -0
  67. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/hooks/crucial_guard.py +0 -0
  68. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/intake.py +0 -0
  69. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ipc/__init__.py +0 -0
  70. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ipc/electron_bridge.py +0 -0
  71. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ipc/handlers.py +0 -0
  72. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/local_exec.py +0 -0
  73. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/models/__init__.py +0 -0
  74. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/models/envelope.py +0 -0
  75. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/models/requests.py +0 -0
  76. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/models/snapshots.py +0 -0
  77. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/phase_timer.py +0 -0
  78. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/plan_run.py +0 -0
  79. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/platform_compat.py +0 -0
  80. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/py.typed +0 -0
  81. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/scheduler.py +0 -0
  82. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/sse.py +0 -0
  83. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/task_file.py +0 -0
  84. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/test.py +0 -0
  85. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/test_pipeline.py +0 -0
  86. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ticket_fields.py +0 -0
  87. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/__init__.py +0 -0
  88. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/dashboard.py +0 -0
  89. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/health.py +0 -0
  90. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/ip_allowlist.py +0 -0
  91. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/jobs.py +0 -0
  92. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/runner.py +0 -0
  93. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/security.py +0 -0
  94. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/server.py +0 -0
  95. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/service.py +0 -0
  96. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/session.py +0 -0
  97. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/setup_manager.py +0 -0
  98. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/agents.html +0 -0
  99. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/chat.html +0 -0
  100. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/custom-agents.html +0 -0
  101. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/default-agents.html +0 -0
  102. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/index.html +0 -0
  103. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/login.html +0 -0
  104. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/runs.html +0 -0
  105. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/schedules.html +0 -0
  106. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/setup.html +0 -0
  107. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/ui/static/tickets.html +0 -0
  108. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/updater.py +0 -0
  109. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/sdk/vps_health.py +0 -0
  110. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/setup.cfg +0 -0
  111. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_activity.py +0 -0
  112. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_activity_tracking.py +0 -0
  113. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_chat_features.py +0 -0
  114. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_listing.py +0 -0
  115. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_prompt_async.py +0 -0
  116. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_prompt_models.py +0 -0
  117. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_run_ticket.py +0 -0
  118. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_schedules.py +0 -0
  119. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agent_workspace_settings.py +0 -0
  120. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_agents_connectors_builder.py +0 -0
  121. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_allowed_tools_config.py +0 -0
  122. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_auto_agent_dispatch.py +0 -0
  123. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_client_jira_projects.py +0 -0
  124. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_config_token_diagnostics.py +0 -0
  125. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_configure_token_owner.py +0 -0
  126. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_console_encoding.py +0 -0
  127. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_departments.py +0 -0
  128. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_epic_decompose_run.py +0 -0
  129. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_epic_inherited_agent_id.py +0 -0
  130. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_epic_session_isolation.py +0 -0
  131. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_formats.py +0 -0
  132. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_git_action.py +0 -0
  133. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_github_ingest.py +0 -0
  134. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_graph_recon.py +0 -0
  135. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_graphify_build.py +0 -0
  136. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_graphify_hook_install.py +0 -0
  137. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_health.py +0 -0
  138. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_intake_plan_review.py +0 -0
  139. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_otp_login.py +0 -0
  140. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_phase_timer.py +0 -0
  141. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_plan_new_command.py +0 -0
  142. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_plan_run_activity.py +0 -0
  143. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_recon_session_isolation.py +0 -0
  144. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_run_auto_agent_only.py +0 -0
  145. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_run_outputs.py +0 -0
  146. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_schedule_api.py +0 -0
  147. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_scheduler.py +0 -0
  148. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_service_token_warning.py +0 -0
  149. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_task_file_and_plan_run.py +0 -0
  150. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_task_file_format_doc.py +0 -0
  151. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_ticket_fields.py +0 -0
  152. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_ticket_run_resolve_by_id.py +0 -0
  153. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_ticket_speed_fixes.py +0 -0
  154. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_update_timer.py +0 -0
  155. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_updater_extras.py +0 -0
  156. {devops_bot_sdk-1.6.25 → devops_bot_sdk-1.6.26}/tests/test_vps_health.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.25
3
+ Version: 1.6.26
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -21,11 +21,13 @@ Provides-Extra: ui
21
21
  Requires-Dist: fastapi>=0.110; extra == "ui"
22
22
  Requires-Dist: uvicorn>=0.29; extra == "ui"
23
23
  Requires-Dist: psutil>=5.9; extra == "ui"
24
+ Requires-Dist: python-multipart>=0.0.9; extra == "ui"
24
25
  Provides-Extra: all
25
26
  Requires-Dist: Pillow>=10.0; extra == "all"
26
27
  Requires-Dist: psutil>=5.9; extra == "all"
27
28
  Requires-Dist: fastapi>=0.110; extra == "all"
28
29
  Requires-Dist: uvicorn>=0.29; extra == "all"
30
+ Requires-Dist: python-multipart>=0.0.9; extra == "all"
29
31
 
30
32
  # DevOpsBot SDK
31
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.25
3
+ Version: 1.6.26
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -21,11 +21,13 @@ Provides-Extra: ui
21
21
  Requires-Dist: fastapi>=0.110; extra == "ui"
22
22
  Requires-Dist: uvicorn>=0.29; extra == "ui"
23
23
  Requires-Dist: psutil>=5.9; extra == "ui"
24
+ Requires-Dist: python-multipart>=0.0.9; extra == "ui"
24
25
  Provides-Extra: all
25
26
  Requires-Dist: Pillow>=10.0; extra == "all"
26
27
  Requires-Dist: psutil>=5.9; extra == "all"
27
28
  Requires-Dist: fastapi>=0.110; extra == "all"
28
29
  Requires-Dist: uvicorn>=0.29; extra == "all"
30
+ Requires-Dist: python-multipart>=0.0.9; extra == "all"
29
31
 
30
32
  # DevOpsBot SDK
31
33
 
@@ -27,6 +27,7 @@ sdk/platform_compat.py
27
27
  sdk/py.typed
28
28
  sdk/run_auto.py
29
29
  sdk/scheduler.py
30
+ sdk/self_report.py
30
31
  sdk/sse.py
31
32
  sdk/task_file.py
32
33
  sdk/test.py
@@ -98,6 +99,7 @@ sdk/ui/static/custom-agents.html
98
99
  sdk/ui/static/default-agents.html
99
100
  sdk/ui/static/index.html
100
101
  sdk/ui/static/login.html
102
+ sdk/ui/static/report.html
101
103
  sdk/ui/static/runs.html
102
104
  sdk/ui/static/schedules.html
103
105
  sdk/ui/static/setup.html
@@ -140,6 +142,7 @@ tests/test_run_auto_agent_only.py
140
142
  tests/test_run_outputs.py
141
143
  tests/test_schedule_api.py
142
144
  tests/test_scheduler.py
145
+ tests/test_self_report.py
143
146
  tests/test_service_token_warning.py
144
147
  tests/test_task_file_and_plan_run.py
145
148
  tests/test_task_file_format_doc.py
@@ -9,6 +9,7 @@ Pillow>=10.0
9
9
  psutil>=5.9
10
10
  fastapi>=0.110
11
11
  uvicorn>=0.29
12
+ python-multipart>=0.0.9
12
13
 
13
14
  [process]
14
15
  psutil>=5.9
@@ -20,3 +21,4 @@ Pillow>=10.0
20
21
  fastapi>=0.110
21
22
  uvicorn>=0.29
22
23
  psutil>=5.9
24
+ python-multipart>=0.0.9
@@ -36,12 +36,17 @@ ui = [
36
36
  # bootstrap installs `[ui]` only, so without it here every
37
37
  # VPS reports "resources.available: False" and the health
38
38
  # cron can only send status + IP.
39
+ "python-multipart>=0.0.9", # POST /api/ui/reports/self — the ONE multipart route in
40
+ # the panel (self-report proofs are file uploads). Starlette
41
+ # raises at request time without it, so it belongs in [ui]
42
+ # alongside fastapi rather than in an optional extra.
39
43
  ]
40
44
  all = [
41
45
  "Pillow>=10.0",
42
46
  "psutil>=5.9",
43
47
  "fastapi>=0.110",
44
48
  "uvicorn>=0.29",
49
+ "python-multipart>=0.0.9",
45
50
  ]
46
51
 
47
52
  [project.scripts]
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.25
3
+ Version: 1.6.26
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.25" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.26" # 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
@@ -124,6 +124,46 @@ def _lines_block(lines: list[str]) -> str:
124
124
  return "\n".join(reversed(out))
125
125
 
126
126
 
127
+ def mirror_to_auto_log_line(text: str, *, pointer: str = "") -> bool:
128
+ """Append ONE already-composed summary line to ``~/.agentos/auto.log``.
129
+
130
+ The append-with-opt-out core of :func:`mirror_to_auto_log`, split out so a
131
+ caller that is not an agent run can use it. `agentos-auto logs [-f]` tails
132
+ that file and nothing else, so anything a person should be able to watch
133
+ live has to land here — including a self-report submission, which has no
134
+ agent, no `agent_dir()` and no `describe()` verb to render.
135
+
136
+ `text` is written verbatim under the shared ` • [<stamp>] ` prefix so every
137
+ line in the file reads the same way. `pointer` adds the indented `↳` line
138
+ the agent path uses for a transcript path; pass an empty string for none.
139
+
140
+ Honours ``AGENTOS_NO_AGENT_LOG_MIRROR=1`` and never raises: a log line must
141
+ never break the work that produced it. Returns True when a line was written.
142
+ """
143
+ line = str(text or "").strip()
144
+ if not line:
145
+ return False
146
+ if os.getenv("AGENTOS_NO_AGENT_LOG_MIRROR"):
147
+ return False
148
+ try:
149
+ from sdk.run_auto import _LOG_FILE
150
+ except Exception: # noqa: BLE001 — never break a caller over a log line
151
+ return False
152
+
153
+ stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
154
+ out = f" • [{stamp}] {line}"
155
+ if pointer:
156
+ out += f"\n ↳ {pointer}"
157
+ try:
158
+ _LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
159
+ with _LOG_FILE.open("a", encoding="utf-8") as fh:
160
+ fh.write(out + "\n")
161
+ return True
162
+ except Exception as exc: # noqa: BLE001
163
+ logger.debug("auto.log mirror failed err=%s", exc)
164
+ return False
165
+
166
+
127
167
  def mirror_to_auto_log(name: str, kind: str, result: dict | None,
128
168
  duration_s: float = 0.0, *, detail: str = "",
129
169
  task_id: str = "") -> bool:
@@ -141,18 +181,20 @@ def mirror_to_auto_log(name: str, kind: str, result: dict | None,
141
181
  the pointer below is how you get from one to the other. Best-effort and
142
182
  silent on failure: a log line must never break a run. Set
143
183
  ``AGENTOS_NO_AGENT_LOG_MIRROR=1`` to turn it off.
184
+
185
+ The append itself lives in :func:`mirror_to_auto_log_line`; what stays here
186
+ is the AGENT-shaped part — the `describe()` verb, the ticket suffix and the
187
+ `agent_dir()` transcript pointer — none of which a non-agent caller wants.
144
188
  """
145
189
  if os.getenv("AGENTOS_NO_AGENT_LOG_MIRROR"):
146
190
  return False
147
191
  try:
148
192
  from sdk.agents.run_log import agent_dir
149
- from sdk.run_auto import _LOG_FILE
150
193
  except Exception: # noqa: BLE001 — never break a run over a log line
151
194
  return False
152
195
 
153
196
  res = result or {}
154
- stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
155
- line = f" • [{stamp}] {describe(name, kind, res, duration_s, detail)}"
197
+ line = describe(name, kind, res, duration_s, detail)
156
198
  # agentd passes the ticket BOTH as `detail` ("task T1") and as `task_id`, so an
157
199
  # unconditional suffix renders "… — task T1 · task T1".
158
200
  if task_id and f"task {task_id}" not in line:
@@ -161,17 +203,8 @@ def mirror_to_auto_log(name: str, kind: str, result: dict | None,
161
203
  where = agent_dir(name)
162
204
  except Exception: # noqa: BLE001
163
205
  where = None
164
- if where:
165
- line += f"\n ↳ transcript: {where}"
166
-
167
- try:
168
- _LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
169
- with _LOG_FILE.open("a", encoding="utf-8") as fh:
170
- fh.write(line + "\n")
171
- return True
172
- except Exception as exc: # noqa: BLE001
173
- logger.debug("auto.log mirror failed agent=%s err=%s", name, exc)
174
- return False
206
+ return mirror_to_auto_log_line(
207
+ line, pointer=f"transcript: {where}" if where else "")
175
208
 
176
209
 
177
210
  async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
@@ -58,6 +58,19 @@ Command surface:
58
58
  agentos integration rm <name>
59
59
  Remove a registered integration.
60
60
 
61
+ agentos report submit [--date YYYY-MM-DD] (--text "…" | --file PATH)
62
+ [--project NAME] [--proof PATH|URL]… [--no-wait]
63
+ Report work that never reached Jira or GitHub, attach proofs (local
64
+ files and/or http(s) URLs), and have the submission SCORED against
65
+ them. Prints the submission id, the score and the verdict. The same
66
+ mechanism the control panel's Report work page uses — see
67
+ sdk/self_report.py.
68
+ agentos report list [--limit N]
69
+ Recent submissions, newest first: date, score, verdict, id.
70
+ agentos report show <ID>
71
+ One submission in full — its text, its proofs and the evaluator's
72
+ per-criterion reasoning.
73
+
61
74
  agentos agentd start [--foreground]
62
75
  Start the local agent daemon (background by default).
63
76
  agentos agentd status
@@ -123,12 +136,16 @@ def main() -> None:
123
136
  _github_command(rest)
124
137
  return
125
138
 
139
+ if noun == "report":
140
+ _report_command(rest)
141
+ return
142
+
126
143
  _print_usage()
127
144
  sys.exit(1)
128
145
 
129
146
 
130
147
  def _print_usage() -> None:
131
- print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github> ...")
148
+ print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report> ...")
132
149
  print()
133
150
  print(" agentos configure [--rotate]")
134
151
  print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
@@ -138,6 +155,7 @@ def _print_usage() -> None:
138
155
  print(" agentos default-agents list [--department-id UUID] # read-only; agents are created in the web app")
139
156
  print(" agentos agentd <start [--foreground]|status>")
140
157
  print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
158
+ print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
141
159
 
142
160
 
143
161
  # ── agent ────────────────────────────────────────────────────────────────
@@ -703,5 +721,214 @@ def _github_command(args: list[str]) -> None:
703
721
  sys.exit(1)
704
722
 
705
723
 
724
+ # ── report ───────────────────────────────────────────────────────────────
725
+
726
+ def _report_usage() -> None:
727
+ print("usage: agentos report submit [--date YYYY-MM-DD] (--text \"…\" | --file PATH)")
728
+ print(" [--project NAME] [--proof PATH|URL]… [--no-wait]")
729
+ print(" agentos report list [--limit N]")
730
+ print(" agentos report show <ID>")
731
+ print()
732
+ print(" Report work that never reached Jira or GitHub, attach proofs, and have the")
733
+ print(" submission scored against them. It then appears in your daily and overall")
734
+ print(" reports and in the reporting chat.")
735
+ print()
736
+ print(" --date the day the work happened (default: today; never the future)")
737
+ print(" --text the report body; --file reads it from a file instead")
738
+ print(" --proof repeatable — a local file path OR an http(s) URL")
739
+ print(" --no-wait submit and exit without waiting for the verdict")
740
+
741
+
742
+ def _report_flag(args: list[str], name: str) -> list[str]:
743
+ """Every ``--name VALUE`` in args, in order. Repeatable flags collect.
744
+
745
+ Same hand-rolled shape as `run_auto._flag_values` — this CLI has no
746
+ argparse subparsers by design (see the module docstring).
747
+ """
748
+ out: list[str] = []
749
+ for i, a in enumerate(args):
750
+ if a == name and i + 1 < len(args):
751
+ out.append(args[i + 1])
752
+ elif a.startswith(name + "="):
753
+ out.append(a.split("=", 1)[1])
754
+ return out
755
+
756
+
757
+ def _report_command(args: list[str]) -> None:
758
+ """``agentos report submit|list|show`` — the CLI half of self-reporting.
759
+
760
+ Every verb resolves the backend client the same way and fails the same way
761
+ when the box has no ``co_`` token: the shared "not configured" message
762
+ ``agentos configure`` prints, and exit 1 — never a traceback.
763
+ """
764
+ from sdk.exceptions import TokenNotConfigured
765
+ from sdk.self_report import SelfReportError
766
+
767
+ verb = args[0] if args else None
768
+ rest = args[1:]
769
+
770
+ try:
771
+ if verb == "submit":
772
+ _report_submit(rest)
773
+ return
774
+ if verb == "list":
775
+ limits = _report_flag(rest, "--limit")
776
+ _report_list(int(limits[0]) if limits and limits[0].isdigit() else 25)
777
+ return
778
+ if verb == "show" and rest and not rest[0].startswith("-"):
779
+ _report_show(rest[0])
780
+ return
781
+ except TokenNotConfigured as exc:
782
+ # The same failure every other verb gives on an unconfigured machine —
783
+ # `sdk.config` composes a cause-specific message; we only add the prefix.
784
+ print(f"error: {exc}")
785
+ sys.exit(1)
786
+ except SelfReportError as exc:
787
+ print(f"error: {exc}")
788
+ sys.exit(1)
789
+
790
+ _report_usage()
791
+ sys.exit(1)
792
+
793
+
794
+ def _report_submit(args: list[str]) -> None:
795
+ """Submit one report, then print the id, the score and the verdict."""
796
+ from sdk import self_report
797
+
798
+ texts = _report_flag(args, "--text")
799
+ files = _report_flag(args, "--file")
800
+ if not texts and not files:
801
+ # A usage line and a non-zero exit — not an empty submission.
802
+ print("error: say what you did — pass --text \"…\" or --file report.md")
803
+ _report_usage()
804
+ sys.exit(1)
805
+
806
+ body = texts[0] if texts else ""
807
+ if files:
808
+ src = Path(files[0]).expanduser()
809
+ if not src.is_file():
810
+ print(f"error: no such file: {src}")
811
+ sys.exit(1)
812
+ try:
813
+ # --file supplies the body; a --text alongside it is a header line.
814
+ file_body = src.read_text(encoding="utf-8", errors="replace").strip()
815
+ except OSError as exc:
816
+ print(f"error: cannot read {src}: {exc}")
817
+ sys.exit(1)
818
+ body = f"{body}\n\n{file_body}".strip() if body else file_body
819
+
820
+ dates = _report_flag(args, "--date")
821
+ projects = _report_flag(args, "--project")
822
+ proofs, rejected = self_report.collect_proofs(_report_flag(args, "--proof"))
823
+ for r in rejected:
824
+ print(f"⚠ proof skipped — {r.get('name', '')}: {r.get('reason', '')}")
825
+
826
+ result = asyncio.run(self_report.submit_and_evaluate(
827
+ date=dates[0] if dates else "",
828
+ text=body,
829
+ project=projects[0] if projects else "",
830
+ proofs=proofs,
831
+ rejected=rejected,
832
+ wait="--no-wait" not in args,
833
+ ))
834
+
835
+ status = str(result.get("status") or "")
836
+ if status == "failed":
837
+ print(f"error: {result.get('error') or 'the submission was not stored.'}")
838
+ sys.exit(1)
839
+
840
+ print(f"submission : {result.get('id') or '(no id returned)'}")
841
+ print(f"date : {result.get('date', '')}"
842
+ + (f" project: {result['project']}" if result.get("project") else ""))
843
+ print(f"proofs : {len(result.get('proofs') or [])} accepted"
844
+ + (f", {len(result.get('rejected') or [])} skipped" if result.get("rejected") else ""))
845
+ if status == "evaluated":
846
+ print(f"score : {result.get('score', 0)}/100")
847
+ print(f"verdict : {result.get('verdict') or 'unknown'}")
848
+ elif status == "evaluation_failed":
849
+ print("score : —")
850
+ print("verdict : evaluation failed — the submission is stored and can be "
851
+ "re-evaluated")
852
+ else:
853
+ print(f"status : {status or 'accepted'} — not evaluated yet; "
854
+ f"`agentos report show {result.get('id', '')}` for the verdict")
855
+ # Exit 0 whenever the submission was STORED. A failed evaluation is a
856
+ # backend-side outcome the person cannot act on, and it is recorded, not lost.
857
+
858
+
859
+ def _report_list(limit: int) -> None:
860
+ """Recent submissions, newest first — date, score, verdict, id."""
861
+ from sdk import self_report
862
+
863
+ rows = asyncio.run(self_report.list_reports(limit))
864
+ if not rows:
865
+ print("no submissions yet — `agentos report submit --text \"…\"` records one.")
866
+ return
867
+ print(f"{'DATE':<12}{'SCORE':<8}{'VERDICT':<26}{'ID':<26}WHAT")
868
+ for r in rows:
869
+ ev = r.get("evaluation") or {}
870
+ score = f"{ev.get('score')}/100" if r.get("status") == "evaluated" else "—"
871
+ verdict = (ev.get("verdict") or r.get("status") or "")[:25]
872
+ what = " ".join(str(r.get("text") or "").split())[:46]
873
+ print(f"{str(r.get('date', '')):<12}{score:<8}{verdict:<26}"
874
+ f"{str(r.get('id', '')):<26}{what}")
875
+
876
+
877
+ def _report_show(report_id: str) -> None:
878
+ """One submission in full — its text, its proofs, and the per-criterion reasoning."""
879
+ from sdk import self_report
880
+
881
+ sub = asyncio.run(self_report.show_report(report_id))
882
+ if not sub:
883
+ print(f"error: no submission {report_id} (or it is not yours).")
884
+ sys.exit(1)
885
+
886
+ ev = sub.get("evaluation") or {}
887
+ print(f"submission : {sub.get('id', report_id)}")
888
+ print(f"date : {sub.get('date', '')}")
889
+ if sub.get("project"):
890
+ print(f"project : {sub['project']}")
891
+ print(f"status : {sub.get('status', '')}")
892
+ if sub.get("status") == "evaluated":
893
+ print(f"score : {ev.get('score', 0)}/100")
894
+ print(f"verdict : {ev.get('verdict', '')}")
895
+ print()
896
+ print("what I did")
897
+ for line in str(sub.get("text") or "").splitlines() or [""]:
898
+ print(f" {line}")
899
+
900
+ proofs = sub.get("proofs") or []
901
+ print()
902
+ print(f"proofs ({len(proofs)})")
903
+ if not proofs:
904
+ print(" (none — an unproven report is recorded, never silently dropped)")
905
+ for p in proofs:
906
+ head = f" · {p.get('name') or p.get('url') or 'proof'}"
907
+ judged = p.get("judgement") or "not judged"
908
+ print(f"{head} [{judged}]")
909
+ if p.get("url"):
910
+ print(f" url : {p['url']}")
911
+ if p.get("size_bytes"):
912
+ print(f" size : {p['size_bytes']} bytes {p.get('content_type', '')}")
913
+ if p.get("reason"):
914
+ print(f" why : {p['reason']}")
915
+
916
+ findings = ev.get("findings") or []
917
+ print()
918
+ print(f"evaluator findings ({len(findings)})")
919
+ if ev.get("summary"):
920
+ print(f" {ev['summary']}")
921
+ for f in findings:
922
+ print(f" · [{f.get('verdict', '')}] {f.get('criterion', '')}")
923
+ if f.get("detail"):
924
+ print(f" {f['detail']}")
925
+ if not findings and sub.get("status") == "evaluation_failed":
926
+ print(" the evaluator returned unparseable output — the raw response is retained "
927
+ "server-side for diagnosis, and the submission can be re-evaluated.")
928
+
929
+ for r in (sub.get("rejected") or []):
930
+ print(f"⚠ proof not accepted — {r.get('name', '')}: {r.get('reason', '')}")
931
+
932
+
706
933
  if __name__ == "__main__":
707
934
  main()
@@ -1544,6 +1544,152 @@ class BackendClient:
1544
1544
  _check_status(resp.status_code, self._base_url)
1545
1545
  return WebhookAck(**resp.json())
1546
1546
 
1547
+ # ── Self-reports (a person reporting their own work, with proofs) ──
1548
+ # The INBOUND half of the reporting feed: work that never reached Jira or
1549
+ # GitHub, submitted by the person who did it and scored against the proofs
1550
+ # they attach. The backend owns the store (`daily_summaries`, channel_type
1551
+ # `self_report`), the blob upload and the evaluator; the SDK only speaks
1552
+ # HTTP to it with the co_ token, exactly like every other method here.
1553
+ #
1554
+ # Best-effort return shapes (dict / None) rather than raised errors, so the
1555
+ # panel and the CLI can present a partial result instead of a traceback —
1556
+ # the same posture `create_task_ml` and `list_jira_projects` take.
1557
+
1558
+ async def submit_self_report(
1559
+ self,
1560
+ *,
1561
+ date: str,
1562
+ text: str,
1563
+ project: str = "",
1564
+ proof_urls: list[str] | None = None,
1565
+ files: list[tuple[str, bytes, str]] | None = None,
1566
+ client_ref: str = "",
1567
+ user_id: str = "",
1568
+ ) -> dict | None:
1569
+ """POST /api/v1/reports/self — submit one self-report. Returns the 202 ack.
1570
+
1571
+ Multipart, following the `update_task_ml` convention (:meth:`update_task_ml`):
1572
+ ``Content-Type`` is dropped from the headers so httpx sets the boundary,
1573
+ scalar fields go as ``(name, (None, value))`` and each proof file as
1574
+ ``(name, (filename, bytes, content_type))``. ``proofUrl[]`` and ``file[]``
1575
+ repeat, which is how a submission carries several proofs.
1576
+
1577
+ ``files`` is a list of ``(filename, data, content_type)`` — the BYTES,
1578
+ read by the caller. The panel reads them inside the request handler on
1579
+ purpose: an ``UploadFile``'s spooled temp file dies with the request,
1580
+ while the evaluation job deliberately outlives it.
1581
+
1582
+ Returns the parsed 202 body (``{id, job_id, status, status_url, …}``)
1583
+ or None on any failure.
1584
+ """
1585
+ url = self._url("/api/v1/reports/self")
1586
+ headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
1587
+ headers["accept"] = "application/json"
1588
+
1589
+ uid = str(user_id or "").strip() or self._user_id_from_token()
1590
+ parts: list[tuple[str, Any]] = [
1591
+ ("date", (None, str(date or ""))),
1592
+ ("text", (None, str(text or ""))),
1593
+ ]
1594
+ if project:
1595
+ parts.append(("project", (None, str(project))))
1596
+ if client_ref:
1597
+ parts.append(("clientRef", (None, str(client_ref))))
1598
+ if uid:
1599
+ parts.append(("userId", (None, uid)))
1600
+ for u in (proof_urls or []):
1601
+ if str(u or "").strip():
1602
+ parts.append(("proofUrl[]", (None, str(u).strip())))
1603
+ for name, data, ctype in (files or []):
1604
+ parts.append(("file[]", (name or "proof", data,
1605
+ ctype or "application/octet-stream")))
1606
+ try:
1607
+ # Generous: the request itself uploads every proof's bytes.
1608
+ async with httpx.AsyncClient(timeout=180.0) as client:
1609
+ resp = await client.post(url, files=parts, headers=headers)
1610
+ if resp.status_code >= 400:
1611
+ logger.warning("client.submit_self_report failed status=%s body=%s",
1612
+ resp.status_code, resp.text[:300])
1613
+ return None
1614
+ body = resp.json() or {}
1615
+ return body if isinstance(body, dict) else None
1616
+ except Exception as exc: # noqa: BLE001
1617
+ logger.warning("client.submit_self_report error err=%s", exc)
1618
+ return None
1619
+
1620
+ async def get_self_report(self, report_id: str) -> dict | None:
1621
+ """GET /api/v1/reports/self/{id} — one submission and its current verdict.
1622
+
1623
+ This is what the poll loop reads while evaluation runs server-side;
1624
+ `status` moves through `accepted` → `evaluating` → `evaluated` |
1625
+ `evaluation_failed`. None on any failure.
1626
+ """
1627
+ rid = str(report_id or "").strip()
1628
+ if not rid:
1629
+ return None
1630
+ url = self._url(f"/api/v1/reports/self/{rid}")
1631
+ try:
1632
+ async with httpx.AsyncClient(timeout=30.0) as client:
1633
+ resp = await client.get(url, headers=self._headers)
1634
+ if resp.status_code >= 400:
1635
+ logger.warning("client.get_self_report failed id=%s status=%s body=%s",
1636
+ rid, resp.status_code, resp.text[:200])
1637
+ return None
1638
+ body = resp.json() or {}
1639
+ return body if isinstance(body, dict) else None
1640
+ except Exception as exc: # noqa: BLE001
1641
+ logger.warning("client.get_self_report error id=%s err=%s", rid, exc)
1642
+ return None
1643
+
1644
+ async def list_self_reports(self, limit: int = 25, user_id: str = "") -> list[dict]:
1645
+ """GET /api/v1/reports/self — this caller's submissions, newest first.
1646
+
1647
+ Returns [] on any failure — the panel renders an empty list rather than
1648
+ an error banner, and the CLI says "no submissions yet".
1649
+ """
1650
+ url = self._url("/api/v1/reports/self")
1651
+ params: dict[str, Any] = {"limit": max(1, min(int(limit or 25), 200))}
1652
+ uid = str(user_id or "").strip() or self._user_id_from_token()
1653
+ if uid:
1654
+ params["userId"] = uid
1655
+ try:
1656
+ async with httpx.AsyncClient(timeout=30.0) as client:
1657
+ resp = await client.get(url, params=params, headers=self._headers)
1658
+ if resp.status_code >= 400:
1659
+ logger.warning("client.list_self_reports failed status=%s body=%s",
1660
+ resp.status_code, resp.text[:200])
1661
+ return []
1662
+ body = resp.json() or {}
1663
+ rows = body.get("reports") if isinstance(body, dict) else body
1664
+ return [r for r in (rows or []) if isinstance(r, dict)]
1665
+ except Exception as exc: # noqa: BLE001
1666
+ logger.warning("client.list_self_reports error err=%s", exc)
1667
+ return []
1668
+
1669
+ async def reevaluate_self_report(self, report_id: str) -> dict | None:
1670
+ """POST /api/v1/reports/self/{id}/evaluate — score an existing submission again.
1671
+
1672
+ The backend REPLACES the stored verdict rather than appending a second
1673
+ one (the previous verdict moves into the submission's audit trail), so
1674
+ this is safe to call repeatedly. None on any failure.
1675
+ """
1676
+ rid = str(report_id or "").strip()
1677
+ if not rid:
1678
+ return None
1679
+ url = self._url(f"/api/v1/reports/self/{rid}/evaluate")
1680
+ try:
1681
+ async with httpx.AsyncClient(timeout=60.0) as client:
1682
+ resp = await client.post(url, json={}, headers=self._headers)
1683
+ if resp.status_code >= 400:
1684
+ logger.warning("client.reevaluate_self_report failed id=%s status=%s body=%s",
1685
+ rid, resp.status_code, resp.text[:300])
1686
+ return None
1687
+ body = resp.json() or {}
1688
+ return body if isinstance(body, dict) else None
1689
+ except Exception as exc: # noqa: BLE001
1690
+ logger.warning("client.reevaluate_self_report error id=%s err=%s", rid, exc)
1691
+ return None
1692
+
1547
1693
  # ── OAuth ─────────────────────────────────────────────────────────
1548
1694
 
1549
1695
  async def oauth_flow_start(self, service: str) -> OAuthStarted: