devops-bot-sdk 1.4.47__tar.gz → 1.4.58__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.47 → devops_bot_sdk-1.4.58}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/client.py +29 -1
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/ipc/handlers.py +311 -24
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/README.md +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/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.58"
|
|
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.58
|
|
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.58"
|
|
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.
|
|
61
|
+
SDK_VERSION = "1.4.58"
|
|
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
|
|
@@ -868,6 +868,34 @@ class BackendClient:
|
|
|
868
868
|
logger.warning("client.create_task_ml error err=%s", exc)
|
|
869
869
|
return None
|
|
870
870
|
|
|
871
|
+
async def add_comment(self, task_id: str, body: str) -> bool:
|
|
872
|
+
"""POST /tasks-ml/{id}/comments — add a comment to a ticket (best-effort).
|
|
873
|
+
|
|
874
|
+
Used by the validation gate to post AI feedback (prefixed with an
|
|
875
|
+
[AI Comments] label) when an Epic/Story is rejected. JSON body: {"body": ...}.
|
|
876
|
+
Returns True on 2xx.
|
|
877
|
+
"""
|
|
878
|
+
from sdk.config import BE_DEFAULT_URL
|
|
879
|
+
|
|
880
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}/comments"
|
|
881
|
+
headers = {
|
|
882
|
+
**self._headers, # includes Content-Type: application/json
|
|
883
|
+
"accept": "*/*",
|
|
884
|
+
"developer-name": "null",
|
|
885
|
+
"product": "",
|
|
886
|
+
}
|
|
887
|
+
try:
|
|
888
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
889
|
+
resp = await client.post(url, json={"body": str(body)}, headers=headers)
|
|
890
|
+
if resp.status_code >= 400:
|
|
891
|
+
logger.warning("client.add_comment failed task=%s status=%s body=%s",
|
|
892
|
+
task_id, resp.status_code, resp.text[:200])
|
|
893
|
+
return False
|
|
894
|
+
return True
|
|
895
|
+
except Exception as exc: # noqa: BLE001
|
|
896
|
+
logger.warning("client.add_comment error task=%s err=%s", task_id, exc)
|
|
897
|
+
return False
|
|
898
|
+
|
|
871
899
|
async def log_activity(self, activity: str) -> bool:
|
|
872
900
|
"""POST /ml-api/agent-activity — emit an agent activity event (best-effort).
|
|
873
901
|
|
|
@@ -210,6 +210,17 @@ def _build_local_prompt(
|
|
|
210
210
|
"before changing it.\n\n"
|
|
211
211
|
+ human_instructions.strip()
|
|
212
212
|
)
|
|
213
|
+
# 1b. If a PRD is present (a dev Story was converted to this Task with its PRD
|
|
214
|
+
# folded into the description), it is the AUTHORITATIVE spec — build the
|
|
215
|
+
# whole feature to satisfy it end-to-end.
|
|
216
|
+
if user_input and _PRD_MARKER in user_input:
|
|
217
|
+
parts.append(
|
|
218
|
+
"This task includes a PRD (Product Requirements Document) in its description "
|
|
219
|
+
"below. Treat the PRD as the AUTHORITATIVE specification: implement the WHOLE "
|
|
220
|
+
"feature to satisfy its Functional Requirements, Non-Functional Requirements, "
|
|
221
|
+
"and Acceptance Criteria, following the PRD end-to-end. Do not narrow or skip "
|
|
222
|
+
"any part of the PRD's scope."
|
|
223
|
+
)
|
|
213
224
|
# 2. Distilled plan (acceptance criteria + steps) — the compact, actionable
|
|
214
225
|
# content the backend planner derived from the full description.
|
|
215
226
|
sp = structured_plan or {}
|
|
@@ -231,6 +242,15 @@ def _build_local_prompt(
|
|
|
231
242
|
"above are authoritative if anything conflicts) ===\n"
|
|
232
243
|
+ _clip_text(user_input.strip())
|
|
233
244
|
)
|
|
245
|
+
# House standards — if this task produces a PRD document, hold it to the
|
|
246
|
+
# same standard format the bot uses everywhere else (a Task converted from a
|
|
247
|
+
# Story keeps its PRD directive in the instructions).
|
|
248
|
+
if _wants_prd(human_instructions) or _wants_prd(user_input):
|
|
249
|
+
parts.append(
|
|
250
|
+
"If you create a PRD / product-requirements document (e.g. a PRD.md), "
|
|
251
|
+
"write it in this STANDARD PRD FORMAT (keep the numbered headings):\n"
|
|
252
|
+
+ _PRD_FORMAT
|
|
253
|
+
)
|
|
234
254
|
if git_managed:
|
|
235
255
|
# The target is a git repo the SDK manages — it branches before and
|
|
236
256
|
# commits/pushes/opens the PR after, so the agent must not touch git here.
|
|
@@ -360,13 +380,78 @@ _MAX_EPIC_STORIES = int(os.getenv("AGENTOS_MAX_EPIC_STORIES", "") or 8)
|
|
|
360
380
|
# Label recorded in the ticket's modelUsed on Epic completion.
|
|
361
381
|
_EPIC_MODELS = [m.strip() for m in (os.getenv("AGENTOS_EPIC_MODELS", "") or "claude-code").split(",") if m.strip()]
|
|
362
382
|
|
|
383
|
+
# ── Standard output formats ─────────────────────────────────────────────────
|
|
384
|
+
# One house style for every generated artifact so Epics, Stories and PRDs read
|
|
385
|
+
# consistently across tickets. Referenced by the generation prompts below.
|
|
386
|
+
_EPIC_FORMAT = (
|
|
387
|
+
"## Epic\n<concise epic title>\n\n"
|
|
388
|
+
"## Overview\n<1-2 paragraph summary of the goal and context>\n\n"
|
|
389
|
+
"## Business Value / Goal\n<why this matters; the outcome it drives>\n\n"
|
|
390
|
+
"## Scope\n**In scope:** <what is included>\n**Out of scope:** <what is not>\n\n"
|
|
391
|
+
"## User Stories\n1. <story title> — <one-line summary>\n2. <story title> — <one-line summary>\n\n"
|
|
392
|
+
"## Epic-level Acceptance Criteria\n- <observable, high-level pass/fail criteria>\n\n"
|
|
393
|
+
"## Dependencies & Risks\n- <key dependencies, assumptions, risks>"
|
|
394
|
+
)
|
|
395
|
+
_USER_STORY_FORMAT = (
|
|
396
|
+
"## User Story\n"
|
|
397
|
+
"As a <role>, I want <capability>, so that <benefit>.\n\n"
|
|
398
|
+
"## Acceptance Criteria\n"
|
|
399
|
+
"- Given <context>, when <action>, then <expected outcome>.\n"
|
|
400
|
+
"- <list every testable criterion, Gherkin Given/When/Then style>\n\n"
|
|
401
|
+
"## Notes & Constraints\n"
|
|
402
|
+
"- <context, dependencies, constraints; for software work: affected files/modules & patterns to follow>\n\n"
|
|
403
|
+
"## Definition of Done\n"
|
|
404
|
+
"- Delivered against the acceptance criteria above\n"
|
|
405
|
+
"- Reviewed / validated (for software: tests added & passing)\n"
|
|
406
|
+
"- Documentation updated where relevant"
|
|
407
|
+
)
|
|
408
|
+
_PRD_FORMAT = (
|
|
409
|
+
"# PRD: <title>\n\n"
|
|
410
|
+
"## 1. Overview\n<what the feature is and why, in brief>\n\n"
|
|
411
|
+
"## 2. Goals & Non-Goals\n<what it must achieve; what is explicitly out of scope>\n\n"
|
|
412
|
+
"## 3. Current Behaviour & Context\n<the existing system/modules this touches — "
|
|
413
|
+
"grounded in the REAL codebase: files, components, current flow>\n\n"
|
|
414
|
+
"## 4. How It Works — Mechanism\n<the core mechanism end-to-end: the exact flow of "
|
|
415
|
+
"control and data, step by step, from trigger/input to output/persistence. Describe "
|
|
416
|
+
"how the feature actually functions, not just what it does>\n\n"
|
|
417
|
+
"## 5. Architecture & Components\n<components/modules/services involved and their "
|
|
418
|
+
"responsibilities; where new code lives; how they interact>\n\n"
|
|
419
|
+
"## 6. Data Model & Schema\n<entities, fields + types, relationships, indexes, and "
|
|
420
|
+
"any migrations / schema changes>\n\n"
|
|
421
|
+
"## 7. API / Interface Contracts\n<each endpoint or interface: HTTP method + path, "
|
|
422
|
+
"request shape, response shape, status codes, auth. Include example payloads>\n\n"
|
|
423
|
+
"## 8. Detailed Logic & Business Rules\n<algorithms, validation rules, state "
|
|
424
|
+
"transitions, sequence of operations, ordering/idempotency, concurrency>\n\n"
|
|
425
|
+
"## 9. Data Flow & Integrations\n<how data moves between components/services; "
|
|
426
|
+
"external systems, events, queues, third-party APIs>\n\n"
|
|
427
|
+
"## 10. Error Handling & Edge Cases\n<failure modes, validation errors, retries, "
|
|
428
|
+
"timeouts, boundary conditions, empty/duplicate/large inputs>\n\n"
|
|
429
|
+
"## 11. Security & Performance (Non-Functional)\n<authn/authz, input hardening, "
|
|
430
|
+
"secrets, rate limits, performance/scale targets, accessibility where relevant>\n\n"
|
|
431
|
+
"## 12. Testing Strategy\n<unit / integration / e2e coverage and the key test cases "
|
|
432
|
+
"that prove each requirement>\n\n"
|
|
433
|
+
"## 13. Acceptance Criteria\n<observable, testable pass/fail tied to the mechanism>\n\n"
|
|
434
|
+
"## 14. Dependencies, Risks & Rollout\n<dependencies, assumptions, risks, migration "
|
|
435
|
+
"and rollout/phasing>"
|
|
436
|
+
)
|
|
437
|
+
|
|
363
438
|
|
|
364
439
|
def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
365
440
|
human_instructions: str | None) -> str:
|
|
366
441
|
"""Read-only prompt that turns an Epic into a decomposition JSON."""
|
|
367
442
|
parts: list[str] = [
|
|
368
|
-
"You are decomposing a JIRA EPIC into implementable
|
|
369
|
-
"ONLY — do NOT create, edit, or delete any file.",
|
|
443
|
+
"You are decomposing a JIRA EPIC into implementable user stories. This is "
|
|
444
|
+
"ANALYSIS ONLY — do NOT create, edit, or delete any file.",
|
|
445
|
+
"Faithfulness (read first — this prevents hallucinated stories):\n"
|
|
446
|
+
"- Derive stories ONLY from what the epic actually states or clearly implies. "
|
|
447
|
+
"Do NOT invent features, scope, integrations, tools, or requirements that are "
|
|
448
|
+
"not grounded in the epic (or, for software, in the REAL codebase you inspect).\n"
|
|
449
|
+
"- If something needed is missing or ambiguous, capture it as an open question "
|
|
450
|
+
"in that story's Notes — do NOT fabricate a solution.\n"
|
|
451
|
+
"- Prefer fewer, well-grounded stories over many speculative ones. Every story "
|
|
452
|
+
"must trace back to something in the epic.\n"
|
|
453
|
+
"- This epic may be for ANY domain (software, Finance, HR, operations, …). Match "
|
|
454
|
+
"the stories to the epic's actual domain; do not assume it is software.",
|
|
370
455
|
f"=== EPIC {jira_key} ===\nSummary: {summary}",
|
|
371
456
|
]
|
|
372
457
|
if description and description.strip():
|
|
@@ -378,18 +463,20 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
|
378
463
|
+ human_instructions.strip()
|
|
379
464
|
)
|
|
380
465
|
parts.append(
|
|
381
|
-
"
|
|
382
|
-
"use graphify / Read / Glob / Grep)
|
|
383
|
-
"
|
|
466
|
+
"Ground your work first: if this is a SOFTWARE project, inspect the existing "
|
|
467
|
+
"codebase in this directory (read CLAUDE.md, use graphify / Read / Glob / Grep) "
|
|
468
|
+
"so the stories fit the REAL architecture. For a NON-software epic (Finance, HR, "
|
|
469
|
+
"operations, …), ground the stories in the epic's stated details and any "
|
|
470
|
+
"reference material present — never in invented facts. Then produce the "
|
|
471
|
+
"decomposition.\n\n"
|
|
384
472
|
"Output ONLY a single fenced ```json block and nothing after it, with shape:\n"
|
|
385
473
|
"{\n"
|
|
386
|
-
' "epic_description": "
|
|
387
|
-
"write-up covering goal, the user stories, acceptance criteria and scope.\",\n"
|
|
474
|
+
' "epic_description": "The epic write-up in the EPIC FORMAT below (Markdown).",\n'
|
|
388
475
|
' "plan": "An ordered, numbered high-level implementation plan.",\n'
|
|
389
476
|
' "stories": [\n'
|
|
390
477
|
' {\n'
|
|
391
478
|
' "summary": "Short imperative story title (a JIRA task summary)",\n'
|
|
392
|
-
' "description": "
|
|
479
|
+
' "description": "The story in the USER STORY FORMAT below (Markdown).",\n'
|
|
393
480
|
' "humanInstructions": "Operator instructions to implement THIS story. MUST '
|
|
394
481
|
"repeat the same project location/path and constraints from the epic so the "
|
|
395
482
|
'coding run works in the right place.",\n'
|
|
@@ -397,12 +484,16 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
|
|
|
397
484
|
' "points": 5\n'
|
|
398
485
|
" }\n"
|
|
399
486
|
" ]\n"
|
|
400
|
-
"}\n"
|
|
487
|
+
"}\n\n"
|
|
401
488
|
"Create as many stories as the epic genuinely needs (do not pad or truncate "
|
|
402
489
|
"artificially); each story must be independently implementable.\n"
|
|
403
490
|
"assigneeEmail is OPTIONAL per story: set it ONLY when the epic explicitly "
|
|
404
491
|
"assigns that specific work to a named person/email (e.g. 'assign the API to "
|
|
405
|
-
"alice@x.com'); otherwise omit it
|
|
492
|
+
"alice@x.com'); otherwise omit it.\n\n"
|
|
493
|
+
"=== EPIC FORMAT (epic_description MUST follow this exactly) ===\n"
|
|
494
|
+
+ _EPIC_FORMAT
|
|
495
|
+
+ "\n\n=== USER STORY FORMAT (every story.description MUST follow this exactly) ===\n"
|
|
496
|
+
+ _USER_STORY_FORMAT
|
|
406
497
|
)
|
|
407
498
|
return "\n\n".join(parts)
|
|
408
499
|
|
|
@@ -459,6 +550,117 @@ def _epic_inherited_fields(task: dict) -> dict:
|
|
|
459
550
|
return {k: v for k, v in fields.items() if v}
|
|
460
551
|
|
|
461
552
|
|
|
553
|
+
# ── Ticket validation gate (Epics + USER-driven Stories) ────────────────────
|
|
554
|
+
# A strict quality gate: before the bot actions an Epic or a USER-created Story it
|
|
555
|
+
# checks the ticket meets the required standard. On failure it posts a harsh,
|
|
556
|
+
# technical comment (prefixed with the label below) and moves the ticket to
|
|
557
|
+
# Backlog. SYSTEM-created Stories (spun off an Epic by the bot) are trusted and
|
|
558
|
+
# skip validation. A later run re-validates; once the gaps are addressed in the
|
|
559
|
+
# description / Human Instructions, it passes and the normal flow proceeds.
|
|
560
|
+
_AI_COMMENT_LABEL = "[AI Comments]"
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _build_validation_prompt(kind: str, jira_key: str, summary: str,
|
|
564
|
+
description: str | None, human: str | None) -> str:
|
|
565
|
+
k = kind.upper()
|
|
566
|
+
parts: list[str] = [
|
|
567
|
+
f"You are a STRICT ticket-quality gate for a JIRA {k}. Decide whether this "
|
|
568
|
+
"ticket meets the required standard to be actioned autonomously. Be HARSH: if "
|
|
569
|
+
"the required structure or information is not strictly present, REJECT it — a "
|
|
570
|
+
"vague or under-specified ticket must NOT pass.",
|
|
571
|
+
f"=== {k} {jira_key} ===\nSummary: {summary or '(none)'}",
|
|
572
|
+
"Description:\n" + (_clip_text(description.strip()) if description and description.strip() else "(none provided)"),
|
|
573
|
+
"Human Instructions:\n" + (human.strip() if human and human.strip() else "(none provided)"),
|
|
574
|
+
]
|
|
575
|
+
parts.append(
|
|
576
|
+
"A VALID ticket MUST have:\n"
|
|
577
|
+
"- A clear, unambiguous goal and scope (no hand-waving).\n"
|
|
578
|
+
"- Explicit Human Instructions: for codebase work, the repository link(s) "
|
|
579
|
+
"and/or file path(s), the branch/PR convention, and build/run/test steps.\n"
|
|
580
|
+
"- Observable, testable acceptance criteria.\n"
|
|
581
|
+
+ ("- Enough concrete detail to decompose into independent user stories.\n"
|
|
582
|
+
if kind == "epic" else
|
|
583
|
+
"- A clear 'As a <role>, I want <capability>, so that <benefit>' intent plus "
|
|
584
|
+
"testable acceptance criteria.\n")
|
|
585
|
+
+ "- Any THIRD-PARTY INTEGRATION or human-effort dependency (API keys, OAuth "
|
|
586
|
+
"credentials, external accounts, secrets, manual provisioning, DNS, billing) "
|
|
587
|
+
"MUST be named explicitly WITH who provides it. If such a dependency is implied "
|
|
588
|
+
"but undefined/missing, REJECT.\n\n"
|
|
589
|
+
"Output ONLY a fenced ```json block, nothing else:\n"
|
|
590
|
+
"{\n"
|
|
591
|
+
' "valid": true or false,\n'
|
|
592
|
+
' "comment": "When invalid: a concise, TECHNICAL rejection that lists EXACTLY '
|
|
593
|
+
"what is missing or ambiguous and must be added. Use precise engineering "
|
|
594
|
+
"terminology (API contract, data model/schema, auth/OAuth flow, env/config, "
|
|
595
|
+
"acceptance criteria, target repository/branch, integration credentials & "
|
|
596
|
+
'ownership). When valid: empty string."\n'
|
|
597
|
+
"}"
|
|
598
|
+
)
|
|
599
|
+
return "\n\n".join(parts)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _parse_validation(text: str | None) -> tuple[bool, str]:
|
|
603
|
+
"""(valid, comment) from the validator's JSON output. Defaults to invalid."""
|
|
604
|
+
obj = _extract_json_block(text) or {}
|
|
605
|
+
return bool(obj.get("valid")), str(obj.get("comment") or "").strip()
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
async def _validate_ticket_or_reject(
|
|
609
|
+
client: BackendClient, task: dict, kind: str, payload: dict,
|
|
610
|
+
send: Callable, thread_id: str,
|
|
611
|
+
) -> bool:
|
|
612
|
+
"""Strict gate. Returns True to proceed, False if the ticket was rejected
|
|
613
|
+
(comment posted + moved to Backlog). Fails OPEN if the validator can't run —
|
|
614
|
+
we never block a ticket on our own infrastructure error."""
|
|
615
|
+
from sdk import local_exec
|
|
616
|
+
|
|
617
|
+
tid = str(task.get("id", ""))
|
|
618
|
+
jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "") or kind.upper()
|
|
619
|
+
project_path = _resolve_local_project_path(task, payload)
|
|
620
|
+
|
|
621
|
+
await send(Envelope(
|
|
622
|
+
type="step_update", thread_id=thread_id,
|
|
623
|
+
data={"jira_task_id": tid, "task_id": tid,
|
|
624
|
+
"current_step": f"{kind.title()} {jira_key}: validating ticket against the standard"},
|
|
625
|
+
).model_dump())
|
|
626
|
+
|
|
627
|
+
res = await local_exec.run_claude_local(
|
|
628
|
+
_build_validation_prompt(kind, jira_key, task.get("summary", "") or "",
|
|
629
|
+
task.get("description"), task.get("humanInstructions")),
|
|
630
|
+
project_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only judgment
|
|
631
|
+
github_token=await client.github_token(),
|
|
632
|
+
)
|
|
633
|
+
if not res.get("ok"):
|
|
634
|
+
await client.log_activity(
|
|
635
|
+
f"{kind} {jira_key}: validation could not run ({(res.get('error') or '')[:120]}) — proceeding"
|
|
636
|
+
)
|
|
637
|
+
return True # fail-open on validator infra error
|
|
638
|
+
|
|
639
|
+
valid, comment = _parse_validation(res.get("result"))
|
|
640
|
+
if valid:
|
|
641
|
+
await send(Envelope(
|
|
642
|
+
type="step_update", thread_id=thread_id,
|
|
643
|
+
data={"jira_task_id": tid, "current_step": f"{kind.title()} {jira_key}: validation passed"},
|
|
644
|
+
).model_dump())
|
|
645
|
+
return True
|
|
646
|
+
|
|
647
|
+
# Rejected — post a labelled, technical comment and move to Backlog.
|
|
648
|
+
fallback = ("This ticket does not meet the required standard. Provide: a clear goal "
|
|
649
|
+
"and scope; explicit Human Instructions (target repository/branch and/or "
|
|
650
|
+
"file paths, PR convention, build/run/test steps); testable acceptance "
|
|
651
|
+
"criteria; and the credentials/ownership for any third-party integration.")
|
|
652
|
+
body = f"{_AI_COMMENT_LABEL}\n\n{comment or fallback}"
|
|
653
|
+
await client.add_comment(tid, body)
|
|
654
|
+
await client.move_to_backlog(tid)
|
|
655
|
+
await client.log_activity(f"{kind} {jira_key}: REJECTED by validation gate — commented + moved to Backlog")
|
|
656
|
+
await send(Envelope(
|
|
657
|
+
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
658
|
+
data={"code": f"{kind.upper()}_VALIDATION_REJECTED", "task_id": tid,
|
|
659
|
+
"message": (comment or fallback)[:300]},
|
|
660
|
+
).model_dump())
|
|
661
|
+
return False
|
|
662
|
+
|
|
663
|
+
|
|
462
664
|
async def _handle_epic(
|
|
463
665
|
client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
|
|
464
666
|
) -> None:
|
|
@@ -537,6 +739,7 @@ async def _handle_epic(
|
|
|
537
739
|
**inherited,
|
|
538
740
|
"type": "Story", # Epic decomposes into user Stories; a Story later
|
|
539
741
|
# becomes a Task (see _handle_story) when it is run.
|
|
742
|
+
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
540
743
|
"status": "To Do",
|
|
541
744
|
"summary": str(st.get("summary", "")).strip()[:250],
|
|
542
745
|
"description": str(st.get("description", "") or "")[:20000],
|
|
@@ -582,12 +785,34 @@ def _wants_prd(text: str | None) -> bool:
|
|
|
582
785
|
return bool(text and _PRD_RE.search(text))
|
|
583
786
|
|
|
584
787
|
|
|
788
|
+
# Signals that a ticket is DEVELOPMENT work (→ always gets a PRD). Deliberately
|
|
789
|
+
# code-specific so Finance/HR/ops tickets don't false-positive (they get a PRD
|
|
790
|
+
# only when explicitly requested).
|
|
791
|
+
_DEV_RE = re.compile(
|
|
792
|
+
r"github\.com/|gitlab\.com/|bitbucket\.org/|\b(?:repo(?:sitory)?|codebase|"
|
|
793
|
+
r"source\s*code|frontend|front-end|back-?end|pull\s+requests?|merge\s+requests?|"
|
|
794
|
+
r"\bPRs?\b|feature\s+branch|create\s+a?\s*branch|commit|deploy(?:ment)?|compile|"
|
|
795
|
+
r"build\s+pipeline|microservice|endpoint|api\s+contract|database\s+migration|"
|
|
796
|
+
r"unit\s+test)\b",
|
|
797
|
+
re.I,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _is_dev_task(text: str | None) -> bool:
|
|
802
|
+
"""True when the text clearly describes software-development work."""
|
|
803
|
+
return bool(text and _DEV_RE.search(text))
|
|
804
|
+
|
|
805
|
+
|
|
585
806
|
def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
|
|
586
807
|
human_instructions: str | None) -> str:
|
|
587
|
-
"""Read-only prompt that produces a PRD (Markdown) for a user story."""
|
|
808
|
+
"""Read-only prompt that produces a TECHNICAL PRD (Markdown) for a user story."""
|
|
588
809
|
parts: list[str] = [
|
|
589
|
-
"Write a PRD (Product Requirements Document) for the user story below.
|
|
590
|
-
"
|
|
810
|
+
"Write a TECHNICAL PRD (Product Requirements Document) for the user story below. "
|
|
811
|
+
"It must fully specify the MECHANISM — how the feature works end-to-end "
|
|
812
|
+
"(architecture, components, data model, API/interface contracts, control & data "
|
|
813
|
+
"flow, business logic, error handling) — in enough detail that a developer can "
|
|
814
|
+
"implement it directly from the PRD without guessing. This is ANALYSIS ONLY — do "
|
|
815
|
+
"NOT create, edit, or delete any file.",
|
|
591
816
|
f"=== USER STORY {jira_key} ===\nSummary: {summary}",
|
|
592
817
|
]
|
|
593
818
|
if user_story and user_story.strip():
|
|
@@ -595,11 +820,17 @@ def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
|
|
|
595
820
|
if human_instructions and human_instructions.strip():
|
|
596
821
|
parts.append("Operator instructions:\n" + human_instructions.strip())
|
|
597
822
|
parts.append(
|
|
598
|
-
"
|
|
599
|
-
"
|
|
600
|
-
"
|
|
601
|
-
"
|
|
602
|
-
"
|
|
823
|
+
"Ground the PRD in reality — for software, FIRST understand the current codebase "
|
|
824
|
+
"STRUCTURE (folder layout, key modules, existing patterns, data models, APIs) via "
|
|
825
|
+
"CLAUDE.md, graphify and Read/Glob/Grep, then specify the mechanism against the "
|
|
826
|
+
"REAL files, modules, data models and APIs that exist; for a non-software task, "
|
|
827
|
+
"ground it in "
|
|
828
|
+
"the story's stated details and mark technical sections that don't apply as "
|
|
829
|
+
"'N/A'. Do NOT invent requirements, systems, endpoints, or facts not supported by "
|
|
830
|
+
"the story or the code you inspect. Output ONLY the PRD as Markdown — no preamble, "
|
|
831
|
+
"no code edits — following this PRD FORMAT exactly (keep the numbered section "
|
|
832
|
+
"headings):\n\n"
|
|
833
|
+
+ _PRD_FORMAT
|
|
603
834
|
)
|
|
604
835
|
return "\n\n".join(parts)
|
|
605
836
|
|
|
@@ -621,14 +852,25 @@ async def _handle_story(
|
|
|
621
852
|
summary = task.get("summary", "") or ""
|
|
622
853
|
user_story = task.get("description") or ""
|
|
623
854
|
human = task.get("humanInstructions")
|
|
624
|
-
|
|
625
|
-
|
|
855
|
+
# STEP 1 — validate whether this is a CODEBASE (development) task: repo links /
|
|
856
|
+
# file paths in the instructions, or dev signals (PRs, branches, endpoints,
|
|
857
|
+
# deploy, …). A codebase task ALWAYS gets a technical PRD built from the current
|
|
858
|
+
# codebase; other domains (Finance, HR, …) get one only when explicitly asked.
|
|
859
|
+
has_repo = bool(_extract_repo_targets(human)) or bool(_extract_repo_targets(user_story))
|
|
860
|
+
is_codebase_task = has_repo or _is_dev_task(human) or _is_dev_task(user_story)
|
|
861
|
+
wants_prd = is_codebase_task or _wants_prd(human) or _wants_prd(user_story)
|
|
862
|
+
|
|
863
|
+
if is_codebase_task:
|
|
864
|
+
step = ("validated as a codebase task — building a technical PRD from the "
|
|
865
|
+
"current codebase, then converting to Task")
|
|
866
|
+
elif wants_prd:
|
|
867
|
+
step = "generating the requested PRD, then converting to Task"
|
|
868
|
+
else:
|
|
869
|
+
step = "converting to Task"
|
|
626
870
|
await send(Envelope(
|
|
627
871
|
type="step_update", thread_id=thread_id,
|
|
628
872
|
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"},
|
|
873
|
+
"current_step": f"Story {jira_key}: {step}"},
|
|
632
874
|
).model_dump())
|
|
633
875
|
|
|
634
876
|
# Idempotent: if a PRD is already in the description, don't regenerate it or
|
|
@@ -1039,6 +1281,40 @@ async def _code_one_repo(
|
|
|
1039
1281
|
except Exception: # noqa: BLE001
|
|
1040
1282
|
branch_info = None
|
|
1041
1283
|
|
|
1284
|
+
# 3a. Technical PRD for a CODEBASE task that doesn't have one yet. A Task created
|
|
1285
|
+
# directly (not via a Story) may carry only a user-story/feature description —
|
|
1286
|
+
# for codebase work we first turn the recon understanding into a technical PRD
|
|
1287
|
+
# and build to it, exactly like a Story-derived Task. Resumes the recon session
|
|
1288
|
+
# (already understands the codebase) so it's cheap; persists the PRD to the
|
|
1289
|
+
# ticket and to pending (shared → other repos reuse it and skip regeneration).
|
|
1290
|
+
# Skipped when a PRD is already present, on a resume, or for non-git targets.
|
|
1291
|
+
if (is_git and recon_session and not resume_session_id
|
|
1292
|
+
and _PRD_MARKER not in (pending.get("user_input") or "")):
|
|
1293
|
+
await send(Envelope(
|
|
1294
|
+
type="step_update", thread_id=thread_id,
|
|
1295
|
+
data={"status": "running",
|
|
1296
|
+
"current_step": f"[{name}] Codebase task — writing a technical PRD from the codebase"},
|
|
1297
|
+
).model_dump())
|
|
1298
|
+
prd_res = await local_exec.run_claude_local(
|
|
1299
|
+
"Using your exploration above, write a TECHNICAL PRD for this task that fully "
|
|
1300
|
+
"specifies the mechanism (how it works end-to-end) against the REAL code you "
|
|
1301
|
+
"found. Output ONLY the PRD as Markdown, following this format exactly:\n\n"
|
|
1302
|
+
+ _PRD_FORMAT,
|
|
1303
|
+
repo_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only
|
|
1304
|
+
github_token=gh_token, resume_session_id=recon_session,
|
|
1305
|
+
)
|
|
1306
|
+
recon_session = prd_res.get("session_id") or recon_session
|
|
1307
|
+
prd = (prd_res.get("result") or "").strip() if prd_res.get("ok") else ""
|
|
1308
|
+
if prd:
|
|
1309
|
+
enriched = (
|
|
1310
|
+
f"{pending.get('user_input') or ''}\n\n---\n\n{_PRD_MARKER}\n\n{prd}"
|
|
1311
|
+
)[:20000]
|
|
1312
|
+
pending["user_input"] = enriched # shared across repos → reused, not regenerated
|
|
1313
|
+
try:
|
|
1314
|
+
await client.update_task_ml(task_id, {"description": enriched}, model_used=_EPIC_MODELS)
|
|
1315
|
+
except Exception: # noqa: BLE001 — persisting the PRD is best-effort
|
|
1316
|
+
pass
|
|
1317
|
+
|
|
1042
1318
|
await send(Envelope(
|
|
1043
1319
|
type="step_update", thread_id=thread_id,
|
|
1044
1320
|
data={"status": "running", "current_step": f"Executing locally in {name} (claude_code)"},
|
|
@@ -1047,7 +1323,8 @@ async def _code_one_repo(
|
|
|
1047
1323
|
# Build the prompt with framing that matches the target: a git repo we manage
|
|
1048
1324
|
# (branch + PR) vs a plain local folder (BA/analyst deliverables, no git/PR).
|
|
1049
1325
|
# When recon ran, the implementation resumes that session so its grounded plan
|
|
1050
|
-
# stays in context;
|
|
1326
|
+
# (and the technical PRD, if generated) stays in context; if a PRD is present the
|
|
1327
|
+
# prompt makes it the authoritative spec.
|
|
1051
1328
|
prompt = _build_local_prompt(
|
|
1052
1329
|
pending["user_input"], pending.get("structured_plan"),
|
|
1053
1330
|
pending.get("human_instructions"),
|
|
@@ -2555,7 +2832,17 @@ async def handle_orchestrate_auto_local_run(
|
|
|
2555
2832
|
if ttype in ("epic", "story"):
|
|
2556
2833
|
handler = _handle_epic if ttype == "epic" else _handle_story
|
|
2557
2834
|
code = "EPIC_DECOMPOSE_FAILED" if ttype == "epic" else "STORY_HANDLE_FAILED"
|
|
2835
|
+
# Validation gate: Epics are always validated; a Story only when it
|
|
2836
|
+
# is USER-driven (a SYSTEM/bot-created Story is trusted). A rejected
|
|
2837
|
+
# ticket is commented + moved to Backlog inside the gate — skip it.
|
|
2838
|
+
driven_by = str(task.get("drivenBy") or "").strip().upper()
|
|
2839
|
+
needs_validation = (ttype == "epic") or (ttype == "story" and driven_by != "SYSTEM")
|
|
2558
2840
|
try:
|
|
2841
|
+
if needs_validation and not await _validate_ticket_or_reject(
|
|
2842
|
+
client, task, ttype, payload, send, thread_id
|
|
2843
|
+
):
|
|
2844
|
+
processed += 1
|
|
2845
|
+
continue
|
|
2559
2846
|
await handler(client, task, payload, send, thread_id)
|
|
2560
2847
|
except Exception as exc: # noqa: BLE001
|
|
2561
2848
|
logger.warning("auto_local.%s_failed task=%s err=%s", ttype, tid, exc)
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.47 → devops_bot_sdk-1.4.58}/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
|