devops-bot-sdk 1.4.71__tar.gz → 1.4.76__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.71 → devops_bot_sdk-1.4.76}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/ipc/handlers.py +166 -20
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/local_exec.py +6 -4
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/README.md +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/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.76"
|
|
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.76
|
|
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.76"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -415,6 +415,56 @@ async def _run_with_heartbeat(coro, *, send: Callable, thread_id: str, label: st
|
|
|
415
415
|
pass
|
|
416
416
|
|
|
417
417
|
|
|
418
|
+
async def _run_generation(
|
|
419
|
+
prompt: str, project_path: str, *, send: Callable, thread_id: str, label: str,
|
|
420
|
+
github_token: str | None = None, resume_session_id: str | None = None,
|
|
421
|
+
name: str | None = None,
|
|
422
|
+
):
|
|
423
|
+
"""Run a read-only generation (epic decomposition / PRD) with a heartbeat AND
|
|
424
|
+
output-token-cap chunking: if a single response is cut off at the per-response
|
|
425
|
+
output cap, resume the session and continue — concatenating the pieces — so a
|
|
426
|
+
large result (a big JSON / PRD) completes across chunks instead of truncating.
|
|
427
|
+
"""
|
|
428
|
+
from sdk import local_exec
|
|
429
|
+
|
|
430
|
+
cap = local_exec._MAX_OUTPUT_TOKENS
|
|
431
|
+
res = await _run_with_heartbeat(
|
|
432
|
+
local_exec.run_claude_local(
|
|
433
|
+
prompt, project_path, allowed_tools=_RECON_ALLOWED_TOOLS,
|
|
434
|
+
github_token=github_token, resume_session_id=resume_session_id, max_output_tokens=cap,
|
|
435
|
+
),
|
|
436
|
+
send=send, thread_id=thread_id, label=label, name=name,
|
|
437
|
+
)
|
|
438
|
+
combined = res.get("result") or ""
|
|
439
|
+
sid = res.get("session_id")
|
|
440
|
+
chunks = 0
|
|
441
|
+
while res.get("output_cap_exceeded") and sid and chunks < _MAX_OUTPUT_CAP_RESUMES:
|
|
442
|
+
chunks += 1
|
|
443
|
+
cap = min(cap * 2, 128000)
|
|
444
|
+
await send(Envelope(
|
|
445
|
+
type="step_update", thread_id=thread_id,
|
|
446
|
+
data={"status": "running",
|
|
447
|
+
"current_step": (f"[{name}] " if name else "") + f"{label}: response hit the "
|
|
448
|
+
f"output-token cap — continuing in chunks (part {chunks + 1})"},
|
|
449
|
+
).model_dump())
|
|
450
|
+
res = await _run_with_heartbeat(
|
|
451
|
+
local_exec.run_claude_local(
|
|
452
|
+
"Your previous response was cut off at the output-token limit. Continue "
|
|
453
|
+
"EXACTLY where you left off — output ONLY the remaining content, with no "
|
|
454
|
+
"repetition and no preamble, so the two pieces concatenate seamlessly.",
|
|
455
|
+
project_path, allowed_tools=_RECON_ALLOWED_TOOLS,
|
|
456
|
+
github_token=github_token, resume_session_id=sid, max_output_tokens=cap,
|
|
457
|
+
),
|
|
458
|
+
send=send, thread_id=thread_id, label=f"{label} (cont.)", name=name,
|
|
459
|
+
)
|
|
460
|
+
combined += res.get("result") or ""
|
|
461
|
+
sid = res.get("session_id") or sid
|
|
462
|
+
out = dict(res)
|
|
463
|
+
out["result"] = combined # the full, reassembled output
|
|
464
|
+
out["session_id"] = sid
|
|
465
|
+
return out
|
|
466
|
+
|
|
467
|
+
|
|
418
468
|
# ── Epic decomposition ────────────────────────────────────────────────────
|
|
419
469
|
# An EPIC is not coded directly. It is analysed (grounded in the codebase when a
|
|
420
470
|
# project path exists) into a detailed user-story description + an implementation
|
|
@@ -574,19 +624,17 @@ def _extract_email(text: str | None) -> str | None:
|
|
|
574
624
|
|
|
575
625
|
|
|
576
626
|
def _epic_inherited_fields(task: dict) -> dict:
|
|
577
|
-
"""Parent-Epic fields a child Task should inherit (drops empties).
|
|
578
|
-
def _date(v: object) -> str | None:
|
|
579
|
-
s = str(v or "")
|
|
580
|
-
return s[:10] if s else None # ISO datetime → YYYY-MM-DD
|
|
627
|
+
"""Parent-Epic fields a child Task should inherit (drops empties).
|
|
581
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
|
+
"""
|
|
582
633
|
fields = {
|
|
583
634
|
"projectKey": task.get("projectKey"),
|
|
584
635
|
"priority": task.get("priority"),
|
|
585
636
|
"assigneeEmail": (task.get("assignee") or {}).get("email"),
|
|
586
637
|
"teamId": task.get("teamId"),
|
|
587
|
-
"startDate": _date(task.get("startDate")),
|
|
588
|
-
"dueDate": _date(task.get("dueDate")),
|
|
589
|
-
"slaTarget": task.get("slaTarget"),
|
|
590
638
|
}
|
|
591
639
|
sprint = task.get("sprint") or {}
|
|
592
640
|
if sprint.get("jiraSprintId"):
|
|
@@ -801,9 +849,10 @@ async def _handle_epic(
|
|
|
801
849
|
await graphify.prepare(project_path)
|
|
802
850
|
except Exception: # noqa: BLE001
|
|
803
851
|
pass
|
|
804
|
-
gen = await
|
|
852
|
+
gen = await _run_generation(
|
|
805
853
|
_build_epic_prompt(jira_key, summary, description, human),
|
|
806
|
-
project_path,
|
|
854
|
+
project_path, send=send, thread_id=thread_id,
|
|
855
|
+
label=f"Epic {jira_key}: generating user stories",
|
|
807
856
|
github_token=await client.github_token(),
|
|
808
857
|
)
|
|
809
858
|
# A usage-limit hit must stop and retry later. For ANY other outcome, still try
|
|
@@ -1052,12 +1101,15 @@ async def _handle_story(
|
|
|
1052
1101
|
await graphify.prepare(project_path)
|
|
1053
1102
|
except Exception: # noqa: BLE001
|
|
1054
1103
|
pass
|
|
1055
|
-
gen = await
|
|
1104
|
+
gen = await _run_generation(
|
|
1056
1105
|
_build_prd_prompt(jira_key, summary, user_story, human),
|
|
1057
|
-
project_path,
|
|
1106
|
+
project_path, send=send, thread_id=thread_id,
|
|
1107
|
+
label=f"Story {jira_key}: generating PRD",
|
|
1058
1108
|
github_token=await client.github_token(),
|
|
1059
1109
|
)
|
|
1060
|
-
|
|
1110
|
+
# Use the output regardless of an is_error flag (a stray read-only tool denial
|
|
1111
|
+
# shouldn't discard a PRD the model actually produced); only a usage limit is fatal.
|
|
1112
|
+
prd = "" if gen.get("limit_exceeded") else (gen.get("result") or "").strip()
|
|
1061
1113
|
if prd:
|
|
1062
1114
|
# PRD lives alongside the user story in the (now Task) description.
|
|
1063
1115
|
new_description = f"{user_story}\n\n---\n\n{_PRD_MARKER}\n\n{prd}"[:20000]
|
|
@@ -1488,16 +1540,14 @@ async def _code_one_repo(
|
|
|
1488
1540
|
"code you found. Output ONLY the PRD as Markdown, following this format "
|
|
1489
1541
|
"exactly:\n\n" + _PRD_FORMAT
|
|
1490
1542
|
)
|
|
1491
|
-
prd_res = await
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
github_token=gh_token, resume_session_id=recon_session,
|
|
1496
|
-
),
|
|
1497
|
-
send=send, thread_id=thread_id, label="Working on the PRD", name=name,
|
|
1543
|
+
prd_res = await _run_generation(
|
|
1544
|
+
instruction, repo_path, send=send, thread_id=thread_id,
|
|
1545
|
+
label="Working on the PRD", name=name,
|
|
1546
|
+
github_token=gh_token, resume_session_id=recon_session, # continue the recon session
|
|
1498
1547
|
)
|
|
1499
1548
|
recon_session = prd_res.get("session_id") or recon_session
|
|
1500
|
-
|
|
1549
|
+
# Use the PRD regardless of an is_error flag; only skip on a real usage limit.
|
|
1550
|
+
prd = "" if prd_res.get("limit_exceeded") else (prd_res.get("result") or "").strip()
|
|
1501
1551
|
if prd:
|
|
1502
1552
|
enriched = f"{before}{_PRD_MARKER}\n\n{prd}"[:20000]
|
|
1503
1553
|
pending["user_input"] = enriched # shared across repos → reused
|
|
@@ -2943,6 +2993,60 @@ async def _poll_fresh_approval(
|
|
|
2943
2993
|
return "timeout"
|
|
2944
2994
|
|
|
2945
2995
|
|
|
2996
|
+
async def _gate_transform_approval(
|
|
2997
|
+
client: BackendClient, tid: str, jira_key: str, summary: str | None, action: str,
|
|
2998
|
+
send: Callable, thread_id: str, should_stop: Callable[[], bool] | None,
|
|
2999
|
+
) -> str:
|
|
3000
|
+
"""Human-approval gate for a non-coding TRANSFORM (Epic decomposition /
|
|
3001
|
+
Story→Task conversion) when agentMode=True. Sends the WhatsApp request and WAITS
|
|
3002
|
+
for the decision. Returns 'approved' | 'rejected' | 'stopped'. On a 15-min timeout
|
|
3003
|
+
the ticket goes to Backlog and we keep polling for a late reply. Does NOT change
|
|
3004
|
+
ticket status itself — the handler owns that (an Epic marks itself In Progress; a
|
|
3005
|
+
Story never does)."""
|
|
3006
|
+
label = jira_key or tid
|
|
3007
|
+
wa_summary = summary or jira_key or "task"
|
|
3008
|
+
await send(Envelope(
|
|
3009
|
+
type=Envelope.TYPE_AWAITING_APPROVAL, thread_id=thread_id,
|
|
3010
|
+
data={"jira_key": jira_key, "task_id": tid,
|
|
3011
|
+
"current_step": f"{label}: awaiting approval to {action}"},
|
|
3012
|
+
).model_dump())
|
|
3013
|
+
|
|
3014
|
+
async def _send_wa() -> bool:
|
|
3015
|
+
ok = await client.notify_whatsapp(
|
|
3016
|
+
tid, f"Approval needed for {label}: {action} — reply on WhatsApp to approve or reject.",
|
|
3017
|
+
summary=wa_summary,
|
|
3018
|
+
)
|
|
3019
|
+
note = "approval request sent" if ok else "approval request FAILED to send"
|
|
3020
|
+
await send(Envelope(
|
|
3021
|
+
type="step_update", thread_id=thread_id,
|
|
3022
|
+
data={"current_step": f"{label}: {note}"},
|
|
3023
|
+
).model_dump())
|
|
3024
|
+
try:
|
|
3025
|
+
await client.log_activity(f"Task {tid}: {note} ({action})")
|
|
3026
|
+
except Exception: # noqa: BLE001
|
|
3027
|
+
pass
|
|
3028
|
+
return ok
|
|
3029
|
+
|
|
3030
|
+
await _send_wa()
|
|
3031
|
+
decision = await _poll_approval(
|
|
3032
|
+
client, tid, should_stop,
|
|
3033
|
+
fallback_notify=_send_wa, fallback_after_s=_FIRST_GATE_WA_FALLBACK_S,
|
|
3034
|
+
)
|
|
3035
|
+
if decision == "timeout":
|
|
3036
|
+
await client.move_to_backlog(tid)
|
|
3037
|
+
await send(Envelope(
|
|
3038
|
+
type="step_update", thread_id=thread_id,
|
|
3039
|
+
data={"current_step": f"{label}: no approval in 15m — Backlog; polling for late approval"},
|
|
3040
|
+
).model_dump())
|
|
3041
|
+
while True:
|
|
3042
|
+
if should_stop is not None and should_stop():
|
|
3043
|
+
return "stopped"
|
|
3044
|
+
late = await client.approval_status(tid)
|
|
3045
|
+
if late in ("approved", "rejected"):
|
|
3046
|
+
return late
|
|
3047
|
+
await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
|
|
3048
|
+
return decision
|
|
3049
|
+
|
|
2946
3050
|
|
|
2947
3051
|
async def handle_orchestrate_auto_local_run(
|
|
2948
3052
|
client: BackendClient, payload: dict, send: Callable,
|
|
@@ -3053,6 +3157,27 @@ async def handle_orchestrate_auto_local_run(
|
|
|
3053
3157
|
if not await _validate_ticket_or_reject(client, task, "epic", payload, send, thread_id):
|
|
3054
3158
|
processed += 1
|
|
3055
3159
|
continue
|
|
3160
|
+
# agentMode gate: wait for human approval before decomposing.
|
|
3161
|
+
if agent_mode:
|
|
3162
|
+
dec = await _gate_transform_approval(
|
|
3163
|
+
client, tid, jira_key, task.get("summary"),
|
|
3164
|
+
"decompose this Epic into user stories",
|
|
3165
|
+
send, thread_id, should_stop,
|
|
3166
|
+
)
|
|
3167
|
+
if dec == "stopped":
|
|
3168
|
+
await send(Envelope(
|
|
3169
|
+
type=Envelope.TYPE_DONE, thread_id=thread_id,
|
|
3170
|
+
data={"mode": "orchestrate-auto-local", "status": "paused",
|
|
3171
|
+
"processed": processed, "task_count": total},
|
|
3172
|
+
).model_dump())
|
|
3173
|
+
return
|
|
3174
|
+
if dec != "approved":
|
|
3175
|
+
await send(Envelope(
|
|
3176
|
+
type="step_update", thread_id=thread_id,
|
|
3177
|
+
data={"current_step": f"{jira_key}: {dec} — Epic not decomposed"},
|
|
3178
|
+
).model_dump())
|
|
3179
|
+
processed += 1
|
|
3180
|
+
continue
|
|
3056
3181
|
await _handle_epic(client, task, payload, send, thread_id)
|
|
3057
3182
|
except Exception as exc: # noqa: BLE001
|
|
3058
3183
|
logger.warning("auto_local.epic_failed task=%s err=%s", tid, exc)
|
|
@@ -3075,6 +3200,27 @@ async def handle_orchestrate_auto_local_run(
|
|
|
3075
3200
|
):
|
|
3076
3201
|
processed += 1
|
|
3077
3202
|
continue
|
|
3203
|
+
# agentMode gate: wait for human approval before converting Story→Task.
|
|
3204
|
+
if agent_mode:
|
|
3205
|
+
dec = await _gate_transform_approval(
|
|
3206
|
+
client, tid, jira_key, task.get("summary"),
|
|
3207
|
+
"convert this Story into a Task",
|
|
3208
|
+
send, thread_id, should_stop,
|
|
3209
|
+
)
|
|
3210
|
+
if dec == "stopped":
|
|
3211
|
+
await send(Envelope(
|
|
3212
|
+
type=Envelope.TYPE_DONE, thread_id=thread_id,
|
|
3213
|
+
data={"mode": "orchestrate-auto-local", "status": "paused",
|
|
3214
|
+
"processed": processed, "task_count": total},
|
|
3215
|
+
).model_dump())
|
|
3216
|
+
return
|
|
3217
|
+
if dec != "approved":
|
|
3218
|
+
await send(Envelope(
|
|
3219
|
+
type="step_update", thread_id=thread_id,
|
|
3220
|
+
data={"current_step": f"{jira_key}: {dec} — Story not converted"},
|
|
3221
|
+
).model_dump())
|
|
3222
|
+
processed += 1
|
|
3223
|
+
continue
|
|
3078
3224
|
new_desc = await _handle_story(client, task, payload, send, thread_id)
|
|
3079
3225
|
except Exception as exc: # noqa: BLE001
|
|
3080
3226
|
logger.warning("auto_local.story_failed task=%s err=%s", tid, exc)
|
|
@@ -509,10 +509,12 @@ async def run_claude_local(
|
|
|
509
509
|
result_text = str(result_event.get("result", ""))
|
|
510
510
|
subtype = str(result_event.get("subtype", ""))
|
|
511
511
|
|
|
512
|
-
# Claude USAGE/RATE limit
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
|
|
512
|
+
# Claude USAGE/RATE limit. A real limit ABORTS the run (is_error) with a limit
|
|
513
|
+
# message, or the CLI flags it in the subtype. Do NOT infer a limit from a
|
|
514
|
+
# SUCCESSFUL run's result text — a task whose OUTPUT legitimately mentions
|
|
515
|
+
# "usage limit" / "rate limit" (e.g. building an observability/quota feature)
|
|
516
|
+
# would otherwise false-positive as a rate-limit hit.
|
|
517
|
+
limit_hit = "limit" in subtype.lower() or (is_error and _is_limit(result_text))
|
|
516
518
|
reset_epoch = _parse_reset_epoch(result_text)
|
|
517
519
|
|
|
518
520
|
err_text = None
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.71 → devops_bot_sdk-1.4.76}/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
|