devops-bot-sdk 1.4.55__tar.gz → 1.4.58__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.55 → devops_bot_sdk-1.4.58}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/client.py +29 -1
  6. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/ipc/handlers.py +122 -0
  7. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/README.md +0 -0
  8. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/local_exec.py +0 -0
  27. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.58}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.55
3
+ Version: 1.4.58
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.55
3
+ Version: 1.4.58
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.55"
7
+ version = "1.4.58"
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.55
3
+ Version: 1.4.58
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.55"
33
+ __version__ = "1.4.58"
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.55"
61
+ SDK_VERSION = "1.4.58"
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
@@ -868,6 +868,34 @@ class BackendClient:
868
868
  logger.warning("client.create_task_ml error err=%s", exc)
869
869
  return None
870
870
 
871
+ async def add_comment(self, task_id: str, body: str) -> bool:
872
+ """POST /tasks-ml/{id}/comments — add a comment to a ticket (best-effort).
873
+
874
+ Used by the validation gate to post AI feedback (prefixed with an
875
+ [AI Comments] label) when an Epic/Story is rejected. JSON body: {"body": ...}.
876
+ Returns True on 2xx.
877
+ """
878
+ from sdk.config import BE_DEFAULT_URL
879
+
880
+ url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}/comments"
881
+ headers = {
882
+ **self._headers, # includes Content-Type: application/json
883
+ "accept": "*/*",
884
+ "developer-name": "null",
885
+ "product": "",
886
+ }
887
+ try:
888
+ async with httpx.AsyncClient(timeout=20.0) as client:
889
+ resp = await client.post(url, json={"body": str(body)}, headers=headers)
890
+ if resp.status_code >= 400:
891
+ logger.warning("client.add_comment failed task=%s status=%s body=%s",
892
+ task_id, resp.status_code, resp.text[:200])
893
+ return False
894
+ return True
895
+ except Exception as exc: # noqa: BLE001
896
+ logger.warning("client.add_comment error task=%s err=%s", task_id, exc)
897
+ return False
898
+
871
899
  async def log_activity(self, activity: str) -> bool:
872
900
  """POST /ml-api/agent-activity — emit an agent activity event (best-effort).
873
901
 
@@ -550,6 +550,117 @@ def _epic_inherited_fields(task: dict) -> dict:
550
550
  return {k: v for k, v in fields.items() if v}
551
551
 
552
552
 
553
+ # ── Ticket validation gate (Epics + USER-driven Stories) ────────────────────
554
+ # A strict quality gate: before the bot actions an Epic or a USER-created Story it
555
+ # checks the ticket meets the required standard. On failure it posts a harsh,
556
+ # technical comment (prefixed with the label below) and moves the ticket to
557
+ # Backlog. SYSTEM-created Stories (spun off an Epic by the bot) are trusted and
558
+ # skip validation. A later run re-validates; once the gaps are addressed in the
559
+ # description / Human Instructions, it passes and the normal flow proceeds.
560
+ _AI_COMMENT_LABEL = "[AI Comments]"
561
+
562
+
563
+ def _build_validation_prompt(kind: str, jira_key: str, summary: str,
564
+ description: str | None, human: str | None) -> str:
565
+ k = kind.upper()
566
+ parts: list[str] = [
567
+ f"You are a STRICT ticket-quality gate for a JIRA {k}. Decide whether this "
568
+ "ticket meets the required standard to be actioned autonomously. Be HARSH: if "
569
+ "the required structure or information is not strictly present, REJECT it — a "
570
+ "vague or under-specified ticket must NOT pass.",
571
+ f"=== {k} {jira_key} ===\nSummary: {summary or '(none)'}",
572
+ "Description:\n" + (_clip_text(description.strip()) if description and description.strip() else "(none provided)"),
573
+ "Human Instructions:\n" + (human.strip() if human and human.strip() else "(none provided)"),
574
+ ]
575
+ parts.append(
576
+ "A VALID ticket MUST have:\n"
577
+ "- A clear, unambiguous goal and scope (no hand-waving).\n"
578
+ "- Explicit Human Instructions: for codebase work, the repository link(s) "
579
+ "and/or file path(s), the branch/PR convention, and build/run/test steps.\n"
580
+ "- Observable, testable acceptance criteria.\n"
581
+ + ("- Enough concrete detail to decompose into independent user stories.\n"
582
+ if kind == "epic" else
583
+ "- A clear 'As a <role>, I want <capability>, so that <benefit>' intent plus "
584
+ "testable acceptance criteria.\n")
585
+ + "- Any THIRD-PARTY INTEGRATION or human-effort dependency (API keys, OAuth "
586
+ "credentials, external accounts, secrets, manual provisioning, DNS, billing) "
587
+ "MUST be named explicitly WITH who provides it. If such a dependency is implied "
588
+ "but undefined/missing, REJECT.\n\n"
589
+ "Output ONLY a fenced ```json block, nothing else:\n"
590
+ "{\n"
591
+ ' "valid": true or false,\n'
592
+ ' "comment": "When invalid: a concise, TECHNICAL rejection that lists EXACTLY '
593
+ "what is missing or ambiguous and must be added. Use precise engineering "
594
+ "terminology (API contract, data model/schema, auth/OAuth flow, env/config, "
595
+ "acceptance criteria, target repository/branch, integration credentials & "
596
+ 'ownership). When valid: empty string."\n'
597
+ "}"
598
+ )
599
+ return "\n\n".join(parts)
600
+
601
+
602
+ def _parse_validation(text: str | None) -> tuple[bool, str]:
603
+ """(valid, comment) from the validator's JSON output. Defaults to invalid."""
604
+ obj = _extract_json_block(text) or {}
605
+ return bool(obj.get("valid")), str(obj.get("comment") or "").strip()
606
+
607
+
608
+ async def _validate_ticket_or_reject(
609
+ client: BackendClient, task: dict, kind: str, payload: dict,
610
+ send: Callable, thread_id: str,
611
+ ) -> bool:
612
+ """Strict gate. Returns True to proceed, False if the ticket was rejected
613
+ (comment posted + moved to Backlog). Fails OPEN if the validator can't run —
614
+ we never block a ticket on our own infrastructure error."""
615
+ from sdk import local_exec
616
+
617
+ tid = str(task.get("id", ""))
618
+ jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "") or kind.upper()
619
+ project_path = _resolve_local_project_path(task, payload)
620
+
621
+ await send(Envelope(
622
+ type="step_update", thread_id=thread_id,
623
+ data={"jira_task_id": tid, "task_id": tid,
624
+ "current_step": f"{kind.title()} {jira_key}: validating ticket against the standard"},
625
+ ).model_dump())
626
+
627
+ res = await local_exec.run_claude_local(
628
+ _build_validation_prompt(kind, jira_key, task.get("summary", "") or "",
629
+ task.get("description"), task.get("humanInstructions")),
630
+ project_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only judgment
631
+ github_token=await client.github_token(),
632
+ )
633
+ if not res.get("ok"):
634
+ await client.log_activity(
635
+ f"{kind} {jira_key}: validation could not run ({(res.get('error') or '')[:120]}) — proceeding"
636
+ )
637
+ return True # fail-open on validator infra error
638
+
639
+ valid, comment = _parse_validation(res.get("result"))
640
+ if valid:
641
+ await send(Envelope(
642
+ type="step_update", thread_id=thread_id,
643
+ data={"jira_task_id": tid, "current_step": f"{kind.title()} {jira_key}: validation passed"},
644
+ ).model_dump())
645
+ return True
646
+
647
+ # Rejected — post a labelled, technical comment and move to Backlog.
648
+ fallback = ("This ticket does not meet the required standard. Provide: a clear goal "
649
+ "and scope; explicit Human Instructions (target repository/branch and/or "
650
+ "file paths, PR convention, build/run/test steps); testable acceptance "
651
+ "criteria; and the credentials/ownership for any third-party integration.")
652
+ body = f"{_AI_COMMENT_LABEL}\n\n{comment or fallback}"
653
+ await client.add_comment(tid, body)
654
+ await client.move_to_backlog(tid)
655
+ await client.log_activity(f"{kind} {jira_key}: REJECTED by validation gate — commented + moved to Backlog")
656
+ await send(Envelope(
657
+ type=Envelope.TYPE_ERROR, thread_id=thread_id,
658
+ data={"code": f"{kind.upper()}_VALIDATION_REJECTED", "task_id": tid,
659
+ "message": (comment or fallback)[:300]},
660
+ ).model_dump())
661
+ return False
662
+
663
+
553
664
  async def _handle_epic(
554
665
  client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
555
666
  ) -> None:
@@ -628,6 +739,7 @@ async def _handle_epic(
628
739
  **inherited,
629
740
  "type": "Story", # Epic decomposes into user Stories; a Story later
630
741
  # becomes a Task (see _handle_story) when it is run.
742
+ "drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
631
743
  "status": "To Do",
632
744
  "summary": str(st.get("summary", "")).strip()[:250],
633
745
  "description": str(st.get("description", "") or "")[:20000],
@@ -2720,7 +2832,17 @@ async def handle_orchestrate_auto_local_run(
2720
2832
  if ttype in ("epic", "story"):
2721
2833
  handler = _handle_epic if ttype == "epic" else _handle_story
2722
2834
  code = "EPIC_DECOMPOSE_FAILED" if ttype == "epic" else "STORY_HANDLE_FAILED"
2835
+ # Validation gate: Epics are always validated; a Story only when it
2836
+ # is USER-driven (a SYSTEM/bot-created Story is trusted). A rejected
2837
+ # ticket is commented + moved to Backlog inside the gate — skip it.
2838
+ driven_by = str(task.get("drivenBy") or "").strip().upper()
2839
+ needs_validation = (ttype == "epic") or (ttype == "story" and driven_by != "SYSTEM")
2723
2840
  try:
2841
+ if needs_validation and not await _validate_ticket_or_reject(
2842
+ client, task, ttype, payload, send, thread_id
2843
+ ):
2844
+ processed += 1
2845
+ continue
2724
2846
  await handler(client, task, payload, send, thread_id)
2725
2847
  except Exception as exc: # noqa: BLE001
2726
2848
  logger.warning("auto_local.%s_failed task=%s err=%s", ttype, tid, exc)