@qa-gentic/stlc-agents 1.0.16 → 1.0.18

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 (45) hide show
  1. package/ORCHESTRATION_RULES.md +283 -0
  2. package/README.md +246 -321
  3. package/bin/postinstall.js +26 -2
  4. package/bin/qa-stlc.js +23 -0
  5. package/package.json +15 -2
  6. package/skills/write-helix-files/SKILL.md +6 -0
  7. package/src/cli/cmd-cost.js +253 -0
  8. package/src/cli/cmd-init.js +19 -2
  9. package/src/cli/cmd-mcp-config.js +123 -62
  10. package/src/cli/cmd-skills.js +21 -4
  11. package/src/stlc_agents/agent_gherkin_generator/server.py +88 -4
  12. package/src/stlc_agents/agent_helix_writer/tools/helix_write.py +60 -28
  13. package/src/stlc_agents/agent_jira_manager/server.py +209 -2
  14. package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +36 -0
  15. package/src/stlc_agents/agent_playwright_generator/server.py +968 -105
  16. package/src/stlc_agents/agent_test_case_manager/server.py +121 -2
  17. package/src/stlc_agents/shared/cost_tracker.py +395 -0
  18. package/src/stlc_agents/shared/install_hook.py +154 -0
  19. package/src/stlc_agents/shared/pricing.py +72 -0
  20. package/src/stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
  21. package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  22. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
  23. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  24. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
  25. package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
  26. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
  27. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  28. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-310.pyc +0 -0
  29. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
  30. package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-310.pyc +0 -0
  31. package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
  32. package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  33. package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-310.pyc +0 -0
  34. package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  35. package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
  36. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  37. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
  38. package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
  39. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
  40. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  41. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
  42. package/src/stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
  43. package/src/stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
  44. package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-310.pyc +0 -0
  45. package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-310.pyc +0 -0
@@ -34,6 +34,7 @@ from stlc_agents.agent_jira_manager.tools.jira_workitem import (
34
34
  create_test_case as _create_test_case,
35
35
  link_test_cases_to_issue as _link_test_cases,
36
36
  get_linked_test_cases as _get_linked_test_cases,
37
+ attach_gherkin_to_issue as _attach_gherkin,
37
38
  )
38
39
 
39
40
  load_dotenv()
@@ -148,9 +149,25 @@ def _validate_linked_test_cases_response(result: dict) -> dict:
148
149
 
149
150
 
150
151
  # ---------------------------------------------------------------------------
151
- # Tool definitions
152
+ # Deduplication helper
152
153
  # ---------------------------------------------------------------------------
153
154
 
155
+ _TC_STOP_WORDS = frozenset({
156
+ "verify", "ensure", "validate", "check", "test", "the", "a", "an",
157
+ "that", "is", "are", "can", "user", "should", "able", "to", "with",
158
+ })
159
+
160
+
161
+ def _normalise_summary(summary: str) -> str:
162
+ """Lowercase, strip punctuation, remove stop words — for dedup comparison."""
163
+ import re
164
+ cleaned = re.sub(r"[^a-z0-9\s]", "", summary.lower())
165
+ tokens = [w for w in cleaned.split() if w not in _TC_STOP_WORDS]
166
+ return " ".join(tokens)
167
+
168
+
169
+
170
+
154
171
  @app.list_tools()
155
172
  async def list_tools() -> list[types.Tool]:
156
173
  return [
@@ -275,6 +292,116 @@ async def list_tools() -> list[types.Tool]:
275
292
  "required": ["issue_key"],
276
293
  },
277
294
  ),
295
+ types.Tool(
296
+ name="attach_gherkin_to_jira_issue",
297
+ description=(
298
+ "Upload a validated .feature file as an attachment on a Jira issue. "
299
+ "Use this after generate_gherkin + validate_gherkin_content to link the "
300
+ "Gherkin artefact back to the source Jira issue — mirrors the ADO "
301
+ "attach_gherkin_to_work_item / attach_gherkin_to_feature tools. "
302
+ "File is named {issue_key}_{summary_kebab}_regression.feature automatically. "
303
+ "Requires the Jira OAuth token to have 'write:jira-work' scope."
304
+ ),
305
+ inputSchema={
306
+ "type": "object",
307
+ "properties": {
308
+ "issue_key": {
309
+ "type": "string",
310
+ "description": "Jira issue key, e.g. 'PROJ-123'",
311
+ },
312
+ "gherkin_content": {
313
+ "type": "string",
314
+ "description": "Complete validated .feature file content",
315
+ },
316
+ "cloud_id": {
317
+ "type": "string",
318
+ "description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
319
+ },
320
+ },
321
+ "required": ["issue_key", "gherkin_content"],
322
+ },
323
+ ),
324
+ types.Tool(
325
+ name="create_deduped_test_cases", description=(
326
+ "Create test cases in Jira, skipping any whose summary already exists as a "
327
+ "linked test case on the issue. "
328
+ "Internally calls get_linked_test_cases, filters the incoming batch against "
329
+ "existing summaries (case-insensitive, stop-word-normalised), then calls "
330
+ "create_and_link_test_cases on the net-new subset only. "
331
+ "Use this instead of create_and_link_test_cases for webhook/headless runs "
332
+ "where re-triggers would otherwise produce duplicates. "
333
+ "Returns skipped_count, created_count, and the full create result."
334
+ ),
335
+ inputSchema={
336
+ "type": "object",
337
+ "properties": {
338
+ "issue_key": {
339
+ "type": "string",
340
+ "description": "Jira issue key to link test cases to, e.g. 'PROJ-123'",
341
+ },
342
+ "cloud_id": {
343
+ "type": "string",
344
+ "description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
345
+ },
346
+ "test_cases": {
347
+ "type": "array",
348
+ "description": "Full proposed test case batch (duplicates will be filtered out)",
349
+ "items": {
350
+ "type": "object",
351
+ "properties": {
352
+ "summary": {"type": "string", "description": "Test case title/summary"},
353
+ "priority": {"type": "string", "description": "Highest|High|Medium|Low|Lowest"},
354
+ "steps": {
355
+ "type": "array",
356
+ "items": {
357
+ "type": "object",
358
+ "properties": {
359
+ "action": {"type": "string"},
360
+ "expected_result": {"type": "string"},
361
+ },
362
+ "required": ["action", "expected_result"],
363
+ },
364
+ },
365
+ },
366
+ "required": ["summary", "steps"],
367
+ },
368
+ },
369
+ },
370
+ "required": ["issue_key", "test_cases"],
371
+ },
372
+ ),
373
+ types.Tool(
374
+ name="attach_gherkin_to_issue",
375
+ description=(
376
+ "Upload and attach a validated .feature file to a Jira issue as an attachment. "
377
+ "File is named {issue_key}_{summary_kebab}_regression.feature automatically. "
378
+ "Use this after validate_gherkin_content passes. "
379
+ "Called by the pipeline's Jira Gherkin path to link the .feature back to "
380
+ "the Jira issue instead of saving to disk only."
381
+ ),
382
+ inputSchema={
383
+ "type": "object",
384
+ "properties": {
385
+ "issue_key": {
386
+ "type": "string",
387
+ "description": "Jira issue key, e.g. 'PROJ-123'",
388
+ },
389
+ "gherkin_content": {
390
+ "type": "string",
391
+ "description": "Validated .feature file content",
392
+ },
393
+ "issue_summary": {
394
+ "type": "string",
395
+ "description": "Issue summary used in the filename (optional)",
396
+ },
397
+ "cloud_id": {
398
+ "type": "string",
399
+ "description": "Atlassian cloud ID. Leave blank to use JIRA_CLOUD_ID env var.",
400
+ },
401
+ },
402
+ "required": ["issue_key", "gherkin_content"],
403
+ },
404
+ ),
278
405
  ]
279
406
 
280
407
 
@@ -420,6 +547,26 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
420
547
  },
421
548
  }
422
549
 
550
+ elif name == "attach_gherkin_to_jira_issue":
551
+ issue_key = arguments["issue_key"]
552
+ gherkin_content = arguments["gherkin_content"]
553
+
554
+ # Fetch issue summary for filename (best-effort)
555
+ issue_summary = ""
556
+ try:
557
+ peek = await asyncio.to_thread(_fetch_work_item, cloud_id, issue_key)
558
+ issue_summary = (peek.get("work_item") or {}).get("summary", "")
559
+ except Exception:
560
+ pass
561
+
562
+ result = await asyncio.to_thread(
563
+ _attach_gherkin,
564
+ cloud_id,
565
+ issue_key,
566
+ gherkin_content,
567
+ issue_summary,
568
+ )
569
+
423
570
  elif name == "get_linked_test_cases":
424
571
  result = await asyncio.to_thread(
425
572
  _get_linked_test_cases,
@@ -428,6 +575,66 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
428
575
  )
429
576
  result["_validation"] = _validate_linked_test_cases_response(result)
430
577
 
578
+ elif name == "create_deduped_test_cases":
579
+ issue_key = arguments["issue_key"]
580
+ proposed = arguments["test_cases"]
581
+
582
+ # Normalise title/summary on input
583
+ for tc in proposed:
584
+ if "title" in tc and "summary" not in tc:
585
+ tc["summary"] = tc.pop("title")
586
+
587
+ # Step 1: fetch existing linked TCs
588
+ existing_result = await asyncio.to_thread(
589
+ _get_linked_test_cases, cloud_id, issue_key
590
+ )
591
+ existing_summaries: set[str] = {
592
+ _normalise_summary(tc.get("summary", ""))
593
+ for tc in existing_result.get("test_cases", [])
594
+ }
595
+
596
+ # Step 2: filter — keep only net-new
597
+ net_new = [
598
+ tc for tc in proposed
599
+ if _normalise_summary(tc.get("summary", "")) not in existing_summaries
600
+ ]
601
+ skipped = len(proposed) - len(net_new)
602
+
603
+ if not net_new:
604
+ result = {
605
+ "status": "all_duplicates",
606
+ "skipped_count": skipped,
607
+ "created_count": 0,
608
+ "message": (
609
+ f"All {skipped} proposed test case(s) already exist as linked "
610
+ "test cases on this issue. Nothing was created."
611
+ ),
612
+ }
613
+ else:
614
+ # Step 3: delegate to create_and_link_test_cases
615
+ create_result_raw = await call_tool("create_and_link_test_cases", {
616
+ "issue_key": issue_key,
617
+ "cloud_id": cloud_id,
618
+ "test_cases": net_new,
619
+ "confirmed": True,
620
+ })
621
+ inner = json.loads(create_result_raw[0].text)
622
+ result = {
623
+ "status": "ok",
624
+ "skipped_count": skipped,
625
+ "created_count": len(net_new),
626
+ "create_result": inner,
627
+ }
628
+
629
+ elif name == "attach_gherkin_to_issue":
630
+ result = await asyncio.to_thread(
631
+ _attach_gherkin,
632
+ cloud_id,
633
+ arguments["issue_key"],
634
+ arguments["gherkin_content"],
635
+ arguments.get("issue_summary", ""),
636
+ )
637
+
431
638
  else:
432
639
  result = {"error": f"Unknown tool: {name}"}
433
640
 
@@ -465,4 +672,4 @@ def main():
465
672
 
466
673
 
467
674
  if __name__ == "__main__":
468
- main()
675
+ main()
@@ -8,6 +8,7 @@ Public API:
8
8
  create_test_case(cloud_id, project_key, title, steps, priority, labels, epic_link) -> dict
9
9
  link_test_cases_to_issue(cloud_id, issue_key, tc_keys) -> dict
10
10
  get_linked_test_cases(cloud_id, issue_key) -> dict
11
+ attach_gherkin_to_issue(cloud_id, issue_key, gherkin_content, file_name) -> dict
11
12
 
12
13
  Jira REST v3 base: https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3
13
14
  """
@@ -280,6 +281,41 @@ def get_linked_test_cases(cloud_id: str, issue_key: str) -> dict:
280
281
  }
281
282
 
282
283
 
284
+ # ---------------------------------------------------------------------------
285
+ # attach_gherkin_to_issue
286
+ # ---------------------------------------------------------------------------
287
+
288
+ def attach_gherkin_to_issue(cloud_id: str, issue_key: str, gherkin_content: str, file_name: str = "feature.feature") -> dict:
289
+ """Attach a .feature file to a Jira issue.
290
+
291
+ Args:
292
+ cloud_id: Jira cloud ID
293
+ issue_key: Issue key (e.g. PROJ-123)
294
+ gherkin_content: Full Gherkin feature file content
295
+ file_name: Name of the file to attach (default: feature.feature)
296
+
297
+ Returns:
298
+ dict with attachment metadata
299
+ """
300
+ headers = get_auth_headers()
301
+
302
+ # Upload attachment
303
+ url = f"{_base(cloud_id)}/issue/{issue_key}/attachments"
304
+ files = {"file": (file_name, gherkin_content, "text/plain")}
305
+
306
+ resp = requests.post(url, headers=headers, files=files, timeout=30)
307
+ resp.raise_for_status()
308
+ data = resp.json()
309
+
310
+ attachment = (data.get("results") or [{}])[0] if data.get("results") else {}
311
+ return {
312
+ "issue_key": issue_key,
313
+ "file_name": file_name,
314
+ "attachment_id": attachment.get("id", ""),
315
+ "success": bool(attachment.get("id")),
316
+ }
317
+
318
+
283
319
  # ---------------------------------------------------------------------------
284
320
  # Helpers
285
321
  # ---------------------------------------------------------------------------