devops-bot-sdk 1.4.74__tar.gz → 1.4.78__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 (38) hide show
  1. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/ipc/handlers.py +113 -10
  7. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/README.md +0 -0
  8. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/local_exec.py +0 -0
  27. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.74
3
+ Version: 1.4.78
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.4.74
3
+ Version: 1.4.78
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.74"
7
+ version = "1.4.78"
8
8
  description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.74
3
+ Version: 1.4.78
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.74"
33
+ __version__ = "1.4.78"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -58,7 +58,7 @@ def _notify_status(task_id: str, status: str) -> None:
58
58
  pass
59
59
 
60
60
 
61
- SDK_VERSION = "1.4.74"
61
+ SDK_VERSION = "1.4.78"
62
62
  _POLL_INTERVAL = 3.0
63
63
  _POLL_TIMEOUT = 600.0
64
64
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -624,19 +624,17 @@ def _extract_email(text: str | None) -> str | None:
624
624
 
625
625
 
626
626
  def _epic_inherited_fields(task: dict) -> dict:
627
- """Parent-Epic fields a child Task should inherit (drops empties)."""
628
- def _date(v: object) -> str | None:
629
- s = str(v or "")
630
- return s[:10] if s else None # ISO datetime → YYYY-MM-DD
627
+ """Parent-Epic fields a child Task should inherit (drops empties).
631
628
 
629
+ Time-related fields (startDate, dueDate, slaTarget) are intentionally NOT
630
+ inherited/sent — they're optional on the POST and left for the backend/board to
631
+ set, so the create call never carries a stale or wrongly-formatted date.
632
+ """
632
633
  fields = {
633
634
  "projectKey": task.get("projectKey"),
634
635
  "priority": task.get("priority"),
635
636
  "assigneeEmail": (task.get("assignee") or {}).get("email"),
636
637
  "teamId": task.get("teamId"),
637
- "startDate": _date(task.get("startDate")),
638
- "dueDate": _date(task.get("dueDate")),
639
- "slaTarget": task.get("slaTarget"),
640
638
  }
641
639
  sprint = task.get("sprint") or {}
642
640
  if sprint.get("jiraSprintId"):
@@ -2492,15 +2490,19 @@ _PATH_ROOTS = (
2492
2490
  # end-of-line. The root is anchored to a known top-level dir to limit false
2493
2491
  # positives in prose; for an inline path followed by other words on the SAME
2494
2492
  # line, quote it (see _QUOTED_PATH_RE) to avoid grabbing the trailing text.
2495
- _ABS_PATH_RE = re.compile(rf"(?<![\w.])/(?:{_PATH_ROOTS})(?:/[A-Za-z0-9._\- ]+)+")
2493
+ _ABS_PATH_RE = re.compile(rf"(?<![\w.:])/(?:{_PATH_ROOTS})(?:/[A-Za-z0-9._\- ]+)+")
2496
2494
  _HOME_PATH_RE = re.compile(r"(?<![\w.])~(?:/[A-Za-z0-9._\- ]+)+")
2497
2495
  # Windows path. Folder names may contain SPACES (e.g. "BA activities clone"), so a
2498
2496
  # segment is any run of valid path chars (everything except \ / : * ? " < > | and
2499
2497
  # newline) up to the next separator — it must NOT stop at the first space.
2500
2498
  _WIN_PATH_RE = re.compile(r"[A-Za-z]:\\(?:[^\\/:*?\"<>|\r\n]+\\?)+")
2499
+ # Windows path written with FORWARD slashes (e.g. C:/Users/me/new_tasks) — common on
2500
+ # Windows. Unquoted form disallows spaces (quote it if a folder has spaces) so it
2501
+ # stops cleanly at the end of the path instead of swallowing trailing prose.
2502
+ _WIN_FS_PATH_RE = re.compile(r"[A-Za-z]:/(?:[A-Za-z0-9._\-]+/?)+")
2501
2503
  # A QUOTED path (single/double/back quotes) — the reliable way to give a path that
2502
- # contains spaces, on any OS. Captures the full content between the quotes.
2503
- _QUOTED_PATH_RE = re.compile(r"""["'`]\s*((?:[A-Za-z]:\\|~/|/)[^"'`\r\n]+?)\s*["'`]""")
2504
+ # contains spaces, on any OS. Accepts a drive letter with EITHER separator (C:\ or C:/).
2505
+ _QUOTED_PATH_RE = re.compile(r"""["'`]\s*((?:[A-Za-z]:[\\/]|~/|/)[^"'`\r\n]+?)\s*["'`]""")
2504
2506
  _FILE_SEG_RE = re.compile(r"\.[A-Za-z0-9]{1,6}$")
2505
2507
 
2506
2508
 
@@ -2552,6 +2554,9 @@ def _extract_path_from_text(text: str | None) -> str | None:
2552
2554
  p = _clean_path(m.group(0))
2553
2555
  return _dir_of(str(Path.home() / p[2:])) or None
2554
2556
  m = _WIN_PATH_RE.search(text)
2557
+ if m:
2558
+ return _dir_of(_clean_path(m.group(0))) or None
2559
+ m = _WIN_FS_PATH_RE.search(text)
2555
2560
  if m:
2556
2561
  return _dir_of(_clean_path(m.group(0))) or None
2557
2562
  return None
@@ -2581,6 +2586,8 @@ def _extract_all_paths(text: str | None) -> list[str]:
2581
2586
  found.append((m.start(), _dir_of(str(Path.home() / p[2:]))))
2582
2587
  for m in _WIN_PATH_RE.finditer(text):
2583
2588
  found.append((m.start(), _dir_of(_clean_path(m.group(0)))))
2589
+ for m in _WIN_FS_PATH_RE.finditer(text):
2590
+ found.append((m.start(), _dir_of(_clean_path(m.group(0)))))
2584
2591
  found.sort(key=lambda x: x[0])
2585
2592
  out: list[str] = []
2586
2593
  for _, p in found:
@@ -2995,6 +3002,60 @@ async def _poll_fresh_approval(
2995
3002
  return "timeout"
2996
3003
 
2997
3004
 
3005
+ async def _gate_transform_approval(
3006
+ client: BackendClient, tid: str, jira_key: str, summary: str | None, action: str,
3007
+ send: Callable, thread_id: str, should_stop: Callable[[], bool] | None,
3008
+ ) -> str:
3009
+ """Human-approval gate for a non-coding TRANSFORM (Epic decomposition /
3010
+ Story→Task conversion) when agentMode=True. Sends the WhatsApp request and WAITS
3011
+ for the decision. Returns 'approved' | 'rejected' | 'stopped'. On a 15-min timeout
3012
+ the ticket goes to Backlog and we keep polling for a late reply. Does NOT change
3013
+ ticket status itself — the handler owns that (an Epic marks itself In Progress; a
3014
+ Story never does)."""
3015
+ label = jira_key or tid
3016
+ wa_summary = summary or jira_key or "task"
3017
+ await send(Envelope(
3018
+ type=Envelope.TYPE_AWAITING_APPROVAL, thread_id=thread_id,
3019
+ data={"jira_key": jira_key, "task_id": tid,
3020
+ "current_step": f"{label}: awaiting approval to {action}"},
3021
+ ).model_dump())
3022
+
3023
+ async def _send_wa() -> bool:
3024
+ ok = await client.notify_whatsapp(
3025
+ tid, f"Approval needed for {label}: {action} — reply on WhatsApp to approve or reject.",
3026
+ summary=wa_summary,
3027
+ )
3028
+ note = "approval request sent" if ok else "approval request FAILED to send"
3029
+ await send(Envelope(
3030
+ type="step_update", thread_id=thread_id,
3031
+ data={"current_step": f"{label}: {note}"},
3032
+ ).model_dump())
3033
+ try:
3034
+ await client.log_activity(f"Task {tid}: {note} ({action})")
3035
+ except Exception: # noqa: BLE001
3036
+ pass
3037
+ return ok
3038
+
3039
+ await _send_wa()
3040
+ decision = await _poll_approval(
3041
+ client, tid, should_stop,
3042
+ fallback_notify=_send_wa, fallback_after_s=_FIRST_GATE_WA_FALLBACK_S,
3043
+ )
3044
+ if decision == "timeout":
3045
+ await client.move_to_backlog(tid)
3046
+ await send(Envelope(
3047
+ type="step_update", thread_id=thread_id,
3048
+ data={"current_step": f"{label}: no approval in 15m — Backlog; polling for late approval"},
3049
+ ).model_dump())
3050
+ while True:
3051
+ if should_stop is not None and should_stop():
3052
+ return "stopped"
3053
+ late = await client.approval_status(tid)
3054
+ if late in ("approved", "rejected"):
3055
+ return late
3056
+ await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
3057
+ return decision
3058
+
2998
3059
 
2999
3060
  async def handle_orchestrate_auto_local_run(
3000
3061
  client: BackendClient, payload: dict, send: Callable,
@@ -3105,6 +3166,27 @@ async def handle_orchestrate_auto_local_run(
3105
3166
  if not await _validate_ticket_or_reject(client, task, "epic", payload, send, thread_id):
3106
3167
  processed += 1
3107
3168
  continue
3169
+ # agentMode gate: wait for human approval before decomposing.
3170
+ if agent_mode:
3171
+ dec = await _gate_transform_approval(
3172
+ client, tid, jira_key, task.get("summary"),
3173
+ "decompose this Epic into user stories",
3174
+ send, thread_id, should_stop,
3175
+ )
3176
+ if dec == "stopped":
3177
+ await send(Envelope(
3178
+ type=Envelope.TYPE_DONE, thread_id=thread_id,
3179
+ data={"mode": "orchestrate-auto-local", "status": "paused",
3180
+ "processed": processed, "task_count": total},
3181
+ ).model_dump())
3182
+ return
3183
+ if dec != "approved":
3184
+ await send(Envelope(
3185
+ type="step_update", thread_id=thread_id,
3186
+ data={"current_step": f"{jira_key}: {dec} — Epic not decomposed"},
3187
+ ).model_dump())
3188
+ processed += 1
3189
+ continue
3108
3190
  await _handle_epic(client, task, payload, send, thread_id)
3109
3191
  except Exception as exc: # noqa: BLE001
3110
3192
  logger.warning("auto_local.epic_failed task=%s err=%s", tid, exc)
@@ -3127,6 +3209,27 @@ async def handle_orchestrate_auto_local_run(
3127
3209
  ):
3128
3210
  processed += 1
3129
3211
  continue
3212
+ # agentMode gate: wait for human approval before converting Story→Task.
3213
+ if agent_mode:
3214
+ dec = await _gate_transform_approval(
3215
+ client, tid, jira_key, task.get("summary"),
3216
+ "convert this Story into a Task",
3217
+ send, thread_id, should_stop,
3218
+ )
3219
+ if dec == "stopped":
3220
+ await send(Envelope(
3221
+ type=Envelope.TYPE_DONE, thread_id=thread_id,
3222
+ data={"mode": "orchestrate-auto-local", "status": "paused",
3223
+ "processed": processed, "task_count": total},
3224
+ ).model_dump())
3225
+ return
3226
+ if dec != "approved":
3227
+ await send(Envelope(
3228
+ type="step_update", thread_id=thread_id,
3229
+ data={"current_step": f"{jira_key}: {dec} — Story not converted"},
3230
+ ).model_dump())
3231
+ processed += 1
3232
+ continue
3130
3233
  new_desc = await _handle_story(client, task, payload, send, thread_id)
3131
3234
  except Exception as exc: # noqa: BLE001
3132
3235
  logger.warning("auto_local.story_failed task=%s err=%s", tid, exc)