devops-bot-sdk 1.4.81__tar.gz → 1.4.85__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.
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/ipc/handlers.py +213 -91
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/README.md +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.85"
|
|
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.
|
|
3
|
+
Version: 1.4.85
|
|
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.
|
|
33
|
+
__version__ = "1.4.85"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -128,6 +128,11 @@ _PLAN_DEADLINE_S = float(os.getenv("AGENTOS_PLAN_DEADLINE_S", "") or 180.0)
|
|
|
128
128
|
# plan (the "follows my instructions sometimes, not others" failure mode). ~6k
|
|
129
129
|
# tokens at ~4 chars/token.
|
|
130
130
|
_DESC_MAX_CHARS = 24_000
|
|
131
|
+
# Analysis/generation prompts (epic decomposition, PRD, validation) must see the
|
|
132
|
+
# WHOLE epic/story, not a middle-elided slice — dropping requirements is what makes
|
|
133
|
+
# the model hallucinate to fill the gap on large tickets. Claude has a large context
|
|
134
|
+
# window, so give these a generous budget (~30k tokens). Env-tunable.
|
|
135
|
+
_GEN_MAX_CHARS = int(os.getenv("AGENTOS_GEN_MAX_CHARS", "") or 120_000)
|
|
131
136
|
|
|
132
137
|
|
|
133
138
|
def _clip_text(text: str, max_chars: int = _DESC_MAX_CHARS) -> str:
|
|
@@ -531,7 +536,8 @@ _PRD_FORMAT = (
|
|
|
531
536
|
|
|
532
537
|
|
|
533
538
|
def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
534
|
-
human_instructions: str | None, story_count: int | None = None
|
|
539
|
+
human_instructions: str | None, story_count: int | None = None,
|
|
540
|
+
story_titles: list[str] | None = None) -> str:
|
|
535
541
|
"""Read-only prompt that turns an Epic into a decomposition JSON."""
|
|
536
542
|
parts: list[str] = [
|
|
537
543
|
"You are decomposing a JIRA EPIC into implementable user stories. This is "
|
|
@@ -544,12 +550,18 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
|
544
550
|
"in that story's Notes — do NOT fabricate a solution.\n"
|
|
545
551
|
"- Prefer fewer, well-grounded stories over many speculative ones. Every story "
|
|
546
552
|
"must trace back to something in the epic.\n"
|
|
553
|
+
"- The epic may be LONG: read ALL of it and cover every stated requirement across "
|
|
554
|
+
"the stories — do not drop, summarise away, or skip parts, and do not pad with "
|
|
555
|
+
"invented scope to look complete. If you cannot ground a piece, note it as an open "
|
|
556
|
+
"question instead of fabricating.\n"
|
|
557
|
+
"- Before finalising, self-check: does every story trace to the epic, and do the "
|
|
558
|
+
"stories together cover the epic's scope? Drop any story you cannot ground.\n"
|
|
547
559
|
"- This epic may be for ANY domain (software, Finance, HR, operations, …). Match "
|
|
548
560
|
"the stories to the epic's actual domain; do not assume it is software.",
|
|
549
561
|
f"=== EPIC {jira_key} ===\nSummary: {summary}",
|
|
550
562
|
]
|
|
551
563
|
if description and description.strip():
|
|
552
|
-
parts.append("Description:\n" + _clip_text(description.strip()))
|
|
564
|
+
parts.append("Description:\n" + _clip_text(description.strip(), _GEN_MAX_CHARS))
|
|
553
565
|
if human_instructions and human_instructions.strip():
|
|
554
566
|
parts.append(
|
|
555
567
|
"Operator instructions (authoritative — carry any project location / "
|
|
@@ -578,11 +590,21 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
|
578
590
|
"}\n\n"
|
|
579
591
|
"Do NOT produce humanInstructions for the stories — created stories are clean "
|
|
580
592
|
"user-story artifacts; implementation instructions are added later.\n"
|
|
581
|
-
+ (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
593
|
+
+ (
|
|
594
|
+
# The epic ENUMERATES its stories → reproduce them EXACTLY (verbatim titles).
|
|
595
|
+
("The epic already lists its user stories. Produce EXACTLY these "
|
|
596
|
+
f"{len(story_titles)} stor{'y' if len(story_titles) == 1 else 'ies'}, in this "
|
|
597
|
+
"order, using each title VERBATIM as the story's summary — do NOT invent, "
|
|
598
|
+
"rename, merge, add, drop, or reorder any. Only expand each into the USER "
|
|
599
|
+
"STORY FORMAT for its description:\n"
|
|
600
|
+
+ "\n".join(f" {i}. {t}" for i, t in enumerate(story_titles, 1)) + "\n")
|
|
601
|
+
if story_titles else
|
|
602
|
+
(f"Generate EXACTLY {story_count} user stor{'y' if story_count == 1 else 'ies'} — "
|
|
603
|
+
"the epic explicitly asks for this many; do not produce more or fewer.\n"
|
|
604
|
+
if story_count else
|
|
605
|
+
"Create as many stories as the epic genuinely needs (do not pad or truncate "
|
|
606
|
+
"artificially); each story must be independently implementable.\n")
|
|
607
|
+
)
|
|
586
608
|
+ "assigneeEmail is OPTIONAL per story: set it ONLY when the epic explicitly "
|
|
587
609
|
"assigns that specific work to a named person/email (e.g. 'assign the API to "
|
|
588
610
|
"alice@x.com'); otherwise omit it.\n\n"
|
|
@@ -684,6 +706,42 @@ def _extract_story_count(text: str | None) -> int | None:
|
|
|
684
706
|
return min(n, 50) if n and n >= 1 else None
|
|
685
707
|
|
|
686
708
|
|
|
709
|
+
_STORY_LIST_ITEM_RE = re.compile(r"^\s*(?:\d+[.)]|[-*•])\s+(.+?)\s*$")
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _extract_story_titles(text: str | None) -> list[str]:
|
|
713
|
+
"""Titles of the user stories the epic ENUMERATES under a 'User Stories' heading.
|
|
714
|
+
|
|
715
|
+
When the epic already lists its stories ("1. Login — …", "- Logout"), those are
|
|
716
|
+
the authoritative stories — the bot must reproduce EXACTLY them (same count, same
|
|
717
|
+
titles), not invent its own. Returns the list of titles (the part before an
|
|
718
|
+
em/en-dash or colon separator), or [] when the epic gives no explicit list.
|
|
719
|
+
"""
|
|
720
|
+
if not text:
|
|
721
|
+
return []
|
|
722
|
+
lines = text.splitlines()
|
|
723
|
+
start = None
|
|
724
|
+
for i, ln in enumerate(lines):
|
|
725
|
+
if re.match(r"\s*#{0,6}\s*user\s+stor(?:y|ies)\b\s*:?\s*$", ln, re.I):
|
|
726
|
+
start = i + 1
|
|
727
|
+
break
|
|
728
|
+
if start is None:
|
|
729
|
+
return []
|
|
730
|
+
titles: list[str] = []
|
|
731
|
+
for ln in lines[start:]:
|
|
732
|
+
if re.match(r"\s*#{1,6}\s+\S", ln): # next heading → section ends
|
|
733
|
+
break
|
|
734
|
+
m = _STORY_LIST_ITEM_RE.match(ln)
|
|
735
|
+
if not m:
|
|
736
|
+
continue
|
|
737
|
+
item = m.group(1).strip()
|
|
738
|
+
title = re.split(r"\s+[—–-]\s+|:\s+", item, maxsplit=1)[0]
|
|
739
|
+
title = title.strip(" *`_").strip()
|
|
740
|
+
if title:
|
|
741
|
+
titles.append(title[:250])
|
|
742
|
+
return titles[:50]
|
|
743
|
+
|
|
744
|
+
|
|
687
745
|
def _extract_stories_dir(text: str | None) -> str | None:
|
|
688
746
|
"""A directory the instructions ask the user stories to be WRITTEN into, or None.
|
|
689
747
|
|
|
@@ -723,7 +781,7 @@ def _build_validation_prompt(kind: str, jira_key: str, summary: str,
|
|
|
723
781
|
"the required structure or information is not strictly present, REJECT it — a "
|
|
724
782
|
"vague or under-specified ticket must NOT pass.",
|
|
725
783
|
f"=== {k} {jira_key} ===\nSummary: {summary or '(none)'}",
|
|
726
|
-
"Description:\n" + (_clip_text(description.strip()) if description and description.strip() else "(none provided)"),
|
|
784
|
+
"Description:\n" + (_clip_text(description.strip(), _GEN_MAX_CHARS) if description and description.strip() else "(none provided)"),
|
|
727
785
|
"Human Instructions:\n" + (human.strip() if human and human.strip() else "(none provided)"),
|
|
728
786
|
]
|
|
729
787
|
is_epic = kind == "epic"
|
|
@@ -852,11 +910,62 @@ async def _validate_ticket_or_reject(
|
|
|
852
910
|
return False
|
|
853
911
|
|
|
854
912
|
|
|
913
|
+
def _build_epic_titles_prompt(jira_key: str, summary: str, description: str | None,
|
|
914
|
+
human_instructions: str | None, story_count: int | None = None,
|
|
915
|
+
story_titles: list[str] | None = None) -> str:
|
|
916
|
+
"""Phase-1 prompt: produce the epic PLAN + a small list of story TITLES only (no
|
|
917
|
+
full descriptions). Kept small so the output can't overflow; each story is
|
|
918
|
+
expanded individually afterwards to avoid context pressure / hallucination."""
|
|
919
|
+
parts: list[str] = [
|
|
920
|
+
"You are decomposing a JIRA EPIC into a LIST of user-story TITLES — titles only, "
|
|
921
|
+
"NOT full descriptions yet. This is ANALYSIS ONLY — do NOT create/edit any file.",
|
|
922
|
+
"Faithfulness: derive titles ONLY from the epic; do NOT invent scope. Cover the "
|
|
923
|
+
"epic's WHOLE scope; a few well-grounded titles beat many speculative ones.",
|
|
924
|
+
f"=== EPIC {jira_key} ===\nSummary: {summary}",
|
|
925
|
+
]
|
|
926
|
+
if description and description.strip():
|
|
927
|
+
parts.append("Description:\n" + _clip_text(description.strip(), _GEN_MAX_CHARS))
|
|
928
|
+
if human_instructions and human_instructions.strip():
|
|
929
|
+
parts.append("Operator instructions:\n" + human_instructions.strip())
|
|
930
|
+
parts.append(
|
|
931
|
+
"If this is a software project, briefly inspect the codebase (CLAUDE.md, "
|
|
932
|
+
"graphify, Read/Glob/Grep) so the split fits the real architecture.\n\n"
|
|
933
|
+
+ (("The epic already lists its stories — use EXACTLY these titles, VERBATIM, in "
|
|
934
|
+
"order; do NOT invent, rename, merge, add, drop, or reorder:\n"
|
|
935
|
+
+ "\n".join(f" {i}. {t}" for i, t in enumerate(story_titles, 1)) + "\n")
|
|
936
|
+
if story_titles else
|
|
937
|
+
(f"Produce EXACTLY {story_count} story titles.\n" if story_count else
|
|
938
|
+
"Produce as many story titles as the epic genuinely needs (no padding).\n"))
|
|
939
|
+
+ "\nOutput ONLY a single fenced ```json block, nothing else:\n"
|
|
940
|
+
"{\n"
|
|
941
|
+
' "plan": "an ordered, numbered high-level implementation plan for the epic",\n'
|
|
942
|
+
' "stories": [ {"summary": "short imperative story title", '
|
|
943
|
+
'"assigneeEmail": "only if the epic assigns it to a named person", "points": 5} ]\n'
|
|
944
|
+
"}"
|
|
945
|
+
)
|
|
946
|
+
return "\n\n".join(parts)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _build_one_story_prompt(jira_key: str, title: str, index: int, total: int) -> str:
|
|
950
|
+
"""Phase-2 prompt: expand ONE story's full description, resuming the grounding
|
|
951
|
+
session from the titles pass so the codebase context is retained."""
|
|
952
|
+
return (
|
|
953
|
+
f"Now write user story {index} of {total} for epic {jira_key}.\n"
|
|
954
|
+
f"Story title (use it VERBATIM as the summary): {title}\n\n"
|
|
955
|
+
"Using the epic and the codebase you already explored, write ONLY this ONE "
|
|
956
|
+
"story's description in the USER STORY FORMAT below — grounded and specific, with "
|
|
957
|
+
"no invented scope and nothing about other stories. Output ONLY the Markdown "
|
|
958
|
+
"(no JSON, no preamble, no closing commentary).\n\n"
|
|
959
|
+
"=== USER STORY FORMAT ===\n" + _USER_STORY_FORMAT
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
|
|
855
963
|
async def _handle_epic(
|
|
856
964
|
client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
|
|
857
965
|
) -> None:
|
|
858
|
-
"""Decompose an Epic
|
|
859
|
-
|
|
966
|
+
"""Decompose an Epic ONE STORY AT A TIME: a small plan+titles pass, then expand
|
|
967
|
+
each story individually (resuming the grounding session), creating each To Do
|
|
968
|
+
child Story (and optional .md file) as it is produced.
|
|
860
969
|
|
|
861
970
|
Child tasks are created in "To Do"; a later `agentos-auto` run picks them up and
|
|
862
971
|
implements them through the normal grounded pipeline. Best-effort throughout —
|
|
@@ -878,41 +987,51 @@ async def _handle_epic(
|
|
|
878
987
|
"current_step": f"Epic {jira_key}: analysing & generating user stories"},
|
|
879
988
|
).model_dump())
|
|
880
989
|
|
|
881
|
-
#
|
|
882
|
-
#
|
|
883
|
-
#
|
|
884
|
-
|
|
990
|
+
# How many stories + their titles:
|
|
991
|
+
# (a) the epic ENUMERATES its stories → use those exact titles/count, verbatim;
|
|
992
|
+
# (b) else an explicit "build N stories" count → exactly N;
|
|
993
|
+
# (c) else the model decides (bounded by _MAX_EPIC_STORIES).
|
|
994
|
+
story_titles = _extract_story_titles(description) or _extract_story_titles(human)
|
|
995
|
+
story_count = (len(story_titles) if story_titles
|
|
996
|
+
else (_extract_story_count(description) or _extract_story_count(human)))
|
|
885
997
|
limit = story_count or _MAX_EPIC_STORIES
|
|
998
|
+
gh_token = await client.github_token()
|
|
886
999
|
try:
|
|
887
1000
|
await graphify.prepare(project_path)
|
|
888
1001
|
except Exception: # noqa: BLE001
|
|
889
1002
|
pass
|
|
890
|
-
|
|
891
|
-
|
|
1003
|
+
|
|
1004
|
+
# ── Phase 1: PLAN + story TITLES (one small, grounded pass) ──────────────
|
|
1005
|
+
plan_gen = await _run_generation(
|
|
1006
|
+
_build_epic_titles_prompt(jira_key, summary, description, human,
|
|
1007
|
+
story_count=story_count, story_titles=story_titles or None),
|
|
892
1008
|
project_path, send=send, thread_id=thread_id,
|
|
893
|
-
label=f"Epic {jira_key}:
|
|
894
|
-
github_token=
|
|
1009
|
+
label=f"Epic {jira_key}: planning & listing stories",
|
|
1010
|
+
github_token=gh_token,
|
|
895
1011
|
)
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
# run is flagged is_error (e.g. a stray read-only tool denial during exploration),
|
|
899
|
-
# and the result then lands in `error`. Don't discard that work over an is_error flag.
|
|
900
|
-
if gen.get("limit_exceeded"):
|
|
901
|
-
await client.log_activity(f"Epic {jira_key}: usage limit during decomposition — will retry after reset")
|
|
1012
|
+
if plan_gen.get("limit_exceeded"):
|
|
1013
|
+
await client.log_activity(f"Epic {jira_key}: usage limit while planning — will retry after reset")
|
|
902
1014
|
await client.move_to_backlog(tid)
|
|
903
1015
|
await send(Envelope(
|
|
904
1016
|
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
905
1017
|
data={"code": "EPIC_DECOMPOSE_LIMIT", "task_id": tid,
|
|
906
|
-
"message": "Claude usage limit reached
|
|
1018
|
+
"message": "Claude usage limit reached while planning the epic"},
|
|
907
1019
|
).model_dump())
|
|
908
1020
|
return
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1021
|
+
recon_session = plan_gen.get("session_id")
|
|
1022
|
+
doc = _extract_json_block(plan_gen.get("result") or plan_gen.get("error") or "") or {}
|
|
1023
|
+
specs = doc.get("stories") or []
|
|
1024
|
+
# Enumerated titles are authoritative — align to them (keep any per-story email/points).
|
|
1025
|
+
if story_titles:
|
|
1026
|
+
specs = [
|
|
1027
|
+
{"summary": t,
|
|
1028
|
+
"assigneeEmail": (specs[i].get("assigneeEmail") if i < len(specs) and isinstance(specs[i], dict) else None),
|
|
1029
|
+
"points": (specs[i].get("points") if i < len(specs) and isinstance(specs[i], dict) else None)}
|
|
1030
|
+
for i, t in enumerate(story_titles)
|
|
1031
|
+
]
|
|
1032
|
+
specs = [s for s in specs[:limit] if isinstance(s, dict) and str(s.get("summary", "")).strip()]
|
|
1033
|
+
if not specs:
|
|
1034
|
+
reason = "could not produce a valid story list from the epic"
|
|
916
1035
|
await client.log_activity(f"Epic {jira_key}: decomposition failed — {reason}")
|
|
917
1036
|
await client.move_to_backlog(tid)
|
|
918
1037
|
await send(Envelope(
|
|
@@ -921,97 +1040,100 @@ async def _handle_epic(
|
|
|
921
1040
|
).model_dump())
|
|
922
1041
|
return
|
|
923
1042
|
|
|
924
|
-
#
|
|
925
|
-
|
|
926
|
-
# plan are recorded.
|
|
927
|
-
plan = (doc.get("plan") or "")[:8000]
|
|
1043
|
+
# ── Mark the Epic Done (description untouched; only status + AI plan recorded) ──
|
|
1044
|
+
plan = str(doc.get("plan") or "")[:8000]
|
|
928
1045
|
await client.update_task_ml(
|
|
929
1046
|
tid, {"status": "Done", "initialAiPlan": plan}, model_used=_EPIC_MODELS,
|
|
930
1047
|
)
|
|
1048
|
+
total = len(specs)
|
|
931
1049
|
await send(Envelope(
|
|
932
1050
|
type="step_update", thread_id=thread_id,
|
|
933
|
-
data={"jira_task_id": tid,
|
|
934
|
-
f"
|
|
1051
|
+
data={"jira_task_id": tid,
|
|
1052
|
+
"current_step": f"Epic {jira_key}: marked Done — building {total} user "
|
|
1053
|
+
f"stor{'y' if total == 1 else 'ies'} one by one"},
|
|
935
1054
|
).model_dump())
|
|
936
1055
|
|
|
937
|
-
#
|
|
1056
|
+
# Optional deliverables folder (from humanInstructions OR description).
|
|
1057
|
+
from pathlib import Path
|
|
1058
|
+
story_dir = _extract_stories_dir(human) or _extract_stories_dir(description)
|
|
1059
|
+
dir_path = None
|
|
1060
|
+
if story_dir:
|
|
1061
|
+
try:
|
|
1062
|
+
dir_path = Path(story_dir).expanduser()
|
|
1063
|
+
dir_path.mkdir(parents=True, exist_ok=True) # create from scratch
|
|
1064
|
+
except Exception: # noqa: BLE001
|
|
1065
|
+
dir_path = None
|
|
1066
|
+
|
|
1067
|
+
# ── Phase 2: expand + create EACH story ONE BY ONE (resume the grounding session) ──
|
|
938
1068
|
inherited = _epic_inherited_fields(task)
|
|
939
|
-
# An assignee email named in the EPIC's description/instructions overrides the
|
|
940
|
-
# inherited assignee — but ONLY on the CHILD task, never on the parent Epic.
|
|
941
1069
|
desc_email = _extract_email(description) or _extract_email(human)
|
|
942
1070
|
created: list[str] = []
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1071
|
+
written = 0
|
|
1072
|
+
for i, spec in enumerate(specs, 1):
|
|
1073
|
+
title = str(spec.get("summary", "")).strip()[:250]
|
|
1074
|
+
await send(Envelope(
|
|
1075
|
+
type="step_update", thread_id=thread_id,
|
|
1076
|
+
data={"jira_task_id": tid,
|
|
1077
|
+
"current_step": f"Epic {jira_key}: story {i}/{total} — {title}"},
|
|
1078
|
+
).model_dump())
|
|
1079
|
+
one = await _run_generation(
|
|
1080
|
+
_build_one_story_prompt(jira_key, title, i, total),
|
|
1081
|
+
project_path, send=send, thread_id=thread_id,
|
|
1082
|
+
label=f"Epic {jira_key}: story {i}/{total}",
|
|
1083
|
+
github_token=gh_token, resume_session_id=recon_session,
|
|
1084
|
+
)
|
|
1085
|
+
recon_session = one.get("session_id") or recon_session
|
|
1086
|
+
if one.get("limit_exceeded"):
|
|
1087
|
+
await client.log_activity(
|
|
1088
|
+
f"Epic {jira_key}: usage limit after {len(created)}/{total} stories — "
|
|
1089
|
+
"run again to continue the rest"
|
|
1090
|
+
)
|
|
1091
|
+
break
|
|
1092
|
+
story_desc = (one.get("result") or one.get("error") or "").strip() or f"## User Story\n{title}"
|
|
1093
|
+
|
|
1094
|
+
story_email = _extract_email(str(spec.get("assigneeEmail") or ""))
|
|
949
1095
|
child_email = story_email or desc_email or inherited.get("assigneeEmail")
|
|
950
1096
|
fields = {
|
|
951
1097
|
**inherited,
|
|
952
|
-
"type": "Story",
|
|
953
|
-
|
|
954
|
-
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
1098
|
+
"type": "Story",
|
|
1099
|
+
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
955
1100
|
"status": "To Do",
|
|
956
|
-
"summary":
|
|
957
|
-
"description":
|
|
958
|
-
#
|
|
959
|
-
# artifacts; implementation instructions (repos/branch/etc.) are added
|
|
960
|
-
# later at the story/task stage, not baked in by the Epic.
|
|
1101
|
+
"summary": title,
|
|
1102
|
+
"description": story_desc[:20000],
|
|
1103
|
+
# No humanInstructions on AI-created stories — added later at the task stage.
|
|
961
1104
|
"parentTaskId": tid,
|
|
962
1105
|
}
|
|
963
1106
|
if child_email:
|
|
964
1107
|
fields["assigneeEmail"] = child_email
|
|
965
|
-
if
|
|
966
|
-
fields["points"] =
|
|
1108
|
+
if spec.get("points"):
|
|
1109
|
+
fields["points"] = spec["points"]
|
|
967
1110
|
new_id = await client.create_task_ml(fields, model_used=_EPIC_MODELS)
|
|
968
1111
|
if new_id:
|
|
969
1112
|
created.append(new_id)
|
|
970
1113
|
await send(Envelope(
|
|
971
1114
|
type="step_update", thread_id=thread_id,
|
|
972
|
-
data={"current_step": f" ↳ created To Do
|
|
1115
|
+
data={"current_step": f" ↳ created To Do story {i}/{total}: {title}"},
|
|
973
1116
|
).model_dump())
|
|
974
1117
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
# failure never fails the Epic.
|
|
979
|
-
stories_dir = _extract_stories_dir(human)
|
|
980
|
-
if stories_dir:
|
|
981
|
-
from pathlib import Path
|
|
982
|
-
try:
|
|
983
|
-
d = Path(stories_dir).expanduser()
|
|
984
|
-
d.mkdir(parents=True, exist_ok=True) # create from scratch
|
|
985
|
-
written = 0
|
|
986
|
-
for i, st in enumerate(stories[:limit], 1):
|
|
987
|
-
if not isinstance(st, dict) or not (st.get("summary") or "").strip():
|
|
988
|
-
continue
|
|
989
|
-
title = str(st.get("summary", "")).strip()
|
|
1118
|
+
# Write this story's .md immediately (incremental), if a folder was given.
|
|
1119
|
+
if dir_path is not None:
|
|
1120
|
+
try:
|
|
990
1121
|
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")[:50] or f"story-{i}"
|
|
991
|
-
|
|
992
|
-
|
|
1122
|
+
(dir_path / f"{i:02d}-{slug}.md").write_text(
|
|
1123
|
+
f"# {title}\n\n{story_desc}\n", encoding="utf-8")
|
|
993
1124
|
written += 1
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
type="step_update", thread_id=thread_id,
|
|
997
|
-
data={"jira_task_id": tid,
|
|
998
|
-
"current_step": f"Epic {jira_key}: wrote {written} user-story file(s) to {stories_dir}"},
|
|
999
|
-
).model_dump())
|
|
1000
|
-
except Exception as exc: # noqa: BLE001
|
|
1001
|
-
await send(Envelope(
|
|
1002
|
-
type="step_update", thread_id=thread_id,
|
|
1003
|
-
data={"jira_task_id": tid,
|
|
1004
|
-
"current_step": f"Epic {jira_key}: could not write story files to {stories_dir} — {str(exc)[:120]}"},
|
|
1005
|
-
).model_dump())
|
|
1125
|
+
except Exception: # noqa: BLE001
|
|
1126
|
+
pass
|
|
1006
1127
|
|
|
1128
|
+
tail = f"; wrote {written} .md file(s) to {story_dir}" if dir_path is not None else ""
|
|
1007
1129
|
await client.log_activity(
|
|
1008
|
-
f"Epic {jira_key}: Done — created {len(created)}/{
|
|
1130
|
+
f"Epic {jira_key}: Done — created {len(created)}/{total} To Do user Story(ies){tail}"
|
|
1009
1131
|
)
|
|
1010
1132
|
await send(Envelope(
|
|
1011
1133
|
type="step_update", thread_id=thread_id,
|
|
1012
1134
|
data={"jira_task_id": tid,
|
|
1013
|
-
"current_step": f"Epic {jira_key} complete — {len(created)} Story(ies) queued
|
|
1014
|
-
"(
|
|
1135
|
+
"current_step": f"Epic {jira_key} complete — {len(created)} Story(ies) queued"
|
|
1136
|
+
+ (f" · {written} file(s) in {story_dir}" if dir_path is not None else "")},
|
|
1015
1137
|
).model_dump())
|
|
1016
1138
|
|
|
1017
1139
|
|
|
@@ -1062,7 +1184,7 @@ def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
|
|
|
1062
1184
|
f"=== USER STORY {jira_key} ===\nSummary: {summary}",
|
|
1063
1185
|
]
|
|
1064
1186
|
if user_story and user_story.strip():
|
|
1065
|
-
parts.append("User story / description:\n" + _clip_text(user_story.strip()))
|
|
1187
|
+
parts.append("User story / description:\n" + _clip_text(user_story.strip(), _GEN_MAX_CHARS))
|
|
1066
1188
|
if human_instructions and human_instructions.strip():
|
|
1067
1189
|
parts.append("Operator instructions:\n" + human_instructions.strip())
|
|
1068
1190
|
parts.append(
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.81 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|