devops-bot-sdk 1.4.62__tar.gz → 1.4.66__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.62 → devops_bot_sdk-1.4.66}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/client.py +3 -3
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/ipc/handlers.py +132 -39
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/README.md +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/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.66"
|
|
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.66
|
|
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.66"
|
|
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.
|
|
61
|
+
SDK_VERSION = "1.4.66"
|
|
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
|
|
@@ -821,7 +821,7 @@ class BackendClient:
|
|
|
821
821
|
# field like `modelUsed` can repeat.
|
|
822
822
|
files = [(k, (None, str(v))) for k, v in fields.items() if v is not None]
|
|
823
823
|
for m in (model_used or []):
|
|
824
|
-
files.append(("modelUsed", (None, str(m))))
|
|
824
|
+
files.append(("modelUsed[]", (None, str(m)))) # [] → backend parses as array
|
|
825
825
|
try:
|
|
826
826
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
827
827
|
resp = await client.put(url, files=files, headers=headers)
|
|
@@ -850,7 +850,7 @@ class BackendClient:
|
|
|
850
850
|
headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
|
|
851
851
|
files = [(k, (None, str(v))) for k, v in fields.items() if v is not None]
|
|
852
852
|
for m in (model_used or []):
|
|
853
|
-
files.append(("modelUsed", (None, str(m))))
|
|
853
|
+
files.append(("modelUsed[]", (None, str(m)))) # [] → backend parses as array
|
|
854
854
|
try:
|
|
855
855
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
856
856
|
resp = await client.post(url, files=files, headers=headers)
|
|
@@ -371,6 +371,50 @@ def _parse_scope(recon_text: str | None) -> str:
|
|
|
371
371
|
return "in"
|
|
372
372
|
|
|
373
373
|
|
|
374
|
+
# Heartbeat cadence for long, quiet local runs (PRD generation, validation) so the
|
|
375
|
+
# log shows liveness even when the agent is generating text with no tool calls.
|
|
376
|
+
_HEARTBEAT_S = float(os.getenv("AGENTOS_HEARTBEAT_S", "") or 20.0)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def _run_with_heartbeat(coro, *, send: Callable, thread_id: str, label: str,
|
|
380
|
+
name: str | None = None, interval: float = _HEARTBEAT_S):
|
|
381
|
+
"""Await `coro` while emitting a periodic 'still working (Ns)' step_update, so a
|
|
382
|
+
long-but-quiet local run (e.g. PRD generation) never looks frozen in the log."""
|
|
383
|
+
import time as _t
|
|
384
|
+
|
|
385
|
+
prefix = f"[{name}] " if name else ""
|
|
386
|
+
stop = asyncio.Event()
|
|
387
|
+
started = _t.monotonic()
|
|
388
|
+
|
|
389
|
+
async def _beat() -> None:
|
|
390
|
+
try:
|
|
391
|
+
while not stop.is_set():
|
|
392
|
+
try:
|
|
393
|
+
await asyncio.wait_for(stop.wait(), timeout=interval)
|
|
394
|
+
return
|
|
395
|
+
except asyncio.TimeoutError:
|
|
396
|
+
pass
|
|
397
|
+
await send(Envelope(
|
|
398
|
+
type="step_update", thread_id=thread_id,
|
|
399
|
+
data={"status": "running",
|
|
400
|
+
"current_step": f"{prefix}{label} … still working "
|
|
401
|
+
f"({int(_t.monotonic() - started)}s)"},
|
|
402
|
+
).model_dump())
|
|
403
|
+
except asyncio.CancelledError:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
beat = asyncio.create_task(_beat())
|
|
407
|
+
try:
|
|
408
|
+
return await coro
|
|
409
|
+
finally:
|
|
410
|
+
stop.set()
|
|
411
|
+
beat.cancel()
|
|
412
|
+
try:
|
|
413
|
+
await beat
|
|
414
|
+
except Exception: # noqa: BLE001
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
|
|
374
418
|
# ── Epic decomposition ────────────────────────────────────────────────────
|
|
375
419
|
# An EPIC is not coded directly. It is analysed (grounded in the codebase when a
|
|
376
420
|
# project path exists) into a detailed user-story description + an implementation
|
|
@@ -572,30 +616,42 @@ def _build_validation_prompt(kind: str, jira_key: str, summary: str,
|
|
|
572
616
|
"Description:\n" + (_clip_text(description.strip()) if description and description.strip() else "(none provided)"),
|
|
573
617
|
"Human Instructions:\n" + (human.strip() if human and human.strip() else "(none provided)"),
|
|
574
618
|
]
|
|
619
|
+
is_epic = kind == "epic"
|
|
575
620
|
parts.append(
|
|
576
|
-
"Judge the ticket
|
|
577
|
-
"
|
|
578
|
-
"
|
|
621
|
+
"Judge the ticket for what it ACTUALLY asks for — software, documentation, "
|
|
622
|
+
"analysis, Finance, HR or operations. Do NOT impose requirements the ticket "
|
|
623
|
+
"does not need.\n\n"
|
|
579
624
|
"A VALID ticket MUST have:\n"
|
|
580
625
|
"- A clear, unambiguous goal and scope (no hand-waving).\n"
|
|
581
|
-
"- A clearly-defined deliverable
|
|
582
|
-
"
|
|
583
|
-
"- Observable
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
626
|
+
"- A clearly-defined deliverable/outcome (and where to produce it when a "
|
|
627
|
+
"location matters).\n"
|
|
628
|
+
"- Observable acceptance criteria (or an equally concrete definition of done).\n"
|
|
629
|
+
+ (
|
|
630
|
+
# EPIC: a decomposition container — NEVER require code-execution details.
|
|
631
|
+
"- Enough substance to DECOMPOSE into user stories.\n"
|
|
632
|
+
"IMPORTANT: this is an EPIC — a high-level container the system decomposes "
|
|
633
|
+
"into user stories. Do NOT require a repository URL, file paths, branch/PR "
|
|
634
|
+
"convention, or build/run/test commands at the epic level; those are "
|
|
635
|
+
"established LATER at the story/task stage, never in the epic. Even when the "
|
|
636
|
+
"epic describes software features, judge it ONLY on goal, scope, user "
|
|
637
|
+
"stories and epic-level acceptance criteria. NEVER reject an epic for missing "
|
|
638
|
+
"repository, branch, PR, build, or test details.\n"
|
|
639
|
+
if is_epic else
|
|
640
|
+
# STORY: repo details help but the PRD/coding stage grounds them.
|
|
641
|
+
"- A clear 'As a <role>, I want <capability>, so that <benefit>' intent plus "
|
|
642
|
+
"testable acceptance criteria.\n"
|
|
643
|
+
"For a code story, a target repo/path is helpful but NOT mandatory here — the "
|
|
644
|
+
"PRD/coding stage grounds it against the codebase. Do NOT reject solely for a "
|
|
645
|
+
"missing branch/PR convention or build/test steps.\n"
|
|
646
|
+
)
|
|
592
647
|
+ "- Any THIRD-PARTY INTEGRATION or human-effort dependency the work GENUINELY "
|
|
593
|
-
"needs (API keys, OAuth credentials, external accounts, secrets,
|
|
594
|
-
"
|
|
595
|
-
"
|
|
596
|
-
"
|
|
597
|
-
"
|
|
598
|
-
"ambiguous information — not for absent
|
|
648
|
+
"needs (API keys, OAuth credentials, external accounts, secrets, provisioning) "
|
|
649
|
+
"MUST be named WITH who provides it; if a REQUIRED dependency is undefined, REJECT.\n\n"
|
|
650
|
+
"Unrendered template placeholders — an EMPTY `{{...}}` or `<...>` with no real "
|
|
651
|
+
"content — are a valid reason to reject. But if a section contains real content "
|
|
652
|
+
"merely wrapped in stray markers, treat it as PRESENT, not missing.\n"
|
|
653
|
+
"Reject only for genuinely missing or ambiguous information — not for absent "
|
|
654
|
+
"execution details the ticket does not need at this stage.\n\n"
|
|
599
655
|
"Output ONLY a fenced ```json block, nothing else:\n"
|
|
600
656
|
"{\n"
|
|
601
657
|
' "valid": true or false,\n'
|
|
@@ -624,9 +680,14 @@ async def _validate_ticket_or_reject(
|
|
|
624
680
|
we never block a ticket on our own infrastructure error."""
|
|
625
681
|
from sdk import local_exec
|
|
626
682
|
|
|
683
|
+
from pathlib import Path
|
|
684
|
+
|
|
627
685
|
tid = str(task.get("id", ""))
|
|
628
686
|
jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "") or kind.upper()
|
|
629
|
-
|
|
687
|
+
# Validation is a pure text judgment — run it in a guaranteed-writable scratch
|
|
688
|
+
# dir, never the resolved project path (which may not exist yet or be unwritable,
|
|
689
|
+
# e.g. a code-reference like /components). This decouples the gate from the repo.
|
|
690
|
+
scratch = str(Path.home() / ".agentos" / "validate")
|
|
630
691
|
|
|
631
692
|
await send(Envelope(
|
|
632
693
|
type="step_update", thread_id=thread_id,
|
|
@@ -634,11 +695,14 @@ async def _validate_ticket_or_reject(
|
|
|
634
695
|
"current_step": f"{kind.title()} {jira_key}: validating ticket against the standard"},
|
|
635
696
|
).model_dump())
|
|
636
697
|
|
|
637
|
-
res = await
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
698
|
+
res = await _run_with_heartbeat(
|
|
699
|
+
local_exec.run_claude_local(
|
|
700
|
+
_build_validation_prompt(kind, jira_key, task.get("summary", "") or "",
|
|
701
|
+
task.get("description"), task.get("humanInstructions")),
|
|
702
|
+
scratch, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only judgment, scratch dir
|
|
703
|
+
github_token=await client.github_token(),
|
|
704
|
+
),
|
|
705
|
+
send=send, thread_id=thread_id, label=f"{kind.title()} {jira_key}: validating",
|
|
642
706
|
)
|
|
643
707
|
if not res.get("ok"):
|
|
644
708
|
await client.log_activity(
|
|
@@ -1237,13 +1301,16 @@ async def _code_one_repo(
|
|
|
1237
1301
|
data={"status": "running",
|
|
1238
1302
|
"current_step": f"[{name}] Recon — exploring the repo to ground the plan"},
|
|
1239
1303
|
).model_dump())
|
|
1240
|
-
recon = await
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1304
|
+
recon = await _run_with_heartbeat(
|
|
1305
|
+
local_exec.run_claude_local(
|
|
1306
|
+
_build_recon_prompt(
|
|
1307
|
+
name, pending["user_input"], pending.get("structured_plan"),
|
|
1308
|
+
pending.get("human_instructions"),
|
|
1309
|
+
),
|
|
1310
|
+
repo_path, on_event=_on_event, github_token=gh_token,
|
|
1311
|
+
allowed_tools=_RECON_ALLOWED_TOOLS, # read-only: no Write/Edit/Bash
|
|
1244
1312
|
),
|
|
1245
|
-
|
|
1246
|
-
allowed_tools=_RECON_ALLOWED_TOOLS, # read-only: no Write/Edit/Bash
|
|
1313
|
+
send=send, thread_id=thread_id, label="Exploring the repo (recon)", name=name,
|
|
1247
1314
|
)
|
|
1248
1315
|
recon_session = recon.get("session_id")
|
|
1249
1316
|
# A usage-limit hit during recon must be surfaced so the loop waits/retries
|
|
@@ -1343,9 +1410,13 @@ async def _code_one_repo(
|
|
|
1343
1410
|
"code you found. Output ONLY the PRD as Markdown, following this format "
|
|
1344
1411
|
"exactly:\n\n" + _PRD_FORMAT
|
|
1345
1412
|
)
|
|
1346
|
-
prd_res = await
|
|
1347
|
-
|
|
1348
|
-
|
|
1413
|
+
prd_res = await _run_with_heartbeat(
|
|
1414
|
+
local_exec.run_claude_local(
|
|
1415
|
+
instruction, repo_path, on_event=_on_event, # stream tool activity
|
|
1416
|
+
allowed_tools=_RECON_ALLOWED_TOOLS, # read-only
|
|
1417
|
+
github_token=gh_token, resume_session_id=recon_session,
|
|
1418
|
+
),
|
|
1419
|
+
send=send, thread_id=thread_id, label="Working on the PRD", name=name,
|
|
1349
1420
|
)
|
|
1350
1421
|
recon_session = prd_res.get("session_id") or recon_session
|
|
1351
1422
|
prd = (prd_res.get("result") or "").strip() if prd_res.get("ok") else ""
|
|
@@ -2588,6 +2659,16 @@ def _extract_repo_targets(text: str | None) -> list[dict]:
|
|
|
2588
2659
|
for p in _extract_all_paths(text):
|
|
2589
2660
|
name = Path(p).name
|
|
2590
2661
|
url = by_name.get(name.lower())
|
|
2662
|
+
# Ignore bare code-reference paths mentioned in the instructions (e.g.
|
|
2663
|
+
# "use shared components in `/components`") that are neither a real repo
|
|
2664
|
+
# (paired with a GitHub URL) nor an existing local directory. Without this
|
|
2665
|
+
# a path like /components gets picked as the work dir → Permission denied.
|
|
2666
|
+
if not url:
|
|
2667
|
+
try:
|
|
2668
|
+
if not Path(p).expanduser().is_dir():
|
|
2669
|
+
continue
|
|
2670
|
+
except OSError:
|
|
2671
|
+
continue
|
|
2591
2672
|
targets.append({
|
|
2592
2673
|
"path": p,
|
|
2593
2674
|
"url": url,
|
|
@@ -2626,13 +2707,25 @@ def _resolve_local_project_path(task: dict, payload: dict) -> str:
|
|
|
2626
2707
|
if overrides.get(tid):
|
|
2627
2708
|
return str(Path(overrides[tid]).expanduser())
|
|
2628
2709
|
|
|
2629
|
-
# PRIMARY — honour the location the human wrote in the Jira task.
|
|
2710
|
+
# PRIMARY — honour the location the human wrote in the Jira task. Prefer a REAL
|
|
2711
|
+
# repo (a path paired with a GitHub URL), then any surviving repo target
|
|
2712
|
+
# (existing local dir), then a bare path that actually exists — never a
|
|
2713
|
+
# code-reference path like `/components` that doesn't exist on disk.
|
|
2630
2714
|
for field in ("humanInstructions", "description"):
|
|
2631
|
-
|
|
2632
|
-
if
|
|
2715
|
+
text = task.get(field)
|
|
2716
|
+
if not text:
|
|
2717
|
+
continue
|
|
2718
|
+
targets = _extract_repo_targets(text) # spurious/non-existent paths filtered out
|
|
2719
|
+
with_url = next((t["path"] for t in targets if t.get("url")), None)
|
|
2720
|
+
if with_url:
|
|
2721
|
+
return with_url
|
|
2722
|
+
if targets:
|
|
2723
|
+
return targets[0]["path"]
|
|
2724
|
+
found = _extract_path_from_text(text)
|
|
2725
|
+
if found and Path(found).expanduser().exists():
|
|
2633
2726
|
return found
|
|
2634
2727
|
|
|
2635
|
-
# FALLBACK —
|
|
2728
|
+
# FALLBACK — no usable location instructed anywhere.
|
|
2636
2729
|
root = payload.get("workspace_root") or str(Path.home() / "AgentOS")
|
|
2637
2730
|
key = (
|
|
2638
2731
|
task.get("projectKey")
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/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
|