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.
Files changed (38) hide show
  1. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/client.py +3 -3
  6. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/ipc/handlers.py +132 -39
  7. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/README.md +0 -0
  8. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/local_exec.py +0 -0
  27. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.62 → devops_bot_sdk-1.4.66}/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.66
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.66
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.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.62
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.62"
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.62"
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 in the context of what it ACTUALLY asks for — it may be "
577
- "software, documentation, analysis, Finance, HR or operations work. Do NOT "
578
- "impose software requirements on a non-software ticket.\n\n"
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 or outcome, and when a location matters "
582
- "WHERE to produce it (a folder path, a repository, or a document).\n"
583
- "- Observable, testable acceptance criteria (or an equally concrete definition of done).\n"
584
- "- ONLY IF the work requires CODE CHANGES to a software project: the target "
585
- "repository link(s) and/or file path(s), the branch/PR convention, and "
586
- "build/run/test steps. Do NOT demand these for documentation, content, analysis "
587
- "or other non-code deliverables.\n"
588
- + ("- Enough concrete detail to decompose into independent user stories.\n"
589
- if kind == "epic" else
590
- "- A clear 'As a <role>, I want <capability>, so that <benefit>' intent plus "
591
- "testable acceptance criteria.\n")
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, manual "
594
- "provisioning, DNS, billing) MUST be named WITH who provides it; if such a "
595
- "REQUIRED dependency is undefined, REJECT.\n\n"
596
- "A ticket that clearly states its deliverable and where to produce it is VALID "
597
- "even without repository/branch details. Reject only for genuinely missing or "
598
- "ambiguous information — not for absent software details the task does not need.\n\n"
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
- project_path = _resolve_local_project_path(task, payload)
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 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(),
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 local_exec.run_claude_local(
1241
- _build_recon_prompt(
1242
- name, pending["user_input"], pending.get("structured_plan"),
1243
- pending.get("human_instructions"),
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
- repo_path, on_event=_on_event, github_token=gh_token,
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 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,
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
- found = _extract_path_from_text(task.get(field))
2632
- if found:
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 — only when no location was instructed anywhere.
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")