devops-bot-sdk 1.4.62__tar.gz → 1.4.65__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.62 → devops_bot_sdk-1.4.65}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/client.py +3 -3
  6. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/ipc/handlers.py +100 -19
  7. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/README.md +0 -0
  8. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/local_exec.py +0 -0
  27. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.65}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.62
3
+ Version: 1.4.65
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.62
3
+ Version: 1.4.65
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.62"
7
+ version = "1.4.65"
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.62
3
+ Version: 1.4.65
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.62"
33
+ __version__ = "1.4.65"
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.62"
61
+ SDK_VERSION = "1.4.65"
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
@@ -624,9 +668,14 @@ async def _validate_ticket_or_reject(
624
668
  we never block a ticket on our own infrastructure error."""
625
669
  from sdk import local_exec
626
670
 
671
+ from pathlib import Path
672
+
627
673
  tid = str(task.get("id", ""))
628
674
  jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "") or kind.upper()
629
- project_path = _resolve_local_project_path(task, payload)
675
+ # Validation is a pure text judgment — run it in a guaranteed-writable scratch
676
+ # dir, never the resolved project path (which may not exist yet or be unwritable,
677
+ # e.g. a code-reference like /components). This decouples the gate from the repo.
678
+ scratch = str(Path.home() / ".agentos" / "validate")
630
679
 
631
680
  await send(Envelope(
632
681
  type="step_update", thread_id=thread_id,
@@ -634,11 +683,14 @@ async def _validate_ticket_or_reject(
634
683
  "current_step": f"{kind.title()} {jira_key}: validating ticket against the standard"},
635
684
  ).model_dump())
636
685
 
637
- res = await local_exec.run_claude_local(
638
- _build_validation_prompt(kind, jira_key, task.get("summary", "") or "",
639
- task.get("description"), task.get("humanInstructions")),
640
- project_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only judgment
641
- github_token=await client.github_token(),
686
+ res = await _run_with_heartbeat(
687
+ local_exec.run_claude_local(
688
+ _build_validation_prompt(kind, jira_key, task.get("summary", "") or "",
689
+ task.get("description"), task.get("humanInstructions")),
690
+ scratch, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only judgment, scratch dir
691
+ github_token=await client.github_token(),
692
+ ),
693
+ send=send, thread_id=thread_id, label=f"{kind.title()} {jira_key}: validating",
642
694
  )
643
695
  if not res.get("ok"):
644
696
  await client.log_activity(
@@ -1237,13 +1289,16 @@ async def _code_one_repo(
1237
1289
  data={"status": "running",
1238
1290
  "current_step": f"[{name}] Recon — exploring the repo to ground the plan"},
1239
1291
  ).model_dump())
1240
- recon = await local_exec.run_claude_local(
1241
- _build_recon_prompt(
1242
- name, pending["user_input"], pending.get("structured_plan"),
1243
- pending.get("human_instructions"),
1292
+ recon = await _run_with_heartbeat(
1293
+ local_exec.run_claude_local(
1294
+ _build_recon_prompt(
1295
+ name, pending["user_input"], pending.get("structured_plan"),
1296
+ pending.get("human_instructions"),
1297
+ ),
1298
+ repo_path, on_event=_on_event, github_token=gh_token,
1299
+ allowed_tools=_RECON_ALLOWED_TOOLS, # read-only: no Write/Edit/Bash
1244
1300
  ),
1245
- repo_path, on_event=_on_event, github_token=gh_token,
1246
- allowed_tools=_RECON_ALLOWED_TOOLS, # read-only: no Write/Edit/Bash
1301
+ send=send, thread_id=thread_id, label="Exploring the repo (recon)", name=name,
1247
1302
  )
1248
1303
  recon_session = recon.get("session_id")
1249
1304
  # A usage-limit hit during recon must be surfaced so the loop waits/retries
@@ -1343,9 +1398,13 @@ async def _code_one_repo(
1343
1398
  "code you found. Output ONLY the PRD as Markdown, following this format "
1344
1399
  "exactly:\n\n" + _PRD_FORMAT
1345
1400
  )
1346
- prd_res = await local_exec.run_claude_local(
1347
- instruction, repo_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only
1348
- github_token=gh_token, resume_session_id=recon_session,
1401
+ prd_res = await _run_with_heartbeat(
1402
+ local_exec.run_claude_local(
1403
+ instruction, repo_path, on_event=_on_event, # stream tool activity
1404
+ allowed_tools=_RECON_ALLOWED_TOOLS, # read-only
1405
+ github_token=gh_token, resume_session_id=recon_session,
1406
+ ),
1407
+ send=send, thread_id=thread_id, label="Working on the PRD", name=name,
1349
1408
  )
1350
1409
  recon_session = prd_res.get("session_id") or recon_session
1351
1410
  prd = (prd_res.get("result") or "").strip() if prd_res.get("ok") else ""
@@ -2588,6 +2647,16 @@ def _extract_repo_targets(text: str | None) -> list[dict]:
2588
2647
  for p in _extract_all_paths(text):
2589
2648
  name = Path(p).name
2590
2649
  url = by_name.get(name.lower())
2650
+ # Ignore bare code-reference paths mentioned in the instructions (e.g.
2651
+ # "use shared components in `/components`") that are neither a real repo
2652
+ # (paired with a GitHub URL) nor an existing local directory. Without this
2653
+ # a path like /components gets picked as the work dir → Permission denied.
2654
+ if not url:
2655
+ try:
2656
+ if not Path(p).expanduser().is_dir():
2657
+ continue
2658
+ except OSError:
2659
+ continue
2591
2660
  targets.append({
2592
2661
  "path": p,
2593
2662
  "url": url,
@@ -2626,13 +2695,25 @@ def _resolve_local_project_path(task: dict, payload: dict) -> str:
2626
2695
  if overrides.get(tid):
2627
2696
  return str(Path(overrides[tid]).expanduser())
2628
2697
 
2629
- # PRIMARY — honour the location the human wrote in the Jira task.
2698
+ # PRIMARY — honour the location the human wrote in the Jira task. Prefer a REAL
2699
+ # repo (a path paired with a GitHub URL), then any surviving repo target
2700
+ # (existing local dir), then a bare path that actually exists — never a
2701
+ # code-reference path like `/components` that doesn't exist on disk.
2630
2702
  for field in ("humanInstructions", "description"):
2631
- found = _extract_path_from_text(task.get(field))
2632
- if found:
2703
+ text = task.get(field)
2704
+ if not text:
2705
+ continue
2706
+ targets = _extract_repo_targets(text) # spurious/non-existent paths filtered out
2707
+ with_url = next((t["path"] for t in targets if t.get("url")), None)
2708
+ if with_url:
2709
+ return with_url
2710
+ if targets:
2711
+ return targets[0]["path"]
2712
+ found = _extract_path_from_text(text)
2713
+ if found and Path(found).expanduser().exists():
2633
2714
  return found
2634
2715
 
2635
- # FALLBACK — only when no location was instructed anywhere.
2716
+ # FALLBACK — no usable location instructed anywhere.
2636
2717
  root = payload.get("workspace_root") or str(Path.home() / "AgentOS")
2637
2718
  key = (
2638
2719
  task.get("projectKey")