devops-bot-sdk 1.4.83__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.83 → devops_bot_sdk-1.4.85}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/ipc/handlers.py +157 -89
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/README.md +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.85}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.83 → 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:
|
|
@@ -545,12 +550,18 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
|
545
550
|
"in that story's Notes — do NOT fabricate a solution.\n"
|
|
546
551
|
"- Prefer fewer, well-grounded stories over many speculative ones. Every story "
|
|
547
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"
|
|
548
559
|
"- This epic may be for ANY domain (software, Finance, HR, operations, …). Match "
|
|
549
560
|
"the stories to the epic's actual domain; do not assume it is software.",
|
|
550
561
|
f"=== EPIC {jira_key} ===\nSummary: {summary}",
|
|
551
562
|
]
|
|
552
563
|
if description and description.strip():
|
|
553
|
-
parts.append("Description:\n" + _clip_text(description.strip()))
|
|
564
|
+
parts.append("Description:\n" + _clip_text(description.strip(), _GEN_MAX_CHARS))
|
|
554
565
|
if human_instructions and human_instructions.strip():
|
|
555
566
|
parts.append(
|
|
556
567
|
"Operator instructions (authoritative — carry any project location / "
|
|
@@ -770,7 +781,7 @@ def _build_validation_prompt(kind: str, jira_key: str, summary: str,
|
|
|
770
781
|
"the required structure or information is not strictly present, REJECT it — a "
|
|
771
782
|
"vague or under-specified ticket must NOT pass.",
|
|
772
783
|
f"=== {k} {jira_key} ===\nSummary: {summary or '(none)'}",
|
|
773
|
-
"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)"),
|
|
774
785
|
"Human Instructions:\n" + (human.strip() if human and human.strip() else "(none provided)"),
|
|
775
786
|
]
|
|
776
787
|
is_epic = kind == "epic"
|
|
@@ -899,11 +910,62 @@ async def _validate_ticket_or_reject(
|
|
|
899
910
|
return False
|
|
900
911
|
|
|
901
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
|
+
|
|
902
963
|
async def _handle_epic(
|
|
903
964
|
client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
|
|
904
965
|
) -> None:
|
|
905
|
-
"""Decompose an Epic
|
|
906
|
-
|
|
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.
|
|
907
969
|
|
|
908
970
|
Child tasks are created in "To Do"; a later `agentos-auto` run picks them up and
|
|
909
971
|
implements them through the normal grounded pipeline. Best-effort throughout —
|
|
@@ -925,47 +987,51 @@ async def _handle_epic(
|
|
|
925
987
|
"current_step": f"Epic {jira_key}: analysing & generating user stories"},
|
|
926
988
|
).model_dump())
|
|
927
989
|
|
|
928
|
-
#
|
|
929
|
-
#
|
|
930
|
-
#
|
|
931
|
-
#
|
|
932
|
-
# (b) else an explicit "build N stories" count → exactly N;
|
|
933
|
-
# (c) else the model decides (bounded by _MAX_EPIC_STORIES).
|
|
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).
|
|
934
994
|
story_titles = _extract_story_titles(description) or _extract_story_titles(human)
|
|
935
995
|
story_count = (len(story_titles) if story_titles
|
|
936
996
|
else (_extract_story_count(description) or _extract_story_count(human)))
|
|
937
997
|
limit = story_count or _MAX_EPIC_STORIES
|
|
998
|
+
gh_token = await client.github_token()
|
|
938
999
|
try:
|
|
939
1000
|
await graphify.prepare(project_path)
|
|
940
1001
|
except Exception: # noqa: BLE001
|
|
941
1002
|
pass
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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),
|
|
945
1008
|
project_path, send=send, thread_id=thread_id,
|
|
946
|
-
label=f"Epic {jira_key}:
|
|
947
|
-
github_token=
|
|
1009
|
+
label=f"Epic {jira_key}: planning & listing stories",
|
|
1010
|
+
github_token=gh_token,
|
|
948
1011
|
)
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
# run is flagged is_error (e.g. a stray read-only tool denial during exploration),
|
|
952
|
-
# and the result then lands in `error`. Don't discard that work over an is_error flag.
|
|
953
|
-
if gen.get("limit_exceeded"):
|
|
954
|
-
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")
|
|
955
1014
|
await client.move_to_backlog(tid)
|
|
956
1015
|
await send(Envelope(
|
|
957
1016
|
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
958
1017
|
data={"code": "EPIC_DECOMPOSE_LIMIT", "task_id": tid,
|
|
959
|
-
"message": "Claude usage limit reached
|
|
1018
|
+
"message": "Claude usage limit reached while planning the epic"},
|
|
960
1019
|
).model_dump())
|
|
961
1020
|
return
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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"
|
|
969
1035
|
await client.log_activity(f"Epic {jira_key}: decomposition failed — {reason}")
|
|
970
1036
|
await client.move_to_backlog(tid)
|
|
971
1037
|
await send(Envelope(
|
|
@@ -974,98 +1040,100 @@ async def _handle_epic(
|
|
|
974
1040
|
).model_dump())
|
|
975
1041
|
return
|
|
976
1042
|
|
|
977
|
-
#
|
|
978
|
-
|
|
979
|
-
# plan are recorded.
|
|
980
|
-
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]
|
|
981
1045
|
await client.update_task_ml(
|
|
982
1046
|
tid, {"status": "Done", "initialAiPlan": plan}, model_used=_EPIC_MODELS,
|
|
983
1047
|
)
|
|
1048
|
+
total = len(specs)
|
|
984
1049
|
await send(Envelope(
|
|
985
1050
|
type="step_update", thread_id=thread_id,
|
|
986
|
-
data={"jira_task_id": tid,
|
|
987
|
-
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"},
|
|
988
1054
|
).model_dump())
|
|
989
1055
|
|
|
990
|
-
#
|
|
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) ──
|
|
991
1068
|
inherited = _epic_inherited_fields(task)
|
|
992
|
-
# An assignee email named in the EPIC's description/instructions overrides the
|
|
993
|
-
# inherited assignee — but ONLY on the CHILD task, never on the parent Epic.
|
|
994
1069
|
desc_email = _extract_email(description) or _extract_email(human)
|
|
995
1070
|
created: list[str] = []
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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 ""))
|
|
1002
1095
|
child_email = story_email or desc_email or inherited.get("assigneeEmail")
|
|
1003
1096
|
fields = {
|
|
1004
1097
|
**inherited,
|
|
1005
|
-
"type": "Story",
|
|
1006
|
-
|
|
1007
|
-
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
1098
|
+
"type": "Story",
|
|
1099
|
+
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
1008
1100
|
"status": "To Do",
|
|
1009
|
-
"summary":
|
|
1010
|
-
"description":
|
|
1011
|
-
#
|
|
1012
|
-
# artifacts; implementation instructions (repos/branch/etc.) are added
|
|
1013
|
-
# 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.
|
|
1014
1104
|
"parentTaskId": tid,
|
|
1015
1105
|
}
|
|
1016
1106
|
if child_email:
|
|
1017
1107
|
fields["assigneeEmail"] = child_email
|
|
1018
|
-
if
|
|
1019
|
-
fields["points"] =
|
|
1108
|
+
if spec.get("points"):
|
|
1109
|
+
fields["points"] = spec["points"]
|
|
1020
1110
|
new_id = await client.create_task_ml(fields, model_used=_EPIC_MODELS)
|
|
1021
1111
|
if new_id:
|
|
1022
1112
|
created.append(new_id)
|
|
1023
1113
|
await send(Envelope(
|
|
1024
1114
|
type="step_update", thread_id=thread_id,
|
|
1025
|
-
data={"current_step": f" ↳ created To Do
|
|
1115
|
+
data={"current_step": f" ↳ created To Do story {i}/{total}: {title}"},
|
|
1026
1116
|
).model_dump())
|
|
1027
1117
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
# failure never fails the Epic.
|
|
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)
|
|
1034
|
-
if stories_dir:
|
|
1035
|
-
from pathlib import Path
|
|
1036
|
-
try:
|
|
1037
|
-
d = Path(stories_dir).expanduser()
|
|
1038
|
-
d.mkdir(parents=True, exist_ok=True) # create from scratch
|
|
1039
|
-
written = 0
|
|
1040
|
-
for i, st in enumerate(stories[:limit], 1):
|
|
1041
|
-
if not isinstance(st, dict) or not (st.get("summary") or "").strip():
|
|
1042
|
-
continue
|
|
1043
|
-
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:
|
|
1044
1121
|
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")[:50] or f"story-{i}"
|
|
1045
|
-
|
|
1046
|
-
|
|
1122
|
+
(dir_path / f"{i:02d}-{slug}.md").write_text(
|
|
1123
|
+
f"# {title}\n\n{story_desc}\n", encoding="utf-8")
|
|
1047
1124
|
written += 1
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
type="step_update", thread_id=thread_id,
|
|
1051
|
-
data={"jira_task_id": tid,
|
|
1052
|
-
"current_step": f"Epic {jira_key}: wrote {written} user-story file(s) to {stories_dir}"},
|
|
1053
|
-
).model_dump())
|
|
1054
|
-
except Exception as exc: # noqa: BLE001
|
|
1055
|
-
await send(Envelope(
|
|
1056
|
-
type="step_update", thread_id=thread_id,
|
|
1057
|
-
data={"jira_task_id": tid,
|
|
1058
|
-
"current_step": f"Epic {jira_key}: could not write story files to {stories_dir} — {str(exc)[:120]}"},
|
|
1059
|
-
).model_dump())
|
|
1125
|
+
except Exception: # noqa: BLE001
|
|
1126
|
+
pass
|
|
1060
1127
|
|
|
1128
|
+
tail = f"; wrote {written} .md file(s) to {story_dir}" if dir_path is not None else ""
|
|
1061
1129
|
await client.log_activity(
|
|
1062
|
-
f"Epic {jira_key}: Done — created {len(created)}/{
|
|
1130
|
+
f"Epic {jira_key}: Done — created {len(created)}/{total} To Do user Story(ies){tail}"
|
|
1063
1131
|
)
|
|
1064
1132
|
await send(Envelope(
|
|
1065
1133
|
type="step_update", thread_id=thread_id,
|
|
1066
1134
|
data={"jira_task_id": tid,
|
|
1067
|
-
"current_step": f"Epic {jira_key} complete — {len(created)} Story(ies) queued
|
|
1068
|
-
"(
|
|
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 "")},
|
|
1069
1137
|
).model_dump())
|
|
1070
1138
|
|
|
1071
1139
|
|
|
@@ -1116,7 +1184,7 @@ def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
|
|
|
1116
1184
|
f"=== USER STORY {jira_key} ===\nSummary: {summary}",
|
|
1117
1185
|
]
|
|
1118
1186
|
if user_story and user_story.strip():
|
|
1119
|
-
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))
|
|
1120
1188
|
if human_instructions and human_instructions.strip():
|
|
1121
1189
|
parts.append("Operator instructions:\n" + human_instructions.strip())
|
|
1122
1190
|
parts.append(
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.83 → 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
|