devops-bot-sdk 1.6.14__tar.gz → 1.6.19__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 (134) hide show
  1. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/SOURCES.txt +14 -0
  4. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/__init__.py +16 -2
  5. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agentd.py +12 -0
  6. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/activity.py +11 -4
  7. devops_bot_sdk-1.6.19/sdk/agents/ticket_run.py +325 -0
  8. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/client.py +327 -34
  9. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/config.py +111 -9
  10. devops_bot_sdk-1.6.19/sdk/departments.py +206 -0
  11. devops_bot_sdk-1.6.19/sdk/formats.py +82 -0
  12. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/github_ingest.py +196 -34
  13. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ipc/handlers.py +492 -91
  14. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/local_exec.py +23 -0
  15. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/run_auto.py +225 -10
  16. devops_bot_sdk-1.6.19/sdk/ticket_fields.py +149 -0
  17. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/service.py +62 -5
  18. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_activity.py +3 -1
  19. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_run_ticket.py +49 -0
  20. devops_bot_sdk-1.6.19/tests/test_auto_agent_dispatch.py +812 -0
  21. devops_bot_sdk-1.6.19/tests/test_client_jira_projects.py +246 -0
  22. devops_bot_sdk-1.6.19/tests/test_config_token_diagnostics.py +88 -0
  23. devops_bot_sdk-1.6.19/tests/test_configure_token_owner.py +59 -0
  24. devops_bot_sdk-1.6.19/tests/test_departments.py +514 -0
  25. devops_bot_sdk-1.6.19/tests/test_epic_inherited_agent_id.py +216 -0
  26. devops_bot_sdk-1.6.19/tests/test_formats.py +209 -0
  27. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_github_ingest.py +99 -11
  28. devops_bot_sdk-1.6.19/tests/test_run_auto_agent_only.py +577 -0
  29. devops_bot_sdk-1.6.19/tests/test_service_token_warning.py +88 -0
  30. devops_bot_sdk-1.6.19/tests/test_ticket_fields.py +402 -0
  31. devops_bot_sdk-1.6.19/tests/test_ticket_run_resolve_by_id.py +508 -0
  32. devops_bot_sdk-1.6.14/sdk/agents/ticket_run.py +0 -129
  33. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/README.md +0 -0
  34. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  35. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  36. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/requires.txt +0 -0
  37. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  38. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/pyproject.toml +0 -0
  39. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/__init__.py +0 -0
  40. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/builder.py +0 -0
  41. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/chat.py +0 -0
  42. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/connectors.py +0 -0
  43. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/__init__.py +0 -0
  44. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/claude_code.py +0 -0
  45. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/http.py +0 -0
  46. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/mcp.py +0 -0
  47. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/n8n.py +0 -0
  48. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/shell.py +0 -0
  49. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/integrations.py +0 -0
  50. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/inventory.py +0 -0
  51. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/library/__init__.py +0 -0
  52. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/manifest.py +0 -0
  53. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/prompt_studio.py +0 -0
  54. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/prompt_template.py +0 -0
  55. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/prompt_vars.py +0 -0
  56. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/run_log.py +0 -0
  57. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/schedules.py +0 -0
  58. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/secrets.py +0 -0
  59. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/usage.py +0 -0
  60. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/varstore.py +0 -0
  61. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/cli.py +0 -0
  62. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/__init__.py +0 -0
  63. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/files.py +0 -0
  64. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/process.py +0 -0
  65. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/screenshot.py +0 -0
  66. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/crucial.py +0 -0
  67. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/__init__.py +0 -0
  68. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/assets.py +0 -0
  69. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/bootstrap.py +0 -0
  70. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/browser.py +0 -0
  71. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/compare.py +0 -0
  72. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/loop.py +0 -0
  73. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/exceptions.py +0 -0
  74. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/git_ops.py +0 -0
  75. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/graphify.py +0 -0
  76. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/hooks/__init__.py +0 -0
  77. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/hooks/crucial_guard.py +0 -0
  78. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ipc/__init__.py +0 -0
  79. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ipc/electron_bridge.py +0 -0
  80. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/__init__.py +0 -0
  81. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/envelope.py +0 -0
  82. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/requests.py +0 -0
  83. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/responses.py +0 -0
  84. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/snapshots.py +0 -0
  85. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/platform_compat.py +0 -0
  86. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/py.typed +0 -0
  87. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/scheduler.py +0 -0
  88. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/sse.py +0 -0
  89. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/test.py +0 -0
  90. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/test_pipeline.py +0 -0
  91. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/__init__.py +0 -0
  92. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/api.py +0 -0
  93. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/dashboard.py +0 -0
  94. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/health.py +0 -0
  95. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/ip_allowlist.py +0 -0
  96. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/jobs.py +0 -0
  97. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/models.py +0 -0
  98. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/runner.py +0 -0
  99. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/security.py +0 -0
  100. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/server.py +0 -0
  101. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/session.py +0 -0
  102. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/setup_manager.py +0 -0
  103. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/agents.html +0 -0
  104. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/app.js +0 -0
  105. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/custom-agents.html +0 -0
  106. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/default-agents.html +0 -0
  107. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/index.html +0 -0
  108. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/login.html +0 -0
  109. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/runs.html +0 -0
  110. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/schedules.html +0 -0
  111. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/setup.html +0 -0
  112. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/styles.css +0 -0
  113. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/tickets.html +0 -0
  114. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/updater.py +0 -0
  115. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/vps_health.py +0 -0
  116. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/setup.cfg +0 -0
  117. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_chat_features.py +0 -0
  118. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_listing.py +0 -0
  119. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_prompt_async.py +0 -0
  120. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_prompt_models.py +0 -0
  121. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_schedules.py +0 -0
  122. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_workspace_settings.py +0 -0
  123. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agents_connectors_builder.py +0 -0
  124. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_allowed_tools_config.py +0 -0
  125. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_console_encoding.py +0 -0
  126. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_git_action.py +0 -0
  127. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_health.py +0 -0
  128. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_otp_login.py +0 -0
  129. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_run_outputs.py +0 -0
  130. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_schedule_api.py +0 -0
  131. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_scheduler.py +0 -0
  132. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_update_timer.py +0 -0
  133. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_updater_extras.py +0 -0
  134. {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/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.14
3
+ Version: 1.6.19
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.14
3
+ Version: 1.6.19
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
@@ -12,7 +12,9 @@ sdk/cli.py
12
12
  sdk/client.py
13
13
  sdk/config.py
14
14
  sdk/crucial.py
15
+ sdk/departments.py
15
16
  sdk/exceptions.py
17
+ sdk/formats.py
16
18
  sdk/git_ops.py
17
19
  sdk/github_ingest.py
18
20
  sdk/graphify.py
@@ -24,6 +26,7 @@ sdk/scheduler.py
24
26
  sdk/sse.py
25
27
  sdk/test.py
26
28
  sdk/test_pipeline.py
29
+ sdk/ticket_fields.py
27
30
  sdk/updater.py
28
31
  sdk/vps_health.py
29
32
  sdk/agents/__init__.py
@@ -104,14 +107,25 @@ tests/test_agent_schedules.py
104
107
  tests/test_agent_workspace_settings.py
105
108
  tests/test_agents_connectors_builder.py
106
109
  tests/test_allowed_tools_config.py
110
+ tests/test_auto_agent_dispatch.py
111
+ tests/test_client_jira_projects.py
112
+ tests/test_config_token_diagnostics.py
113
+ tests/test_configure_token_owner.py
107
114
  tests/test_console_encoding.py
115
+ tests/test_departments.py
116
+ tests/test_epic_inherited_agent_id.py
117
+ tests/test_formats.py
108
118
  tests/test_git_action.py
109
119
  tests/test_github_ingest.py
110
120
  tests/test_health.py
111
121
  tests/test_otp_login.py
122
+ tests/test_run_auto_agent_only.py
112
123
  tests/test_run_outputs.py
113
124
  tests/test_schedule_api.py
114
125
  tests/test_scheduler.py
126
+ tests/test_service_token_warning.py
127
+ tests/test_ticket_fields.py
128
+ tests/test_ticket_run_resolve_by_id.py
115
129
  tests/test_update_timer.py
116
130
  tests/test_updater_extras.py
117
131
  tests/test_vps_health.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.14
3
+ Version: 1.6.19
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -19,6 +19,10 @@ Collectors (payload producers — submit via BackendClient.submit_webhook):
19
19
  collectors.screenshot.build_webhook_payload(token, project)
20
20
  collectors.process.build_webhook_payload(token, project)
21
21
 
22
+ Standard artifact formats (house style for generated Epics/Stories/PRDs):
23
+ formats.EPIC_FORMAT, formats.USER_STORY_FORMAT, formats.PRD_FORMAT
24
+ formats.FORMAT_VERSION — generation stamp for the three above
25
+
22
26
  IPC entry point (Electron main process):
23
27
  python -m sdk.ipc.electron_bridge (stdio newline-delimited JSON)
24
28
 
@@ -30,7 +34,7 @@ Rules:
30
34
  - All data egress through submit_webhook only
31
35
  """
32
36
 
33
- __version__ = "1.6.14" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.19" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
38
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
39
 
36
40
  from sdk.client import BackendClient
@@ -41,6 +45,12 @@ from sdk.exceptions import (
41
45
  CollectorError,
42
46
  TokenNotConfigured,
43
47
  )
48
+ from sdk.formats import (
49
+ EPIC_FORMAT,
50
+ FORMAT_VERSION,
51
+ PRD_FORMAT,
52
+ USER_STORY_FORMAT,
53
+ )
44
54
  from sdk.models.envelope import Envelope
45
55
 
46
56
  __all__ = [
@@ -52,4 +62,8 @@ __all__ = [
52
62
  "BackendVersionTooOld",
53
63
  "CollectorError",
54
64
  "TokenNotConfigured",
65
+ "EPIC_FORMAT",
66
+ "USER_STORY_FORMAT",
67
+ "PRD_FORMAT",
68
+ "FORMAT_VERSION",
55
69
  ]
@@ -346,10 +346,20 @@ async def _dispatch_agent_run(
346
346
  if skill_text:
347
347
  prompt_text = f"{prompt_text}\n\n{skill_text}"
348
348
 
349
+ # When a TICKET triggered this run, the backend must drive ITS ML_TASK_FLOW
350
+ # lifecycle — that flow is a no-op unless `jira_task_id` is set (see
351
+ # sdk/models/requests.py). Without it nothing ever moves the ticket out of
352
+ # "To Do", so the next auto pass re-fetches and re-executes the same agent
353
+ # forever. A scheduled (task=None) run has no ticket and stays session-only,
354
+ # exactly as before. It also makes `_on_status_update` below address the
355
+ # TICKET id rather than the session id on the approval_reply round-trip.
356
+ jira_task_id = str((task or {}).get("id") or "") or None
357
+
349
358
  req = OrchestrateRequest(
350
359
  user_input=prompt_text,
351
360
  local_execution=True,
352
361
  dev_engine="claude_code",
362
+ jira_task_id=jira_task_id,
353
363
  metadata={"custom_agent_id": manifest.name, "custom_agent_name": manifest.name},
354
364
  )
355
365
 
@@ -420,6 +430,7 @@ async def _dispatch_agent_run(
420
430
  manifest.name, _activity_kind, local_result,
421
431
  time.monotonic() - _started, client=client,
422
432
  detail=f"task {task_id}" if task_id else "",
433
+ task_id=task_id or "",
423
434
  )
424
435
  outcome.update({
425
436
  "task_id": task_id,
@@ -435,6 +446,7 @@ async def _dispatch_agent_run(
435
446
  await activity.record_run(
436
447
  manifest.name, _activity_kind, {"ok": False, "error": str(exc)},
437
448
  0.0, client=client, detail=f"task {task_id}" if task_id else "",
449
+ task_id=task_id or "",
438
450
  )
439
451
  return
440
452
 
@@ -63,11 +63,15 @@ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
63
63
 
64
64
 
65
65
  async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
66
- *, client: "BackendClient | None" = None, detail: str = "") -> bool:
67
- """Emit ONE ``{activity, agentId}`` event for a finished agent run.
66
+ *, client: "BackendClient | None" = None, detail: str = "",
67
+ task_id: str = "") -> bool:
68
+ """Emit ONE ``{activity, agentId[, taskId]}`` event for a finished agent run.
68
69
 
69
70
  Returns True when the webhook accepted it, False otherwise (unknown agent,
70
71
  no token, backend down) — never raises.
72
+
73
+ ``task_id`` attributes a ticket-triggered run to its ticket. A scheduled or
74
+ CLI run has no ticket and omits it.
71
75
  """
72
76
  try:
73
77
  if client is None:
@@ -78,6 +82,7 @@ async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.
78
82
  return False
79
83
  return await client.log_agent_activity(
80
84
  describe(name, kind, result, duration_s, detail), agent_id,
85
+ task_id=task_id,
81
86
  )
82
87
  except Exception as exc: # noqa: BLE001 — activity emit must never break a run
83
88
  logger.debug("activity emit failed agent=%s err=%s", name, exc)
@@ -85,7 +90,8 @@ async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.
85
90
 
86
91
 
87
92
  async def record_run(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
88
- *, client: "BackendClient | None" = None, detail: str = "") -> None:
93
+ *, client: "BackendClient | None" = None, detail: str = "",
94
+ task_id: str = "") -> None:
89
95
  """Record a finished run to BOTH sinks: local ledger, then platform feed.
90
96
 
91
97
  The ledger write happens first and independently of the webhook, so a
@@ -107,4 +113,5 @@ async def record_run(name: str, kind: str, result: dict | None, duration_s: floa
107
113
  except Exception as exc: # noqa: BLE001 — ledger write must never break a run
108
114
  logger.debug("activity ledger write failed agent=%s err=%s", name, exc)
109
115
 
110
- await emit(name, kind, res, duration_s, client=client, detail=detail)
116
+ await emit(name, kind, res, duration_s, client=client, detail=detail,
117
+ task_id=task_id)
@@ -0,0 +1,325 @@
1
+ """On-demand "run an agent ON a Jira ticket" — the manual counterpart to the
2
+ ``agentd`` ``ticket_label`` trigger.
3
+
4
+ The daemon fires an agent when a To-Do ticket carries its label; this module
5
+ lets a human pick an agent AND a ticket and run it now (from the UI, the API, or
6
+ the CLI). It reuses the exact same engine — ``agentd._dispatch_agent_run`` — so
7
+ the ticket's fields fill the agent's ``{{variables}}`` and the work is planned,
8
+ gated, executed locally, and reported back to the platform against that ticket.
9
+
10
+ Because a human initiated the run, ``auto_approve=True`` is passed: the GO-AHEAD
11
+ is sent immediately, skipping the agent's ``approval.policy`` WhatsApp gate for
12
+ this single run.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING, Any, NamedTuple
17
+
18
+ if TYPE_CHECKING:
19
+ from sdk.agents.manifest import AgentManifest
20
+ from sdk.client import BackendClient
21
+
22
+ # Process-lifetime cache of backend agentId → (row, source) for the agents this
23
+ # caller may run — the INVERSE of ``_AGENT_ID_CACHE`` in sdk/client.py (name→id),
24
+ # and cached for the same reason: one run pass touches many tickets that share an
25
+ # agentId, and each would otherwise cost a pair of list GETs.
26
+ #
27
+ # ``("", …)`` — an empty ``source`` — is the NEGATIVE entry: "no agent with this id
28
+ # is visible to me". Negative caching is what keeps thirty tickets pointing at a
29
+ # revoked agent from re-fetching both lists thirty times. It is only ever written
30
+ # once absence has actually been ESTABLISHED (both lists read without error) — see
31
+ # ``_load_agent_index``'s ``complete`` flag.
32
+ _AGENT_BY_ID_CACHE: dict[str, tuple[dict[str, Any], str]] = {}
33
+
34
+ # The two caller-scoped sources, in precedence order, with the provenance label
35
+ # each one stamps. These labels are the same strings ``sdk/agents/inventory.py``
36
+ # and ``sdk/ui/api.py`` already use, so a caller can compare them directly.
37
+ #
38
+ # ``list_department_agents`` covers department defaults AND org-wide common agents
39
+ # in ONE response (the backend's ``department.list_for_department`` ORs both
40
+ # filters), which is why there is no third source here.
41
+ _ID_SOURCES: tuple[tuple[str, str, str], ...] = (
42
+ ("your agents", "list_custom_agents", "custom"),
43
+ ("department defaults", "list_department_agents", "default"),
44
+ )
45
+
46
+
47
+ class ResolvedAgent(NamedTuple):
48
+ """A backend agent this caller is entitled to run, and where it came from.
49
+
50
+ A ``NamedTuple`` rather than a field on ``AgentManifest``: the manifest is a
51
+ closed model shared with local YAML agents, which have no backend provenance
52
+ to report. ``source`` is ``"custom"`` or ``"default"``.
53
+ """
54
+
55
+ agent_id: str
56
+ manifest: AgentManifest
57
+ source: str
58
+
59
+
60
+ def _is_deleted(row: dict[str, Any]) -> bool:
61
+ """True when the row is soft-deleted, in any of the spellings a row may use.
62
+
63
+ Defensive: the list endpoints are expected to exclude soft-deleted rows
64
+ server-side, but a manifest resolved here is dispatched with the agent's
65
+ secrets injected as engine env, so the gate is re-asserted locally rather than
66
+ trusted. Fails closed — an unrecognised truthy value counts as deleted.
67
+ """
68
+ return any(row.get(key) for key in ("deleted_at", "deletedAt", "is_deleted", "isDeleted"))
69
+
70
+
71
+ async def _load_agent_index(
72
+ client: BackendClient, reasons: list[str] | None = None,
73
+ ) -> tuple[bool, int]:
74
+ """Warm ``_AGENT_BY_ID_CACHE`` from both caller-scoped lists.
75
+
76
+ Returns ``(complete, visible)`` — whether BOTH sources were read without
77
+ error, and how many rows were indexed. ``complete`` is what lets the caller
78
+ tell "this agent is not mine" from "I could not find out": only the former may
79
+ be negative-cached for the process lifetime.
80
+
81
+ Per-source failures are tolerated and recorded, never raised: a
82
+ department-list outage must not hide a custom hit, and vice versa — the same
83
+ posture ``chat.resolve_agent`` takes. A client missing one of the methods
84
+ entirely (an older client, or a test fake) is skipped, and also counts as
85
+ incomplete.
86
+
87
+ ``_ID_SOURCES`` is walked in precedence order and a row already written in
88
+ THIS pass is not overwritten, so a custom agent wins an id collision with a
89
+ department one. A row written by an EARLIER pass is refreshed, which is also
90
+ how a negative entry is upgraded once the agent becomes visible.
91
+ """
92
+ def _note(reason: str) -> None:
93
+ if reasons is not None:
94
+ reasons.append(reason)
95
+
96
+ complete = True
97
+ seen: set[str] = set()
98
+
99
+ for label, attr, source in _ID_SOURCES:
100
+ fetch = getattr(client, attr, None)
101
+ if fetch is None:
102
+ complete = False
103
+ continue
104
+ try:
105
+ rows = await fetch() or []
106
+ except Exception as exc: # noqa: BLE001 — a fetch failure is not "not mine"
107
+ # str() is EMPTY on some httpx errors (ReadTimeout) — name the class then.
108
+ complete = False
109
+ _note(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
110
+ continue
111
+ for row in rows:
112
+ if not isinstance(row, dict):
113
+ continue
114
+ row_id = str(row.get("id") or "").strip()
115
+ if not row_id or row_id in seen:
116
+ continue
117
+ seen.add(row_id)
118
+ _AGENT_BY_ID_CACHE[row_id] = (row, source)
119
+
120
+ return complete, len(seen)
121
+
122
+
123
+ async def resolve_manifest_by_id(
124
+ client: BackendClient, agent_id: str, reasons: list[str] | None = None,
125
+ ) -> ResolvedAgent | None:
126
+ """Find the runnable manifest for a ticket's ``agentId``, scoped to the caller.
127
+
128
+ The counterpart to :func:`resolve_manifest`, which resolves by NAME. A ticket
129
+ carries an id, so this walks the id instead — but the important difference is
130
+ not the key, it is the SCOPE.
131
+
132
+ **Scope is the authorisation check.** The only agents reachable here are those
133
+ in the union of ``list_custom_agents()`` (the caller's own) and
134
+ ``list_department_agents()`` (their department defaults plus org-wide common
135
+ agents — one response covers both). Membership in that union IS the
136
+ entitlement: no ``relation_id`` comparison, no department-id argument, and
137
+ deliberately no fetch-by-id endpoint. An id that exists platform-side but is
138
+ absent from both responses is simply unresolvable, and nothing further is
139
+ tried. That matters because a resolved manifest carries a system prompt and is
140
+ handed to ``agentd._dispatch_agent_run``, which runs it on THIS machine with
141
+ the agent's resolved secrets injected as engine env — so a broader lookup
142
+ would be a privilege escalation, not a convenience.
143
+
144
+ There is also no local-manifest fallback (``resolve_manifest``'s ``custom``
145
+ scope has one): a hand-written ``~/.agentos/agents`` YAML has no backend id,
146
+ so it is unreachable by id by construction — and honouring a local override
147
+ here would let a local file stand in for an agent the caller may not run.
148
+
149
+ Returns ``None`` — never a manifest — for a blank id, an id in neither list, a
150
+ non-``active`` status, a soft-deleted row, or a row with no usable prompt. Pass
151
+ ``reasons``, a list this appends to, to learn WHICH: exactly as with
152
+ ``resolve_manifest``, "not yours", "your token is not valid so the list could
153
+ not be read", "it is paused" and "it has no prompt" are four different
154
+ problems that all end in ``None``, and they demand different fixes.
155
+
156
+ Results are memoised for the process lifetime, positive AND negative, so N
157
+ tickets sharing one agentId cost one pair of list GETs. The runnability gate
158
+ below runs on EVERY call rather than at cache-fill time, so a cached-but-paused
159
+ agent answers ``None`` every time — from cache, with no extra fetch.
160
+ """
161
+ from sdk.agents.manifest import manifest_from_backend_row
162
+
163
+ def _note(reason: str) -> None:
164
+ if reasons is not None:
165
+ reasons.append(reason)
166
+
167
+ aid = str(agent_id or "").strip()
168
+ if not aid:
169
+ # No network call: a ticket with no agentId is not a lookup failure.
170
+ _note("the ticket carries no agentId, so there is nothing to resolve")
171
+ return None
172
+
173
+ entry = _AGENT_BY_ID_CACHE.get(aid)
174
+ if entry is None:
175
+ complete, visible = await _load_agent_index(client, reasons)
176
+ entry = _AGENT_BY_ID_CACHE.get(aid)
177
+ if entry is None:
178
+ if complete:
179
+ # Absence is ESTABLISHED — safe to remember.
180
+ _AGENT_BY_ID_CACHE[aid] = ({}, "")
181
+ _note(f"no agent with id '{aid}' is in your own agents or your "
182
+ f"department defaults ({visible} visible to you)")
183
+ else:
184
+ # A source went unread, so absence was never established. Do NOT
185
+ # cache it: a three-minute outage must not become a
186
+ # process-lifetime "this agent does not exist". The per-source
187
+ # reasons are already recorded by _load_agent_index.
188
+ _note(f"'{aid}' could not be looked up")
189
+ return None
190
+
191
+ row, source = entry
192
+ if not source:
193
+ _note(f"no agent with id '{aid}' is in your own agents or your department defaults")
194
+ return None
195
+
196
+ name = str(row.get("name") or "").strip() or aid
197
+ if _is_deleted(row):
198
+ _note(f"agent '{name}' (id {aid}) has been deleted")
199
+ return None
200
+
201
+ status = str(row.get("status") or "").strip()
202
+ if status.lower() != "active":
203
+ # Strict, like _AgentRow.is_active and agentd._load_department_agents —
204
+ # NOT scheduler._agent_is_active's lenient "blank means active" variant,
205
+ # which exists to keep schedules alive through a backend hiccup. Nothing
206
+ # is guessed on the path that hands an agent its secrets.
207
+ _note(f"agent '{name}' (id {aid}) is not active (status={status or 'unknown'})")
208
+ return None
209
+
210
+ manifest = manifest_from_backend_row(row)
211
+ if manifest is None:
212
+ _note(f"agent '{name}' (id {aid}) has no usable prompt, so there is "
213
+ f"no instruction to run")
214
+ return None
215
+
216
+ return ResolvedAgent(agent_id=aid, manifest=manifest, source=source)
217
+
218
+
219
+ async def resolve_manifest(
220
+ client: "BackendClient", name: str, scope: str = "custom", department_id: str = "",
221
+ reasons: list[str] | None = None,
222
+ ) -> "AgentManifest | None":
223
+ """Find the agent manifest to run, by ``scope``.
224
+
225
+ * ``department`` — an ACTIVE department default pulled from the backend
226
+ (``list_department_agents``) and materialized read-only (never written back).
227
+ * ``custom`` (default) — a local manifest, then the caller's own backend
228
+ agents, then their department defaults, via ``chat.resolve_agent``.
229
+
230
+ Backend rows are prompt-based (``prompt.system_prompt`` + attached skills);
231
+ the ``manifest`` column this used to read no longer exists on the table, so
232
+ reading it raised ``KeyError`` and made every department agent unrunnable.
233
+
234
+ Returns None when no matching agent is found, so the caller reports it. Pass
235
+ ``reasons`` — a list this appends to — to learn WHY: "not found" and "your
236
+ token is not valid so the agent list could not be read" and "the agent exists
237
+ but has no prompt" are three very different problems that all end in None, and
238
+ a bare "was not found" sends the user hunting for a typo that isn't there.
239
+ """
240
+ from sdk.agents.chat import resolve_agent
241
+ from sdk.agents.manifest import manifest_from_backend_row
242
+
243
+ def _note(reason: str) -> None:
244
+ if reasons is not None:
245
+ reasons.append(reason)
246
+
247
+ name = (name or "").strip()
248
+ if scope == "department":
249
+ try:
250
+ rows = await client.list_department_agents()
251
+ except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
252
+ # str() is EMPTY on some httpx errors (ReadTimeout) — name the class then.
253
+ _note(f"department defaults could not be fetched "
254
+ f"({str(exc) or type(exc).__name__})")
255
+ return None
256
+ for row in rows:
257
+ if row.get("name") == name and row.get("status") == "active":
258
+ manifest = manifest_from_backend_row(row)
259
+ if manifest is None:
260
+ _note(f"'{name}' is a department default but its prompt is empty, "
261
+ f"so there is no instruction to run")
262
+ return manifest
263
+ _note(f"no ACTIVE department default named '{name}' "
264
+ f"({len(rows)} department agent(s) visible to you)")
265
+ return None
266
+
267
+ try:
268
+ return await resolve_agent(name, client)
269
+ except LookupError as exc:
270
+ # resolve_agent names every source it checked and why each one missed —
271
+ # keep that, it is the whole diagnosis.
272
+ _note(str(exc))
273
+ return None
274
+
275
+
276
+ async def run_on_ticket(
277
+ client: "BackendClient", name: str, ticket_key: str = "",
278
+ scope: str = "custom", department_id: str = "",
279
+ variables: dict | None = None, secrets: dict | None = None,
280
+ ) -> dict:
281
+ """Run agent ``name`` (``scope``) now, OPTIONALLY against a Jira ticket.
282
+
283
+ * ``ticket_key`` — optional. When given, the ticket record is fetched (any
284
+ board status — a targeted run may re-run a Done ticket) and its fields fill
285
+ the agent's ``{{variables}}``. When omitted, the agent runs without a ticket.
286
+ * ``variables`` — optional EPHEMERAL ``{{var}}`` values for this run; they win
287
+ over ticket-derived values and are never persisted.
288
+ * ``secrets`` — optional EPHEMERAL env secrets for this run only; injected for
289
+ the engine invocation and never written to the agent's stored keys.
290
+
291
+ Resolves the manifest and dispatches through the shared pipeline with
292
+ ``auto_approve=True``. Returns the dispatch outcome dict
293
+ (``{status, task_id, result, ok, error}``).
294
+
295
+ Raises ``ValueError`` when the agent — or an explicitly-named ticket — cannot
296
+ be found, so the caller reports a clear message. For the agent, that message
297
+ carries the resolver's REASON (unreadable agent list vs. genuinely absent vs.
298
+ present-but-promptless): they demand different fixes and used to be
299
+ indistinguishable behind one "was not found".
300
+ """
301
+ from sdk.agentd import _dispatch_agent_run
302
+
303
+ key = (ticket_key or "").strip()
304
+ task: dict | None = None
305
+ if key:
306
+ records = await client.list_tasks_by_keys([key])
307
+ if not records:
308
+ raise ValueError(f"Jira ticket '{key}' was not found.")
309
+ task = records[0]
310
+
311
+ reasons: list[str] = []
312
+ manifest = await resolve_manifest(
313
+ client, name, scope=scope, department_id=department_id, reasons=reasons,
314
+ )
315
+ if manifest is None:
316
+ detail = " — ".join(reasons)
317
+ raise ValueError(
318
+ f"Agent '{name}' could not be run (scope={scope})"
319
+ + (f" — {detail}." if detail else ".")
320
+ )
321
+
322
+ return await _dispatch_agent_run(
323
+ manifest, client, task, auto_approve=True,
324
+ extra_variables=variables or None, extra_secrets=secrets or None,
325
+ )