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.
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/ipc/handlers.py +113 -10
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/README.md +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.4.
|
|
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.
|
|
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.
|
|
33
|
+
__version__ = "1.4.78"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -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
|
|
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.
|
|
2503
|
-
_QUOTED_PATH_RE = re.compile(r"""["'`]\s*((?:[A-Za-z]
|
|
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)
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.74 → devops_bot_sdk-1.4.78}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|