@qa-gentic/stlc-agents 1.0.25 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/skills/generate-test-cases/SKILL.md +5 -0
  3. package/src/cli/cmd-cost.js +61 -30
  4. package/src/cli/cmd-init.js +88 -8
  5. package/src/stlc_agents/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  7. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
  8. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
  10. package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-314.pyc +0 -0
  11. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
  12. package/src/stlc_agents/agent_helix_writer/server.py +41 -6
  13. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-314.pyc +0 -0
  15. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-314.pyc +0 -0
  16. package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  17. package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
  18. package/src/stlc_agents/agent_playwright_generator/server.py +419 -213
  19. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
  21. package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
  22. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
  23. package/src/stlc_agents/agent_test_case_manager/server.py +12 -0
  24. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  25. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
  26. package/src/stlc_agents/agent_test_case_manager/tools/ado_workitem.py +65 -1
  27. package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
  29. package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
  30. package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
  31. package/src/stlc_agents/shared/cost_tracker.py +378 -70
  32. package/src/stlc_agents/shared/pricing.py +115 -24
  33. package/src/stlc_agents/webhook_orchestrator/__init__.py +0 -0
  34. package/src/stlc_agents/webhook_orchestrator/agent_runner.py +599 -0
  35. package/src/stlc_agents/webhook_orchestrator/main.py +43 -0
  36. package/src/stlc_agents/webhook_orchestrator/models.py +63 -0
  37. package/src/stlc_agents/webhook_orchestrator/orchestrator.py +103 -0
  38. package/src/stlc_agents/webhook_orchestrator/pipelines/__init__.py +0 -0
  39. package/src/stlc_agents/webhook_orchestrator/pipelines/_base.py +57 -0
  40. package/src/stlc_agents/webhook_orchestrator/pipelines/ado_test_cases.py +55 -0
  41. package/src/stlc_agents/webhook_orchestrator/pipelines/full_pipeline.py +202 -0
  42. package/src/stlc_agents/webhook_orchestrator/pipelines/gherkin_playwright.py +156 -0
  43. package/src/stlc_agents/webhook_orchestrator/pipelines/jira_test_cases.py +48 -0
  44. package/src/stlc_agents/webhook_orchestrator/webhook_bridge.py +368 -0
  45. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
  46. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
  47. package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
  48. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
  49. package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-310.pyc +0 -0
  50. package/src/stlc_agents/shared/__pycache__/pricing.cpython-310.pyc +0 -0
@@ -0,0 +1,48 @@
1
+ """
2
+ jira_test_cases.py — Agent 5 pipeline: generate and link test cases for a Jira issue.
3
+
4
+ Delegates entirely to qa-jira-manager.
5
+ The agent server owns all test-case generation logic; this module only defines
6
+ the task and the stop conditions the LLM must respect.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ from ..agent_runner import run_agent_loop
13
+ from ..models import PipelineResult, WebhookEvent
14
+ from ._base import event_env, mcp_servers
15
+
16
+ logger = logging.getLogger("stlc.pipeline.jira_test_cases")
17
+
18
+
19
+ def _task_prompt(event: WebhookEvent) -> str:
20
+ return (
21
+ f"You are a QA automation agent running in webhook mode — auto-confirm all prompts.\n\n"
22
+ f"Task: Generate and link test cases for Jira issue {event.work_item_id}.\n"
23
+ f" Cloud ID: {event.cloud_id}\n"
24
+ f" Project: {event.project_key}\n"
25
+ f" Type: {event.work_item_type}\n"
26
+ f" Status: {event.state}\n\n"
27
+ f"Use the qa-jira-manager tools to complete this task.\n\n"
28
+ f"Stop immediately if:\n"
29
+ f" - The issue type is Epic (tool returns epic_not_supported)\n"
30
+ f" - Linked test cases already exist (get_linked_test_cases returns non-empty list)\n"
31
+ )
32
+
33
+
34
+ async def run(event: WebhookEvent) -> PipelineResult:
35
+ label = f"jira-test-cases: wi={event.work_item_id}"
36
+ success, error = await run_agent_loop(
37
+ mcp_config=mcp_servers("qa-jira-manager"),
38
+ prompt=_task_prompt(event),
39
+ llm_provider=event.llm_provider,
40
+ llm_model=event.llm_model,
41
+ llm_api_key=event.llm_api_key,
42
+ llm_base_url=event.llm_base_url,
43
+ env=event_env(event),
44
+ work_item_id=event.work_item_id,
45
+ )
46
+ if not success:
47
+ return PipelineResult(success=False, steps=[label], error=error)
48
+ return PipelineResult(success=True, steps=[label, "completed"])
@@ -0,0 +1,368 @@
1
+ """
2
+ webhook_bridge.py — FastAPI HTTP bridge for the stlc-agents webhook orchestrator.
3
+
4
+ Receives webhook POST requests from Azure DevOps or Jira Cloud and routes them
5
+ to the correct agent pipeline via the orchestrator.
6
+
7
+ Endpoints
8
+ ─────────
9
+ POST /webhook/ado — Azure DevOps Service Hook (work item updated)
10
+ POST /webhook/jira — Jira Cloud webhook (issue updated)
11
+ GET /health — Liveness check
12
+
13
+ Configuration (env vars or .env)
14
+ ─────────────────────────────────
15
+ # Required: ADO
16
+ ADO_ORGANIZATION_URL https://dev.azure.com/yourorg
17
+ ADO_PROJECT_NAME YourProject
18
+
19
+ # Required: Jira
20
+ JIRA_CLOUD_ID your-cloud-id
21
+
22
+ # LLM provider (default: anthropic)
23
+ LLM_PROVIDER anthropic|openai|copilot|ollama|grok|deepseek
24
+ LLM_MODEL (optional) provider-specific model name
25
+ LLM_API_KEY API key (or use provider-specific env var)
26
+ LLM_BASE_URL (optional) base URL for ollama / deepseek
27
+
28
+ # Helix-QA
29
+ HELIX_PROJECT_ROOT /path/to/helix-qa-project (leave blank to skip Agent 4)
30
+
31
+ # Webhook security
32
+ ADO_WEBHOOK_SECRET optional shared secret for ADO service hooks
33
+ JIRA_WEBHOOK_SECRET optional shared secret for Jira webhooks
34
+
35
+ # Optional Agent 3 overrides
36
+ HEALING_STRATEGY role-label-text-ai | role-text-ai | ai-first
37
+ AUTH_HOOK microsoft-sso | azure-keyvault-mfa | none
38
+ ENABLE_VISUAL_REGRESSION true | false
39
+ ENABLE_TIMING_HEALING true | false
40
+
41
+ Run:
42
+ uvicorn stlc_agents.webhook_orchestrator.webhook_bridge:app --port 8080 --reload
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import hashlib
47
+ import hmac
48
+ import json
49
+ import logging
50
+ import os
51
+ from typing import Any
52
+
53
+ from dotenv import load_dotenv
54
+ from fastapi import BackgroundTasks, FastAPI, HTTPException, Request, status
55
+ from fastapi.responses import JSONResponse
56
+
57
+ from stlc_agents.webhook_orchestrator.orchestrator import (
58
+ Platform,
59
+ WebhookEvent,
60
+ run_pipeline,
61
+ )
62
+
63
+ load_dotenv()
64
+
65
+ logging.basicConfig(
66
+ level=logging.INFO,
67
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
68
+ )
69
+ logger = logging.getLogger("stlc.bridge")
70
+
71
+ app = FastAPI(title="STLC Agents Webhook Bridge", version="1.0.0")
72
+
73
+
74
+ # ──────────────────────────────────────────────────────────────────────────────
75
+ # Config helpers
76
+ # ──────────────────────────────────────────────────────────────────────────────
77
+
78
+ def _env(key: str, default: str = "") -> str:
79
+ return os.environ.get(key, default).strip()
80
+
81
+
82
+ def _bool_env(key: str, default: bool = True) -> bool:
83
+ v = os.environ.get(key, "").strip().lower()
84
+ if not v:
85
+ return default
86
+ return v not in ("false", "0", "no", "off")
87
+
88
+
89
+ def _llm_kwargs() -> dict:
90
+ return {
91
+ "llm_provider": _env("LLM_PROVIDER", "anthropic"),
92
+ "llm_model": _env("LLM_MODEL"),
93
+ "llm_api_key": _env("LLM_API_KEY"),
94
+ "llm_base_url": _env("LLM_BASE_URL"),
95
+ }
96
+
97
+
98
+ def _agent3_extra() -> dict:
99
+ return {
100
+ "healing_strategy": _env("HEALING_STRATEGY", "role-label-text-ai"),
101
+ "auth_hook": _env("AUTH_HOOK", "microsoft-sso"),
102
+ "enable_visual_regression": _bool_env("ENABLE_VISUAL_REGRESSION", True),
103
+ "enable_timing_healing": _bool_env("ENABLE_TIMING_HEALING", True),
104
+ }
105
+
106
+
107
+ # ──────────────────────────────────────────────────────────────────────────────
108
+ # Webhook signature verification
109
+ # ──────────────────────────────────────────────────────────────────────────────
110
+
111
+ def _verify_hmac(
112
+ body: bytes, signature_header: str | None, secret: str, prefix: str = "sha256="
113
+ ) -> bool:
114
+ if not secret:
115
+ return True
116
+ if not signature_header:
117
+ return False
118
+ expected = prefix + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
119
+ return hmac.compare_digest(expected, signature_header)
120
+
121
+
122
+ # ──────────────────────────────────────────────────────────────────────────────
123
+ # ADO payload parser
124
+ # ──────────────────────────────────────────────────────────────────────────────
125
+
126
+ def _parse_ado_webhook(payload: dict) -> WebhookEvent | None:
127
+ event_type = payload.get("eventType", "")
128
+ if event_type not in (
129
+ "workitem.updated",
130
+ "workitem.created",
131
+ "workitem.commented",
132
+ "ms.vss-work.workitem-updated-event",
133
+ ):
134
+ logger.debug("ADO event type '%s' not handled", event_type)
135
+ return None
136
+
137
+ resource = payload.get("resource") or {}
138
+ fields: dict[str, Any] = resource.get("fields", {})
139
+ revision_fields: dict[str, Any] = (resource.get("revision") or {}).get("fields", {})
140
+
141
+ # Extract state; for updates the field is a dict with oldValue/newValue
142
+ previous_state = ""
143
+ state_field = fields.get("System.State") or {}
144
+ if isinstance(state_field, dict):
145
+ old_val = (state_field.get("oldValue") or "").strip()
146
+ new_val = (state_field.get("newValue") or state_field.get("value") or "").strip()
147
+ # Skip no-op transitions — avoids reprocessing when only other fields changed
148
+ if old_val and new_val and old_val.lower() == new_val.lower():
149
+ logger.debug("ADO webhook: state unchanged (%s) — skipping", new_val)
150
+ return None
151
+ state = new_val
152
+ previous_state = old_val
153
+ else:
154
+ state = str(state_field).strip()
155
+
156
+ if not state:
157
+ state = str(revision_fields.get("System.State", "")).strip()
158
+
159
+ if not state:
160
+ logger.debug("ADO webhook: could not determine state")
161
+ return None
162
+
163
+ wi_id = str(resource.get("workItemId") or resource.get("id", ""))
164
+ if not wi_id:
165
+ logger.debug("ADO webhook: no work item ID")
166
+ return None
167
+
168
+ wi_type_field = fields.get("System.WorkItemType") or {}
169
+ if isinstance(wi_type_field, dict):
170
+ wi_type = wi_type_field.get("newValue") or wi_type_field.get("value") or ""
171
+ else:
172
+ wi_type = str(wi_type_field)
173
+ if not wi_type:
174
+ wi_type = revision_fields.get("System.WorkItemType", "")
175
+
176
+ title = revision_fields.get("System.Title", "")
177
+
178
+ # Extract org URL and project from payload first; fall back to env vars.
179
+ # ADO service hooks include these in resourceContainers.
180
+ resource_containers = payload.get("resourceContainers") or {}
181
+ org_url = (
182
+ (resource_containers.get("account") or {}).get("baseUrl", "").rstrip("/")
183
+ or _env("ADO_ORGANIZATION_URL")
184
+ )
185
+ project_name = (
186
+ (resource_containers.get("project") or {}).get("name", "")
187
+ or _env("ADO_PROJECT_NAME")
188
+ )
189
+
190
+ return WebhookEvent(
191
+ platform=Platform.ADO,
192
+ work_item_id=wi_id,
193
+ state=state,
194
+ previous_state=previous_state,
195
+ work_item_type=wi_type,
196
+ title=title,
197
+ organization_url=org_url,
198
+ project_name=project_name,
199
+ helix_project_root=_env("HELIX_PROJECT_ROOT"),
200
+ extra=_agent3_extra(),
201
+ **_llm_kwargs(),
202
+ )
203
+
204
+
205
+ # ──────────────────────────────────────────────────────────────────────────────
206
+ # Jira payload parser
207
+ # ──────────────────────────────────────────────────────────────────────────────
208
+
209
+ def _parse_jira_webhook(payload: dict) -> WebhookEvent | None:
210
+ event_type = payload.get("webhookEvent", "")
211
+ if event_type not in ("jira:issue_updated", "jira:issue_created"):
212
+ logger.debug("Jira event type '%s' not handled", event_type)
213
+ return None
214
+
215
+ issue = payload.get("issue", {})
216
+ if not issue:
217
+ logger.debug("Jira webhook: no issue in payload")
218
+ return None
219
+
220
+ issue_key = issue.get("key", "")
221
+ if not issue_key:
222
+ logger.debug("Jira webhook: no issue key")
223
+ return None
224
+
225
+ fields = issue.get("fields", {})
226
+
227
+ state = (fields.get("status") or {}).get("name", "")
228
+ if not state:
229
+ logger.debug("Jira webhook: could not determine status")
230
+ return None
231
+
232
+ # For updates, require an actual status transition in the changelog.
233
+ # This prevents re-running pipelines when only comments or other fields changed.
234
+ previous_state = ""
235
+ if event_type == "jira:issue_updated":
236
+ changelog = payload.get("changelog") or {}
237
+ status_item: dict | None = None
238
+ for item in changelog.get("items", []):
239
+ if item.get("field") == "status":
240
+ status_item = item
241
+ break
242
+ if status_item is None:
243
+ logger.debug("Jira webhook: no status field in changelog — skipping")
244
+ return None
245
+ from_str = (status_item.get("fromString") or "").strip()
246
+ to_str = (status_item.get("toString") or "").strip()
247
+ if from_str.lower() == to_str.lower():
248
+ logger.debug("Jira webhook: status unchanged (%s) — skipping", to_str)
249
+ return None
250
+ previous_state = from_str
251
+
252
+ wi_type = (fields.get("issuetype") or {}).get("name", "")
253
+ title = fields.get("summary", "")
254
+ proj_key = (fields.get("project") or {}).get("key", "")
255
+ # Derive project key from issue key when not present in fields (e.g. minimal payloads)
256
+ if not proj_key and "-" in issue_key:
257
+ proj_key = issue_key.split("-")[0]
258
+
259
+ return WebhookEvent(
260
+ platform=Platform.JIRA,
261
+ work_item_id=issue_key,
262
+ state=state,
263
+ previous_state=previous_state,
264
+ work_item_type=wi_type,
265
+ title=title,
266
+ cloud_id=_env("JIRA_CLOUD_ID"),
267
+ project_key=proj_key,
268
+ helix_project_root=_env("HELIX_PROJECT_ROOT"),
269
+ extra=_agent3_extra(),
270
+ **_llm_kwargs(),
271
+ )
272
+
273
+
274
+ # ──────────────────────────────────────────────────────────────────────────────
275
+ # Routes
276
+ # ──────────────────────────────────────────────────────────────────────────────
277
+
278
+ async def _pipeline_bg(event: WebhookEvent) -> None:
279
+ try:
280
+ result = await run_pipeline(event)
281
+ if result.success:
282
+ logger.info("Pipeline done: wi=%s steps=%s", event.work_item_id, result.steps)
283
+ else:
284
+ logger.error(
285
+ "Pipeline failed: wi=%s error=%s steps=%s",
286
+ event.work_item_id, result.error, result.steps,
287
+ )
288
+ except Exception:
289
+ logger.exception("Unhandled pipeline error for wi=%s", event.work_item_id)
290
+
291
+
292
+ @app.get("/health")
293
+ async def health() -> dict:
294
+ return {"status": "ok", "service": "stlc-webhook-bridge"}
295
+
296
+
297
+ @app.post("/webhook/ado")
298
+ async def webhook_ado(request: Request, background_tasks: BackgroundTasks) -> JSONResponse:
299
+ body = await request.body()
300
+ sig = request.headers.get("X-Hub-Signature-256") or request.headers.get("X-ADO-Signature")
301
+ if not _verify_hmac(body, sig, _env("ADO_WEBHOOK_SECRET")):
302
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")
303
+
304
+ try:
305
+ payload = json.loads(body)
306
+ except json.JSONDecodeError:
307
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid JSON")
308
+
309
+ event = _parse_ado_webhook(payload)
310
+ if event is None:
311
+ return JSONResponse({"accepted": False, "reason": "event not actionable"})
312
+
313
+ missing = []
314
+ if not event.organization_url:
315
+ missing.append("ADO_ORGANIZATION_URL")
316
+ if not event.project_name:
317
+ missing.append("ADO_PROJECT_NAME")
318
+ if missing:
319
+ msg = f"Missing required env var(s): {', '.join(missing)}. Set them in .env and restart."
320
+ logger.error("ADO webhook config error: %s", msg)
321
+ return JSONResponse(
322
+ {"accepted": False, "error": msg},
323
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
324
+ )
325
+
326
+ logger.info(
327
+ "ADO webhook: work_item=%s state=%s type=%s",
328
+ event.work_item_id, event.state, event.work_item_type,
329
+ )
330
+
331
+ # Acknowledge immediately — ADO retries on 5xx or slow responses.
332
+ # The pipeline runs in the background; results are logged.
333
+ background_tasks.add_task(_pipeline_bg, event)
334
+ return JSONResponse({"accepted": True, "work_item": event.work_item_id, "stage": "queued"})
335
+
336
+
337
+ @app.post("/webhook/jira")
338
+ async def webhook_jira(request: Request, background_tasks: BackgroundTasks) -> JSONResponse:
339
+ body = await request.body()
340
+ sig = request.headers.get("X-Hub-Signature-256") or request.headers.get("X-Jira-Signature")
341
+ if not _verify_hmac(body, sig, _env("JIRA_WEBHOOK_SECRET")):
342
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")
343
+
344
+ try:
345
+ payload = json.loads(body)
346
+ except json.JSONDecodeError:
347
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid JSON")
348
+
349
+ event = _parse_jira_webhook(payload)
350
+ if event is None:
351
+ return JSONResponse({"accepted": False, "reason": "event not actionable"})
352
+
353
+ if not event.cloud_id:
354
+ msg = "Missing required env var: JIRA_CLOUD_ID. Set it in .env and restart."
355
+ logger.error("Jira webhook config error: %s", msg)
356
+ return JSONResponse(
357
+ {"accepted": False, "error": msg},
358
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
359
+ )
360
+
361
+ logger.info(
362
+ "Jira webhook: issue=%s state=%s type=%s",
363
+ event.work_item_id, event.state, event.work_item_type,
364
+ )
365
+
366
+ # Acknowledge immediately — Jira retries on 5xx or slow responses.
367
+ background_tasks.add_task(_pipeline_bg, event)
368
+ return JSONResponse({"accepted": True, "issue": event.work_item_id, "stage": "queued"})