devops-bot-sdk 1.4.79__tar.gz → 1.4.83__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.79 → devops_bot_sdk-1.4.83}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/ipc/handlers.py +70 -9
  7. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/README.md +0 -0
  8. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/local_exec.py +0 -0
  27. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.79 → devops_bot_sdk-1.4.83}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.79
3
+ Version: 1.4.83
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.79
3
+ Version: 1.4.83
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.79"
7
+ version = "1.4.83"
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.79
3
+ Version: 1.4.83
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.79"
33
+ __version__ = "1.4.83"
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.79"
61
+ SDK_VERSION = "1.4.83"
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
@@ -531,7 +531,8 @@ _PRD_FORMAT = (
531
531
 
532
532
 
533
533
  def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
534
- human_instructions: str | None, story_count: int | None = None) -> str:
534
+ human_instructions: str | None, story_count: int | None = None,
535
+ story_titles: list[str] | None = None) -> str:
535
536
  """Read-only prompt that turns an Epic into a decomposition JSON."""
536
537
  parts: list[str] = [
537
538
  "You are decomposing a JIRA EPIC into implementable user stories. This is "
@@ -578,9 +579,22 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
578
579
  "}\n\n"
579
580
  "Do NOT produce humanInstructions for the stories — created stories are clean "
580
581
  "user-story artifacts; implementation instructions are added later.\n"
581
- "Create as many stories as the epic genuinely needs (do not pad or truncate "
582
- "artificially); each story must be independently implementable.\n"
583
- "assigneeEmail is OPTIONAL per story: set it ONLY when the epic explicitly "
582
+ + (
583
+ # The epic ENUMERATES its stories → reproduce them EXACTLY (verbatim titles).
584
+ ("The epic already lists its user stories. Produce EXACTLY these "
585
+ f"{len(story_titles)} stor{'y' if len(story_titles) == 1 else 'ies'}, in this "
586
+ "order, using each title VERBATIM as the story's summary — do NOT invent, "
587
+ "rename, merge, add, drop, or reorder any. Only expand each into the USER "
588
+ "STORY FORMAT for its description:\n"
589
+ + "\n".join(f" {i}. {t}" for i, t in enumerate(story_titles, 1)) + "\n")
590
+ if story_titles else
591
+ (f"Generate EXACTLY {story_count} user stor{'y' if story_count == 1 else 'ies'} — "
592
+ "the epic explicitly asks for this many; do not produce more or fewer.\n"
593
+ if story_count else
594
+ "Create as many stories as the epic genuinely needs (do not pad or truncate "
595
+ "artificially); each story must be independently implementable.\n")
596
+ )
597
+ + "assigneeEmail is OPTIONAL per story: set it ONLY when the epic explicitly "
584
598
  "assigns that specific work to a named person/email (e.g. 'assign the API to "
585
599
  "alice@x.com'); otherwise omit it.\n\n"
586
600
  "=== EPIC FORMAT (epic_description MUST follow this exactly) ===\n"
@@ -681,6 +695,42 @@ def _extract_story_count(text: str | None) -> int | None:
681
695
  return min(n, 50) if n and n >= 1 else None
682
696
 
683
697
 
698
+ _STORY_LIST_ITEM_RE = re.compile(r"^\s*(?:\d+[.)]|[-*•])\s+(.+?)\s*$")
699
+
700
+
701
+ def _extract_story_titles(text: str | None) -> list[str]:
702
+ """Titles of the user stories the epic ENUMERATES under a 'User Stories' heading.
703
+
704
+ When the epic already lists its stories ("1. Login — …", "- Logout"), those are
705
+ the authoritative stories — the bot must reproduce EXACTLY them (same count, same
706
+ titles), not invent its own. Returns the list of titles (the part before an
707
+ em/en-dash or colon separator), or [] when the epic gives no explicit list.
708
+ """
709
+ if not text:
710
+ return []
711
+ lines = text.splitlines()
712
+ start = None
713
+ for i, ln in enumerate(lines):
714
+ if re.match(r"\s*#{0,6}\s*user\s+stor(?:y|ies)\b\s*:?\s*$", ln, re.I):
715
+ start = i + 1
716
+ break
717
+ if start is None:
718
+ return []
719
+ titles: list[str] = []
720
+ for ln in lines[start:]:
721
+ if re.match(r"\s*#{1,6}\s+\S", ln): # next heading → section ends
722
+ break
723
+ m = _STORY_LIST_ITEM_RE.match(ln)
724
+ if not m:
725
+ continue
726
+ item = m.group(1).strip()
727
+ title = re.split(r"\s+[—–-]\s+|:\s+", item, maxsplit=1)[0]
728
+ title = title.strip(" *`_").strip()
729
+ if title:
730
+ titles.append(title[:250])
731
+ return titles[:50]
732
+
733
+
684
734
  def _extract_stories_dir(text: str | None) -> str | None:
685
735
  """A directory the instructions ask the user stories to be WRITTEN into, or None.
686
736
 
@@ -876,12 +926,22 @@ async def _handle_epic(
876
926
  ).model_dump())
877
927
 
878
928
  # 1. Generate the decomposition, grounded in the codebase where one exists.
929
+ # Priority for how many stories + their titles:
930
+ # (a) the epic ENUMERATES its stories (a "User Stories" list) → use those
931
+ # exact titles and count, verbatim;
932
+ # (b) else an explicit "build N stories" count → exactly N;
933
+ # (c) else the model decides (bounded by _MAX_EPIC_STORIES).
934
+ story_titles = _extract_story_titles(description) or _extract_story_titles(human)
935
+ story_count = (len(story_titles) if story_titles
936
+ else (_extract_story_count(description) or _extract_story_count(human)))
937
+ limit = story_count or _MAX_EPIC_STORIES
879
938
  try:
880
939
  await graphify.prepare(project_path)
881
940
  except Exception: # noqa: BLE001
882
941
  pass
883
942
  gen = await _run_generation(
884
- _build_epic_prompt(jira_key, summary, description, human),
943
+ _build_epic_prompt(jira_key, summary, description, human,
944
+ story_count=story_count, story_titles=story_titles or None),
885
945
  project_path, send=send, thread_id=thread_id,
886
946
  label=f"Epic {jira_key}: generating user stories",
887
947
  github_token=await client.github_token(),
@@ -924,7 +984,7 @@ async def _handle_epic(
924
984
  await send(Envelope(
925
985
  type="step_update", thread_id=thread_id,
926
986
  data={"jira_task_id": tid, "current_step": f"Epic {jira_key}: marked Done — "
927
- f"creating {min(len(stories), _MAX_EPIC_STORIES)} user Story(ies)"},
987
+ f"creating {min(len(stories), limit)} user Story(ies)"},
928
988
  ).model_dump())
929
989
 
930
990
  # 3. Create a To Do child Task per story, inheriting the Epic's fields.
@@ -933,7 +993,7 @@ async def _handle_epic(
933
993
  # inherited assignee — but ONLY on the CHILD task, never on the parent Epic.
934
994
  desc_email = _extract_email(description) or _extract_email(human)
935
995
  created: list[str] = []
936
- for st in stories[:_MAX_EPIC_STORIES]:
996
+ for st in stories[:limit]:
937
997
  if not isinstance(st, dict) or not (st.get("summary") or "").strip():
938
998
  continue
939
999
  # Precedence: a per-story email the model extracted → an email in the
@@ -969,14 +1029,15 @@ async def _handle_epic(
969
1029
  # the instructions (e.g. "create /home/me/new_tasks and write the user stories
970
1030
  # there"). This is IN ADDITION to the Story tickets. Best-effort — a file-write
971
1031
  # failure never fails the Epic.
972
- stories_dir = _extract_stories_dir(human)
1032
+ # Folder may be named in the humanInstructions OR the description — check both.
1033
+ stories_dir = _extract_stories_dir(human) or _extract_stories_dir(description)
973
1034
  if stories_dir:
974
1035
  from pathlib import Path
975
1036
  try:
976
1037
  d = Path(stories_dir).expanduser()
977
1038
  d.mkdir(parents=True, exist_ok=True) # create from scratch
978
1039
  written = 0
979
- for i, st in enumerate(stories[:_MAX_EPIC_STORIES], 1):
1040
+ for i, st in enumerate(stories[:limit], 1):
980
1041
  if not isinstance(st, dict) or not (st.get("summary") or "").strip():
981
1042
  continue
982
1043
  title = str(st.get("summary", "")).strip()