devops-bot-sdk 1.4.83__tar.gz → 1.4.86__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.86}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/ipc/handlers.py +162 -89
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/README.md +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/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.86"
|
|
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.86
|
|
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.86"
|
|
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,67 @@ 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 (critical): derive the stories STRICTLY from the epic's own "
|
|
923
|
+
"Scope / Requirements / User-Stories content. Do NOT invent, add, or split "
|
|
924
|
+
"stories based on assumptions or on what the codebase happens to contain. Cover "
|
|
925
|
+
"the epic's stated scope and NOTHING beyond it. If the epic is too vague to split "
|
|
926
|
+
"faithfully, produce FEWER stories and note the ambiguity in the plan — never "
|
|
927
|
+
"guess to look complete.",
|
|
928
|
+
f"=== EPIC {jira_key} ===\nSummary: {summary}",
|
|
929
|
+
]
|
|
930
|
+
if description and description.strip():
|
|
931
|
+
parts.append("Description:\n" + _clip_text(description.strip(), _GEN_MAX_CHARS))
|
|
932
|
+
if human_instructions and human_instructions.strip():
|
|
933
|
+
parts.append("Operator instructions:\n" + human_instructions.strip())
|
|
934
|
+
parts.append(
|
|
935
|
+
"You MAY glance at the codebase (CLAUDE.md, graphify, Read/Glob/Grep) ONLY to "
|
|
936
|
+
"phrase a title accurately or confirm a module name — NEVER to introduce scope "
|
|
937
|
+
"the epic does not state. The stories come from the EPIC, not the code.\n\n"
|
|
938
|
+
+ (("The epic already lists its stories — use EXACTLY these titles, VERBATIM, in "
|
|
939
|
+
"order; do NOT invent, rename, merge, add, drop, or reorder:\n"
|
|
940
|
+
+ "\n".join(f" {i}. {t}" for i, t in enumerate(story_titles, 1)) + "\n")
|
|
941
|
+
if story_titles else
|
|
942
|
+
(f"Produce EXACTLY {story_count} story titles.\n" if story_count else
|
|
943
|
+
"Produce as many story titles as the epic genuinely needs (no padding).\n"))
|
|
944
|
+
+ "\nOutput ONLY a single fenced ```json block, nothing else:\n"
|
|
945
|
+
"{\n"
|
|
946
|
+
' "plan": "an ordered, numbered high-level implementation plan for the epic",\n'
|
|
947
|
+
' "stories": [ {"summary": "short imperative story title", '
|
|
948
|
+
'"assigneeEmail": "only if the epic assigns it to a named person", "points": 5} ]\n'
|
|
949
|
+
"}"
|
|
950
|
+
)
|
|
951
|
+
return "\n\n".join(parts)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _build_one_story_prompt(jira_key: str, title: str, index: int, total: int) -> str:
|
|
955
|
+
"""Phase-2 prompt: expand ONE story's full description, resuming the grounding
|
|
956
|
+
session from the titles pass so the codebase context is retained."""
|
|
957
|
+
return (
|
|
958
|
+
f"Now write user story {index} of {total} for epic {jira_key}.\n"
|
|
959
|
+
f"Story title (use it VERBATIM as the summary): {title}\n\n"
|
|
960
|
+
"Using the epic and the codebase you already explored, write ONLY this ONE "
|
|
961
|
+
"story's description in the USER STORY FORMAT below — grounded and specific, with "
|
|
962
|
+
"no invented scope and nothing about other stories. Output ONLY the Markdown "
|
|
963
|
+
"(no JSON, no preamble, no closing commentary).\n\n"
|
|
964
|
+
"=== USER STORY FORMAT ===\n" + _USER_STORY_FORMAT
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
|
|
902
968
|
async def _handle_epic(
|
|
903
969
|
client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
|
|
904
970
|
) -> None:
|
|
905
|
-
"""Decompose an Epic
|
|
906
|
-
|
|
971
|
+
"""Decompose an Epic ONE STORY AT A TIME: a small plan+titles pass, then expand
|
|
972
|
+
each story individually (resuming the grounding session), creating each To Do
|
|
973
|
+
child Story (and optional .md file) as it is produced.
|
|
907
974
|
|
|
908
975
|
Child tasks are created in "To Do"; a later `agentos-auto` run picks them up and
|
|
909
976
|
implements them through the normal grounded pipeline. Best-effort throughout —
|
|
@@ -925,47 +992,51 @@ async def _handle_epic(
|
|
|
925
992
|
"current_step": f"Epic {jira_key}: analysing & generating user stories"},
|
|
926
993
|
).model_dump())
|
|
927
994
|
|
|
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).
|
|
995
|
+
# How many stories + their titles:
|
|
996
|
+
# (a) the epic ENUMERATES its stories → use those exact titles/count, verbatim;
|
|
997
|
+
# (b) else an explicit "build N stories" count → exactly N;
|
|
998
|
+
# (c) else the model decides (bounded by _MAX_EPIC_STORIES).
|
|
934
999
|
story_titles = _extract_story_titles(description) or _extract_story_titles(human)
|
|
935
1000
|
story_count = (len(story_titles) if story_titles
|
|
936
1001
|
else (_extract_story_count(description) or _extract_story_count(human)))
|
|
937
1002
|
limit = story_count or _MAX_EPIC_STORIES
|
|
1003
|
+
gh_token = await client.github_token()
|
|
938
1004
|
try:
|
|
939
1005
|
await graphify.prepare(project_path)
|
|
940
1006
|
except Exception: # noqa: BLE001
|
|
941
1007
|
pass
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1008
|
+
|
|
1009
|
+
# ── Phase 1: PLAN + story TITLES (one small, grounded pass) ──────────────
|
|
1010
|
+
plan_gen = await _run_generation(
|
|
1011
|
+
_build_epic_titles_prompt(jira_key, summary, description, human,
|
|
1012
|
+
story_count=story_count, story_titles=story_titles or None),
|
|
945
1013
|
project_path, send=send, thread_id=thread_id,
|
|
946
|
-
label=f"Epic {jira_key}:
|
|
947
|
-
github_token=
|
|
1014
|
+
label=f"Epic {jira_key}: planning & listing stories",
|
|
1015
|
+
github_token=gh_token,
|
|
948
1016
|
)
|
|
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")
|
|
1017
|
+
if plan_gen.get("limit_exceeded"):
|
|
1018
|
+
await client.log_activity(f"Epic {jira_key}: usage limit while planning — will retry after reset")
|
|
955
1019
|
await client.move_to_backlog(tid)
|
|
956
1020
|
await send(Envelope(
|
|
957
1021
|
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
958
1022
|
data={"code": "EPIC_DECOMPOSE_LIMIT", "task_id": tid,
|
|
959
|
-
"message": "Claude usage limit reached
|
|
1023
|
+
"message": "Claude usage limit reached while planning the epic"},
|
|
960
1024
|
).model_dump())
|
|
961
1025
|
return
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1026
|
+
recon_session = plan_gen.get("session_id")
|
|
1027
|
+
doc = _extract_json_block(plan_gen.get("result") or plan_gen.get("error") or "") or {}
|
|
1028
|
+
specs = doc.get("stories") or []
|
|
1029
|
+
# Enumerated titles are authoritative — align to them (keep any per-story email/points).
|
|
1030
|
+
if story_titles:
|
|
1031
|
+
specs = [
|
|
1032
|
+
{"summary": t,
|
|
1033
|
+
"assigneeEmail": (specs[i].get("assigneeEmail") if i < len(specs) and isinstance(specs[i], dict) else None),
|
|
1034
|
+
"points": (specs[i].get("points") if i < len(specs) and isinstance(specs[i], dict) else None)}
|
|
1035
|
+
for i, t in enumerate(story_titles)
|
|
1036
|
+
]
|
|
1037
|
+
specs = [s for s in specs[:limit] if isinstance(s, dict) and str(s.get("summary", "")).strip()]
|
|
1038
|
+
if not specs:
|
|
1039
|
+
reason = "could not produce a valid story list from the epic"
|
|
969
1040
|
await client.log_activity(f"Epic {jira_key}: decomposition failed — {reason}")
|
|
970
1041
|
await client.move_to_backlog(tid)
|
|
971
1042
|
await send(Envelope(
|
|
@@ -974,98 +1045,100 @@ async def _handle_epic(
|
|
|
974
1045
|
).model_dump())
|
|
975
1046
|
return
|
|
976
1047
|
|
|
977
|
-
#
|
|
978
|
-
|
|
979
|
-
# plan are recorded.
|
|
980
|
-
plan = (doc.get("plan") or "")[:8000]
|
|
1048
|
+
# ── Mark the Epic Done (description untouched; only status + AI plan recorded) ──
|
|
1049
|
+
plan = str(doc.get("plan") or "")[:8000]
|
|
981
1050
|
await client.update_task_ml(
|
|
982
1051
|
tid, {"status": "Done", "initialAiPlan": plan}, model_used=_EPIC_MODELS,
|
|
983
1052
|
)
|
|
1053
|
+
total = len(specs)
|
|
984
1054
|
await send(Envelope(
|
|
985
1055
|
type="step_update", thread_id=thread_id,
|
|
986
|
-
data={"jira_task_id": tid,
|
|
987
|
-
f"
|
|
1056
|
+
data={"jira_task_id": tid,
|
|
1057
|
+
"current_step": f"Epic {jira_key}: marked Done — building {total} user "
|
|
1058
|
+
f"stor{'y' if total == 1 else 'ies'} one by one"},
|
|
988
1059
|
).model_dump())
|
|
989
1060
|
|
|
990
|
-
#
|
|
1061
|
+
# Optional deliverables folder (from humanInstructions OR description).
|
|
1062
|
+
from pathlib import Path
|
|
1063
|
+
story_dir = _extract_stories_dir(human) or _extract_stories_dir(description)
|
|
1064
|
+
dir_path = None
|
|
1065
|
+
if story_dir:
|
|
1066
|
+
try:
|
|
1067
|
+
dir_path = Path(story_dir).expanduser()
|
|
1068
|
+
dir_path.mkdir(parents=True, exist_ok=True) # create from scratch
|
|
1069
|
+
except Exception: # noqa: BLE001
|
|
1070
|
+
dir_path = None
|
|
1071
|
+
|
|
1072
|
+
# ── Phase 2: expand + create EACH story ONE BY ONE (resume the grounding session) ──
|
|
991
1073
|
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
1074
|
desc_email = _extract_email(description) or _extract_email(human)
|
|
995
1075
|
created: list[str] = []
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1076
|
+
written = 0
|
|
1077
|
+
for i, spec in enumerate(specs, 1):
|
|
1078
|
+
title = str(spec.get("summary", "")).strip()[:250]
|
|
1079
|
+
await send(Envelope(
|
|
1080
|
+
type="step_update", thread_id=thread_id,
|
|
1081
|
+
data={"jira_task_id": tid,
|
|
1082
|
+
"current_step": f"Epic {jira_key}: story {i}/{total} — {title}"},
|
|
1083
|
+
).model_dump())
|
|
1084
|
+
one = await _run_generation(
|
|
1085
|
+
_build_one_story_prompt(jira_key, title, i, total),
|
|
1086
|
+
project_path, send=send, thread_id=thread_id,
|
|
1087
|
+
label=f"Epic {jira_key}: story {i}/{total}",
|
|
1088
|
+
github_token=gh_token, resume_session_id=recon_session,
|
|
1089
|
+
)
|
|
1090
|
+
recon_session = one.get("session_id") or recon_session
|
|
1091
|
+
if one.get("limit_exceeded"):
|
|
1092
|
+
await client.log_activity(
|
|
1093
|
+
f"Epic {jira_key}: usage limit after {len(created)}/{total} stories — "
|
|
1094
|
+
"run again to continue the rest"
|
|
1095
|
+
)
|
|
1096
|
+
break
|
|
1097
|
+
story_desc = (one.get("result") or one.get("error") or "").strip() or f"## User Story\n{title}"
|
|
1098
|
+
|
|
1099
|
+
story_email = _extract_email(str(spec.get("assigneeEmail") or ""))
|
|
1002
1100
|
child_email = story_email or desc_email or inherited.get("assigneeEmail")
|
|
1003
1101
|
fields = {
|
|
1004
1102
|
**inherited,
|
|
1005
|
-
"type": "Story",
|
|
1006
|
-
|
|
1007
|
-
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
1103
|
+
"type": "Story",
|
|
1104
|
+
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
1008
1105
|
"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.
|
|
1106
|
+
"summary": title,
|
|
1107
|
+
"description": story_desc[:20000],
|
|
1108
|
+
# No humanInstructions on AI-created stories — added later at the task stage.
|
|
1014
1109
|
"parentTaskId": tid,
|
|
1015
1110
|
}
|
|
1016
1111
|
if child_email:
|
|
1017
1112
|
fields["assigneeEmail"] = child_email
|
|
1018
|
-
if
|
|
1019
|
-
fields["points"] =
|
|
1113
|
+
if spec.get("points"):
|
|
1114
|
+
fields["points"] = spec["points"]
|
|
1020
1115
|
new_id = await client.create_task_ml(fields, model_used=_EPIC_MODELS)
|
|
1021
1116
|
if new_id:
|
|
1022
1117
|
created.append(new_id)
|
|
1023
1118
|
await send(Envelope(
|
|
1024
1119
|
type="step_update", thread_id=thread_id,
|
|
1025
|
-
data={"current_step": f" ↳ created To Do
|
|
1120
|
+
data={"current_step": f" ↳ created To Do story {i}/{total}: {title}"},
|
|
1026
1121
|
).model_dump())
|
|
1027
1122
|
|
|
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()
|
|
1123
|
+
# Write this story's .md immediately (incremental), if a folder was given.
|
|
1124
|
+
if dir_path is not None:
|
|
1125
|
+
try:
|
|
1044
1126
|
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")[:50] or f"story-{i}"
|
|
1045
|
-
|
|
1046
|
-
|
|
1127
|
+
(dir_path / f"{i:02d}-{slug}.md").write_text(
|
|
1128
|
+
f"# {title}\n\n{story_desc}\n", encoding="utf-8")
|
|
1047
1129
|
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())
|
|
1130
|
+
except Exception: # noqa: BLE001
|
|
1131
|
+
pass
|
|
1060
1132
|
|
|
1133
|
+
tail = f"; wrote {written} .md file(s) to {story_dir}" if dir_path is not None else ""
|
|
1061
1134
|
await client.log_activity(
|
|
1062
|
-
f"Epic {jira_key}: Done — created {len(created)}/{
|
|
1135
|
+
f"Epic {jira_key}: Done — created {len(created)}/{total} To Do user Story(ies){tail}"
|
|
1063
1136
|
)
|
|
1064
1137
|
await send(Envelope(
|
|
1065
1138
|
type="step_update", thread_id=thread_id,
|
|
1066
1139
|
data={"jira_task_id": tid,
|
|
1067
|
-
"current_step": f"Epic {jira_key} complete — {len(created)} Story(ies) queued
|
|
1068
|
-
"(
|
|
1140
|
+
"current_step": f"Epic {jira_key} complete — {len(created)} Story(ies) queued"
|
|
1141
|
+
+ (f" · {written} file(s) in {story_dir}" if dir_path is not None else "")},
|
|
1069
1142
|
).model_dump())
|
|
1070
1143
|
|
|
1071
1144
|
|
|
@@ -1116,7 +1189,7 @@ def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
|
|
|
1116
1189
|
f"=== USER STORY {jira_key} ===\nSummary: {summary}",
|
|
1117
1190
|
]
|
|
1118
1191
|
if user_story and user_story.strip():
|
|
1119
|
-
parts.append("User story / description:\n" + _clip_text(user_story.strip()))
|
|
1192
|
+
parts.append("User story / description:\n" + _clip_text(user_story.strip(), _GEN_MAX_CHARS))
|
|
1120
1193
|
if human_instructions and human_instructions.strip():
|
|
1121
1194
|
parts.append("Operator instructions:\n" + human_instructions.strip())
|
|
1122
1195
|
parts.append(
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.83 → devops_bot_sdk-1.4.86}/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
|