devops-bot-sdk 1.4.39__tar.gz → 1.4.45__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.39 → devops_bot_sdk-1.4.45}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/ipc/handlers.py +191 -16
  7. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/README.md +0 -0
  8. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/local_exec.py +0 -0
  27. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.39 → devops_bot_sdk-1.4.45}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.39
3
+ Version: 1.4.45
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.39
3
+ Version: 1.4.45
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.39"
7
+ version = "1.4.45"
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.39
3
+ Version: 1.4.45
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.39"
33
+ __version__ = "1.4.45"
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.39"
61
+ SDK_VERSION = "1.4.45"
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
@@ -393,12 +393,16 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
393
393
  ' "humanInstructions": "Operator instructions to implement THIS story. MUST '
394
394
  "repeat the same project location/path and constraints from the epic so the "
395
395
  'coding run works in the right place.",\n'
396
+ ' "assigneeEmail": "dev@company.com",\n'
396
397
  ' "points": 5\n'
397
398
  " }\n"
398
399
  " ]\n"
399
400
  "}\n"
400
401
  "Create as many stories as the epic genuinely needs (do not pad or truncate "
401
- "artificially); each story must be independently implementable."
402
+ "artificially); each story must be independently implementable.\n"
403
+ "assigneeEmail is OPTIONAL per story: set it ONLY when the epic explicitly "
404
+ "assigns that specific work to a named person/email (e.g. 'assign the API to "
405
+ "alice@x.com'); otherwise omit it."
402
406
  )
403
407
  return "\n\n".join(parts)
404
408
 
@@ -423,6 +427,17 @@ def _extract_json_block(text: str | None) -> dict | None:
423
427
  return None
424
428
 
425
429
 
430
+ _EMAIL_RE = re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}")
431
+
432
+
433
+ def _extract_email(text: str | None) -> str | None:
434
+ """First email address mentioned in `text`, lower-cased, or None."""
435
+ if not text:
436
+ return None
437
+ m = _EMAIL_RE.search(text)
438
+ return m.group(0).strip().lower() if m else None
439
+
440
+
426
441
  def _epic_inherited_fields(task: dict) -> dict:
427
442
  """Parent-Epic fields a child Task should inherit (drops empties)."""
428
443
  def _date(v: object) -> str | None:
@@ -502,24 +517,34 @@ async def _handle_epic(
502
517
  await send(Envelope(
503
518
  type="step_update", thread_id=thread_id,
504
519
  data={"jira_task_id": tid, "current_step": f"Epic {jira_key}: marked Done — "
505
- f"creating {min(len(stories), _MAX_EPIC_STORIES)} child task(s)"},
520
+ f"creating {min(len(stories), _MAX_EPIC_STORIES)} user Story(ies)"},
506
521
  ).model_dump())
507
522
 
508
523
  # 3. Create a To Do child Task per story, inheriting the Epic's fields.
509
524
  inherited = _epic_inherited_fields(task)
525
+ # An assignee email named in the EPIC's description/instructions overrides the
526
+ # inherited assignee — but ONLY on the CHILD task, never on the parent Epic.
527
+ desc_email = _extract_email(description) or _extract_email(human)
510
528
  created: list[str] = []
511
529
  for st in stories[:_MAX_EPIC_STORIES]:
512
530
  if not isinstance(st, dict) or not (st.get("summary") or "").strip():
513
531
  continue
532
+ # Precedence: a per-story email the model extracted → an email in the
533
+ # epic description/instructions → the inherited parent assignee.
534
+ story_email = _extract_email(str(st.get("assigneeEmail") or ""))
535
+ child_email = story_email or desc_email or inherited.get("assigneeEmail")
514
536
  fields = {
515
537
  **inherited,
516
- "type": "Task",
538
+ "type": "Story", # Epic decomposes into user Stories; a Story later
539
+ # becomes a Task (see _handle_story) when it is run.
517
540
  "status": "To Do",
518
541
  "summary": str(st.get("summary", "")).strip()[:250],
519
542
  "description": str(st.get("description", "") or "")[:20000],
520
543
  "humanInstructions": str(st.get("humanInstructions") or human or "")[:20000] or None,
521
544
  "parentTaskId": tid,
522
545
  }
546
+ if child_email:
547
+ fields["assigneeEmail"] = child_email
523
548
  if st.get("points"):
524
549
  fields["points"] = st["points"]
525
550
  new_id = await client.create_task_ml(fields, model_used=_EPIC_MODELS)
@@ -531,13 +556,141 @@ async def _handle_epic(
531
556
  ).model_dump())
532
557
 
533
558
  await client.log_activity(
534
- f"Epic {jira_key}: Done — created {len(created)}/{len(stories)} child To Do task(s)"
559
+ f"Epic {jira_key}: Done — created {len(created)}/{len(stories)} To Do user Story(ies)"
535
560
  )
536
561
  await send(Envelope(
537
562
  type="step_update", thread_id=thread_id,
538
563
  data={"jira_task_id": tid,
539
- "current_step": f"Epic {jira_key} complete — {len(created)} child task(s) queued "
540
- "(run again to implement them)"},
564
+ "current_step": f"Epic {jira_key} complete — {len(created)} Story(ies) queued "
565
+ "(run again: each Story → PRD + Task)"},
566
+ ).model_dump())
567
+
568
+
569
+ # ── Story → Task (with optional PRD) ───────────────────────────────────────
570
+ # A Story (produced by decomposing an Epic) is not coded directly. When it is
571
+ # run: if a PRD is asked for in the instructions, generate one and fold it into
572
+ # the description alongside the user story; then convert the ticket to a Task
573
+ # (kept To Do) so the next run implements it through the normal pipeline.
574
+ _PRD_RE = re.compile(r"\bprd\b|product\s+requirements?\s+document", re.I)
575
+ # Section header the generated PRD is filed under in the description. Also the
576
+ # idempotency marker: if it's already present we neither regenerate nor re-write.
577
+ _PRD_MARKER = "# PRD — Product Requirements Document"
578
+
579
+
580
+ def _wants_prd(text: str | None) -> bool:
581
+ """True when the text asks for a PRD / product-requirements document."""
582
+ return bool(text and _PRD_RE.search(text))
583
+
584
+
585
+ def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
586
+ human_instructions: str | None) -> str:
587
+ """Read-only prompt that produces a PRD (Markdown) for a user story."""
588
+ parts: list[str] = [
589
+ "Write a PRD (Product Requirements Document) for the user story below. This is "
590
+ "ANALYSIS ONLY — do NOT create, edit, or delete any file.",
591
+ f"=== USER STORY {jira_key} ===\nSummary: {summary}",
592
+ ]
593
+ if user_story and user_story.strip():
594
+ parts.append("User story / description:\n" + _clip_text(user_story.strip()))
595
+ if human_instructions and human_instructions.strip():
596
+ parts.append("Operator instructions:\n" + human_instructions.strip())
597
+ parts.append(
598
+ "Explore the codebase (CLAUDE.md, graphify, Read/Glob/Grep) to ground the PRD in "
599
+ "the REAL system. Output ONLY the PRD as Markdown — no preamble, no code edits — "
600
+ "with sections: Overview, Goals & Success Metrics, User Stories, Functional "
601
+ "Requirements, Non-Functional Requirements, Acceptance Criteria, Out of Scope, "
602
+ "Dependencies & Risks."
603
+ )
604
+ return "\n\n".join(parts)
605
+
606
+
607
+ async def _handle_story(
608
+ client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
609
+ ) -> None:
610
+ """Run a Story: optionally generate a PRD (when instructed) into the description,
611
+ then convert the ticket to a Task (To Do) for the coding run.
612
+
613
+ Best-effort. If a PRD was explicitly instructed but generation fails, the ticket
614
+ goes to Backlog for retry (so the PRD requirement isn't silently dropped); a
615
+ conversion failure likewise backlogs it.
616
+ """
617
+ from sdk import local_exec, graphify
618
+
619
+ tid = str(task.get("id", ""))
620
+ jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "") or "STORY"
621
+ summary = task.get("summary", "") or ""
622
+ user_story = task.get("description") or ""
623
+ human = task.get("humanInstructions")
624
+ wants_prd = _wants_prd(human) or _wants_prd(user_story)
625
+
626
+ await send(Envelope(
627
+ type="step_update", thread_id=thread_id,
628
+ data={"jira_task_id": tid, "task_id": tid,
629
+ "current_step": f"Story {jira_key}: "
630
+ + ("generating PRD, then " if wants_prd else "")
631
+ + "converting to Task"},
632
+ ).model_dump())
633
+
634
+ # Idempotent: if a PRD is already in the description, don't regenerate it or
635
+ # re-write the description — only the type conversion (if still needed) runs.
636
+ already_has_prd = _PRD_MARKER in (user_story or "")
637
+ new_description: str | None = None
638
+ if wants_prd and already_has_prd:
639
+ await send(Envelope(
640
+ type="step_update", thread_id=thread_id,
641
+ data={"jira_task_id": tid,
642
+ "current_step": f"Story {jira_key}: PRD already present — skipping regeneration"},
643
+ ).model_dump())
644
+ elif wants_prd:
645
+ project_path = _resolve_local_project_path(task, payload)
646
+ try:
647
+ await graphify.prepare(project_path)
648
+ except Exception: # noqa: BLE001
649
+ pass
650
+ gen = await local_exec.run_claude_local(
651
+ _build_prd_prompt(jira_key, summary, user_story, human),
652
+ project_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only analysis
653
+ github_token=await client.github_token(),
654
+ )
655
+ prd = (gen.get("result") or "").strip() if gen.get("ok") else ""
656
+ if not prd:
657
+ reason = (gen.get("error") or "PRD generation produced no content")[:200]
658
+ # Leave the Story To Do (never mark it In Progress/Done/Backlog) so a
659
+ # retry or the developer can pick it up.
660
+ await client.log_activity(f"Story {jira_key}: PRD generation failed — {reason}")
661
+ await send(Envelope(
662
+ type=Envelope.TYPE_ERROR, thread_id=thread_id,
663
+ data={"code": "STORY_PRD_FAILED", "task_id": tid, "message": reason},
664
+ ).model_dump())
665
+ return
666
+ # PRD lives alongside the user story in the (now Task) description.
667
+ new_description = f"{user_story}\n\n---\n\n{_PRD_MARKER}\n\n{prd}"[:20000]
668
+
669
+ # Convert Story → Task on the SAME ticket; leave it To Do so the next run codes
670
+ # it. humanInstructions are untouched (they still carry the location + any PRD
671
+ # directive, so the coding run can also write the PRD.md file if asked).
672
+ update: dict = {"type": "Task"}
673
+ if new_description:
674
+ update["description"] = new_description
675
+ if not await client.update_task_ml(tid, update, model_used=_EPIC_MODELS):
676
+ # Leave the Story To Do (never In Progress/Done/Backlog) on failure.
677
+ await client.log_activity(f"Story {jira_key}: convert-to-Task failed")
678
+ await send(Envelope(
679
+ type=Envelope.TYPE_ERROR, thread_id=thread_id,
680
+ data={"code": "STORY_CONVERT_FAILED", "task_id": tid,
681
+ "message": "could not convert Story to Task"},
682
+ ).model_dump())
683
+ return
684
+
685
+ await client.log_activity(
686
+ f"Story {jira_key}: converted to Task{' with PRD' if wants_prd else ''} (To Do)"
687
+ )
688
+ await send(Envelope(
689
+ type="step_update", thread_id=thread_id,
690
+ data={"jira_task_id": tid,
691
+ "current_step": f"Story {jira_key} → Task (To Do)"
692
+ + (" + PRD in description" if wants_prd else "")
693
+ + " — run again to implement"},
541
694
  ).model_dump())
542
695
 
543
696
 
@@ -2372,6 +2525,22 @@ async def handle_orchestrate_auto_local_run(
2372
2525
  jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "")
2373
2526
  agent_mode = bool(task.get("agentMode", True)) # default True (fail-safe)
2374
2527
  project_path = _resolve_local_project_path(task, payload)
2528
+ ttype = str(task.get("type") or "").strip().lower()
2529
+
2530
+ # User STORIES are for the ASSIGNED DEVELOPERS, not the bot: the default
2531
+ # auto-loop skips Story-type tickets and never marks them In Progress or
2532
+ # Done. (An explicit target — `agentos-auto ticket <KEY>`, i.e. only_keys
2533
+ # set — still lets the bot act on a Story: generate its PRD and convert
2534
+ # it to a Task.) Once a ticket is a TASK it is coded normally
2535
+ # (In Progress → Done), even when it descended from an Epic/Story.
2536
+ if ttype == "story" and not only_keys:
2537
+ await send(Envelope(
2538
+ type="step_update", thread_id=thread_id,
2539
+ data={"jira_task_id": tid,
2540
+ "current_step": f"({idx}/{total}) {jira_key}: skipped — user Story "
2541
+ f"left for the assigned developer"},
2542
+ ).model_dump())
2543
+ continue
2375
2544
 
2376
2545
  await send(Envelope(
2377
2546
  type="step_update", thread_id=thread_id,
@@ -2379,23 +2548,29 @@ async def handle_orchestrate_auto_local_run(
2379
2548
  "project_path": project_path, "jira_task_id": tid, "agent_mode": agent_mode},
2380
2549
  ).model_dump())
2381
2550
 
2382
- # EPIC decompose (generate user stories, mark Done, create To Do
2383
- # child tasks) instead of coding it directly. Isolated like every other
2384
- # task: a failure here must not abort the remaining queue.
2385
- if str(task.get("type") or "").strip().lower() == "epic":
2551
+ # Staged decomposition an EPIC and a STORY are transformed, not coded:
2552
+ # Epic → decompose into user Stories (To Do), mark Epic Done
2553
+ # Story (optional PRD) + convert to Task (To Do) [only when targeted]
2554
+ # Task → implemented by the normal pipeline below
2555
+ # Each is isolated: a failure must not abort the remaining queue.
2556
+ if ttype in ("epic", "story"):
2557
+ handler = _handle_epic if ttype == "epic" else _handle_story
2558
+ code = "EPIC_DECOMPOSE_FAILED" if ttype == "epic" else "STORY_HANDLE_FAILED"
2386
2559
  try:
2387
- await _handle_epic(client, task, payload, send, thread_id)
2560
+ await handler(client, task, payload, send, thread_id)
2388
2561
  except Exception as exc: # noqa: BLE001
2389
- logger.warning("auto_local.epic_failed task=%s err=%s", tid, exc)
2562
+ logger.warning("auto_local.%s_failed task=%s err=%s", ttype, tid, exc)
2390
2563
  try:
2391
- await client.log_activity(f"Task {tid}: epic decomposition errored — {str(exc)[:200]}")
2392
- await client.move_to_backlog(tid)
2564
+ await client.log_activity(f"Task {tid}: {ttype} handling errored — {str(exc)[:200]}")
2565
+ # Backlog only Epics for retry; a Story is left To Do (never
2566
+ # marked In Progress/Done/Backlog) so developers keep it.
2567
+ if ttype == "epic":
2568
+ await client.move_to_backlog(tid)
2393
2569
  except Exception: # noqa: BLE001
2394
2570
  pass
2395
2571
  await send(Envelope(
2396
2572
  type=Envelope.TYPE_ERROR, thread_id=thread_id,
2397
- data={"code": "EPIC_DECOMPOSE_FAILED", "task_id": tid,
2398
- "message": str(exc)[:200]},
2573
+ data={"code": code, "task_id": tid, "message": str(exc)[:200]},
2399
2574
  ).model_dump())
2400
2575
  processed += 1
2401
2576
  continue