@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
@@ -0,0 +1,154 @@
1
+ """
2
+ install_hook.py — stlc_agents.shared.install_hook
3
+ ─────────────────────────────────────────────────────
4
+ Called automatically by postinstall.js after `pip install qa-gentic-stlc-agents`.
5
+ Also callable manually: python -m stlc_agents.shared.install_hook
6
+
7
+ Applies the cost tracking patch to all 5 MCP server files by importing
8
+ and running the same logic as scripts/apply_cost_tracking.py, but resolved
9
+ relative to the installed package location (works in site-packages, .venv, etc).
10
+ """
11
+
12
+ from __future__ import annotations
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ SERVERS = [
19
+ ("agent_gherkin_generator", "qa-gherkin-generator"),
20
+ ("agent_test_case_manager", "qa-test-case-manager"),
21
+ ("agent_playwright_generator", "qa-playwright-generator"),
22
+ ("agent_helix_writer", "qa-helix-writer"),
23
+ ("agent_jira_manager", "qa-jira-manager"),
24
+ ]
25
+
26
+ IMPORT_MARKER = "from stlc_agents.shared.cost_tracker import track"
27
+ TIME_IMPORT = "import time"
28
+
29
+ OLD_RETURN = (
30
+ 'return [types.TextContent(type="text", '
31
+ 'text=json.dumps(result, indent=2, ensure_ascii=False))]'
32
+ )
33
+ NEW_RETURN = "return track(result, tool_name=name, server={server!r}, t0=t0)"
34
+
35
+ OLD_ERR_BLOCK = """\
36
+ return [types.TextContent(
37
+ type="text",
38
+ text=json.dumps({"error": str(exc), "tool": name}, indent=2),
39
+ )]"""
40
+
41
+ NEW_ERR_BLOCK = """\
42
+ err_result = {{"error": str(exc), "tool": name}}
43
+ return track(err_result, tool_name=name, server={server!r}, t0=t0)"""
44
+
45
+
46
+ def _root() -> Path:
47
+ """Resolve the stlc_agents package root regardless of install method."""
48
+ import stlc_agents
49
+ return Path(stlc_agents.__file__).parent
50
+
51
+
52
+ def patch_server(agent_dir: str, server_name: str, root: Path) -> str:
53
+ """Patch one server file. Returns 'patched' | 'already_patched' | 'not_found' | 'no_change'."""
54
+ path = root / agent_dir / "server.py"
55
+ if not path.exists():
56
+ return "not_found"
57
+
58
+ src = path.read_text(encoding="utf-8")
59
+ if IMPORT_MARKER in src:
60
+ return "already_patched"
61
+
62
+ original = src
63
+
64
+ # 1. import time
65
+ if TIME_IMPORT not in src:
66
+ src = src.replace("import sys\n", "import sys\nimport time\n", 1)
67
+
68
+ # 2. cost_tracker import — after last `from stlc_agents...` line
69
+ last_match = None
70
+ for m in re.finditer(r"^from stlc_agents\..+\n", src, re.MULTILINE):
71
+ last_match = m
72
+ if last_match:
73
+ pos = last_match.end()
74
+ src = src[:pos] + "from stlc_agents.shared.cost_tracker import track\n" + src[pos:]
75
+ else:
76
+ src = src.replace(
77
+ "from dotenv import load_dotenv\n",
78
+ "from dotenv import load_dotenv\nfrom stlc_agents.shared.cost_tracker import track\n",
79
+ 1,
80
+ )
81
+
82
+ # 3. t0 = time.monotonic() inside call_tool()
83
+ src = src.replace(
84
+ "@app.call_tool()\nasync def call_tool(name: str, arguments: dict)"
85
+ " -> list[types.TextContent]:\n try:\n",
86
+ "@app.call_tool()\nasync def call_tool(name: str, arguments: dict)"
87
+ " -> list[types.TextContent]:\n t0 = time.monotonic()\n try:\n",
88
+ 1,
89
+ )
90
+
91
+ # 4. Replace all result return lines
92
+ new_ret = NEW_RETURN.format(server=server_name)
93
+ for indent in (" ", " ", " "):
94
+ src = src.replace(f"{indent}{OLD_RETURN}", f"{indent}{new_ret}")
95
+
96
+ # 5. Replace error-path block
97
+ src = src.replace(OLD_ERR_BLOCK, NEW_ERR_BLOCK.format(server=server_name))
98
+
99
+ if src == original:
100
+ return "no_change"
101
+
102
+ path.write_text(src, encoding="utf-8")
103
+ return "patched"
104
+
105
+
106
+ def apply_cost_tracking() -> None:
107
+ """Entry point — called by postinstall.js and the console_script."""
108
+ root = _root()
109
+
110
+ ok = "\x1b[32m✓\x1b[0m"
111
+ skip = "\x1b[33m–\x1b[0m"
112
+ err = "\x1b[31m✗\x1b[0m"
113
+
114
+ print("\n stlc-agents · Activating cost tracking on MCP servers...\n")
115
+
116
+ any_patched = False
117
+ for agent_dir, server_name in SERVERS:
118
+ status = patch_server(agent_dir, server_name, root)
119
+ if status == "patched":
120
+ print(f" {ok} {agent_dir} ({server_name})")
121
+ any_patched = True
122
+ elif status == "already_patched":
123
+ print(f" {skip} {agent_dir} — already active")
124
+ elif status == "not_found":
125
+ print(f" {err} {agent_dir}/server.py — not found (skip)")
126
+ elif status == "no_change":
127
+ print(f" {skip} {agent_dir} — no matching pattern (manual patch needed)")
128
+
129
+ print()
130
+ if any_patched:
131
+ print(" Cost tracking is now active. On every MCP tool call you will see:")
132
+ print(" [stlc-cost] <server> · <tool> ~<N>K tokens $<cost> (session: $<total>)")
133
+ print()
134
+ print(" Session logs: ~/.qa-stlc/cost-<session-id>.jsonl")
135
+ print(" View report: qa-stlc cost")
136
+ print(" View all: qa-stlc cost --all")
137
+ print()
138
+ print(" Environment variables:")
139
+ print(" STLC_COST_TRACKING=false disable output")
140
+ print(" STLC_CODING_AGENT_MODEL=<model> set your agent's model for exact pricing")
141
+ print(" e.g. claude-sonnet-4-6 | claude-opus-4-6 | gpt-4o")
142
+ print(" STLC_COST_LOG_DIR=<path> change log directory")
143
+ else:
144
+ print(" All servers already have cost tracking active.")
145
+
146
+ print()
147
+
148
+
149
+ def main() -> None:
150
+ apply_cost_tracking()
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
@@ -0,0 +1,72 @@
1
+ """
2
+ pricing.py — Model pricing registry for stlc-agents cost tracking.
3
+
4
+ Prices: USD per million tokens (MTok).
5
+ Source: Anthropic official docs, April 2026.
6
+
7
+ Models this repo actually calls:
8
+ - claude-sonnet-4-20250514 (LocatorHealer AI Vision, default)
9
+ - gpt-4o (LocatorHealer AI Vision, copilot provider)
10
+ + whatever coding agent the user runs (Claude / Copilot / Cursor / Windsurf)
11
+ — the user declares this via STLC_CODING_AGENT_MODEL env var.
12
+ """
13
+
14
+ from __future__ import annotations
15
+ from dataclasses import dataclass
16
+ from typing import Optional
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ModelPricing:
21
+ model_id: str
22
+ display_name: str
23
+ provider: str
24
+ input_per_mtok: float # USD / 1M input tokens
25
+ output_per_mtok: float # USD / 1M output tokens
26
+ cache_write_per_mtok: float # USD / 1M cache-write tokens
27
+ cache_read_per_mtok: float # USD / 1M cache-read tokens
28
+
29
+ def cost(
30
+ self,
31
+ input_tokens: int = 0,
32
+ output_tokens: int = 0,
33
+ cache_write_tokens: int = 0,
34
+ cache_read_tokens: int = 0,
35
+ ) -> float:
36
+ return (
37
+ (input_tokens / 1_000_000) * self.input_per_mtok
38
+ + (output_tokens / 1_000_000) * self.output_per_mtok
39
+ + (cache_write_tokens/ 1_000_000) * self.cache_write_per_mtok
40
+ + (cache_read_tokens / 1_000_000) * self.cache_read_per_mtok
41
+ )
42
+
43
+
44
+ _REGISTRY: list[ModelPricing] = [
45
+ # ── Anthropic ──────────────────────────────────────────────────────────
46
+ ModelPricing("claude-sonnet-4-20250514", "Claude Sonnet 4", "anthropic", 3.00, 15.00, 3.75, 0.30),
47
+ ModelPricing("claude-sonnet-4-6", "Claude Sonnet 4.6", "anthropic", 3.00, 15.00, 3.75, 0.30),
48
+ ModelPricing("claude-haiku-4-5-20251001","Claude Haiku 4.5", "anthropic", 1.00, 5.00, 1.25, 0.10),
49
+ ModelPricing("claude-opus-4-6", "Claude Opus 4.6", "anthropic", 5.00, 25.00, 6.25, 0.50),
50
+ ModelPricing("claude-opus-4-7", "Claude Opus 4.7", "anthropic", 5.00, 25.00, 6.25, 0.50),
51
+ # ── OpenAI / Copilot ──────────────────────────────────────────────────
52
+ ModelPricing("gpt-4o", "GPT-4o", "openai", 2.50, 10.00, 0.00, 0.00),
53
+ ModelPricing("gpt-4o-mini", "GPT-4o Mini", "openai", 0.15, 0.60, 0.00, 0.00),
54
+ ]
55
+
56
+ _by_id: dict[str, ModelPricing] = {p.model_id: p for p in _REGISTRY}
57
+
58
+
59
+ def get_pricing(model_id: str) -> Optional[ModelPricing]:
60
+ """Exact match first, then longest substring match."""
61
+ key = model_id.lower().strip()
62
+ if key in _by_id:
63
+ return _by_id[key]
64
+ # Substring: "claude-sonnet-4-20250514" ⊇ "sonnet-4"
65
+ for p in _REGISTRY:
66
+ if key in p.model_id or p.model_id in key:
67
+ return p
68
+ return None
69
+
70
+
71
+ def list_models() -> list[ModelPricing]:
72
+ return list(_REGISTRY)