@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qa-gentic/stlc-agents",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "QA STLC Agents — five MCP servers + skills for AI-powered test case, Gherkin, Playwright generation, and Helix-QA file writing against Azure DevOps and Jira Cloud. Full pipeline for both: fetch → test cases → Gherkin → Playwright → Helix-QA. Works with Claude Code, GitHub Copilot, Cursor, Windsurf.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -152,6 +152,11 @@ Each test case:
152
152
  - `steps` — array of `{ action, expected_result }` — at least 2 steps per TC
153
153
  - `priority` — 1=Critical, 2=High (default), 3=Medium, 4=Low
154
154
 
155
+ **Automatic post-creation actions (server-side — no skill action required):**
156
+ - Each `TestedBy-Forward` relation is created with `"attributes": {"comment": "STLC-Agent generated test case"}` — this comment appears in the Links tab Comments column in ADO.
157
+ - The tag `STLCAgentTestCases` is appended to the parent work item (PBI/Bug/Feature).
158
+ Both are best-effort; a failure does not roll back test case creation.
159
+
155
160
  ## Example tool call
156
161
 
157
162
  ```json
@@ -124,64 +124,88 @@ function readLogs() {
124
124
 
125
125
  // ── Printing ───────────────────────────────────────────────────────────────
126
126
 
127
+ function fmtTokens(r) {
128
+ const n = r.estimated_tokens || 0;
129
+ const exact = r.token_method === "exact";
130
+ const prefix = exact ? "" : "~";
131
+ return prefix + fmtTok(n);
132
+ }
133
+
127
134
  function printSession(sess) {
128
135
  const { sessionId, records } = sess;
129
136
  if (!records.length) return;
130
137
 
131
138
  const byServer = {};
132
- let totalCost = 0, totalTokens = 0;
139
+ let totalCost = 0, totalExactTokens = 0, totalEstTokens = 0;
140
+ let hasExact = false, hasEst = false;
141
+
133
142
  for (const r of records) {
134
143
  const k = r.server || "unknown";
135
- if (!byServer[k]) byServer[k] = { calls: 0, tokens: 0, cost: 0 };
144
+ if (!byServer[k]) byServer[k] = { calls: 0, tokens: 0, cost: 0, exact: false };
136
145
  byServer[k].calls++;
137
146
  byServer[k].tokens += r.estimated_tokens || 0;
138
147
  byServer[k].cost += r.cost_usd || 0;
139
- totalCost += r.cost_usd || 0;
140
- totalTokens += r.estimated_tokens || 0;
148
+ if (r.token_method === "exact") { byServer[k].exact = true; hasExact = true; }
149
+ else { hasEst = true; }
150
+ totalCost += r.cost_usd || 0;
151
+ if (r.token_method === "exact") totalExactTokens += r.estimated_tokens || 0;
152
+ else totalEstTokens += r.estimated_tokens || 0;
141
153
  }
142
154
 
143
- const model = records[0]?.model || "unknown";
144
- const source = records[0]?.model_source || "";
145
- const method = records.some((r) => r.token_method === "estimated")
146
- ? "estimated (payload chars÷4)" : "exact";
155
+ const methodLabel = hasExact && hasEst ? "mixed (exact + estimated)"
156
+ : hasExact ? "exact (from API response)"
157
+ : "estimated (payload chars÷4)";
158
+
159
+ const models = [...new Set(records.map((r) => r.model).filter(Boolean))].join(", ");
160
+ const sources = [...new Set(records.map((r) => r.model_source).filter(Boolean))].join(", ");
147
161
  const ts = records[0]?.timestamp?.slice(0, 19).replace("T", " ") || "";
148
162
  const tsEnd = records[records.length - 1]?.timestamp?.slice(0, 19).replace("T", " ") || "";
149
163
 
150
- console.log(`\n${"═".repeat(68)}`);
164
+ console.log(`\n${"═".repeat(72)}`);
151
165
  console.log(b(` stlc-agents · Cost Report · ${sessionId}`));
152
- console.log(`${"═".repeat(68)}`);
166
+ console.log(`${"═".repeat(72)}`);
153
167
  console.log(dim(` ${ts} → ${tsEnd}`));
154
- console.log(dim(` Model: ${model} (detected via: ${source})`));
155
- console.log(dim(` Token method: ${method}`));
168
+ console.log(dim(` Model(s): ${models} (via: ${sources})`));
169
+ console.log(dim(` Token method: ${methodLabel}`));
156
170
 
157
171
  // Per-server
158
- console.log(`\n ${"Server".padEnd(30)} ${"Calls".padStart(6)} ${"~Tokens".padStart(10)} ${"Cost (USD)".padStart(14)}`);
159
- console.log(` ${"─".repeat(60)}`);
172
+ console.log(`\n ${"Server".padEnd(30)} ${"Calls".padStart(6)} ${"Tokens".padStart(10)} ${"Cost (USD)".padStart(14)} Method`);
173
+ console.log(` ${"─".repeat(70)}`);
160
174
  for (const [svr, d] of Object.entries(byServer)) {
175
+ const meth = d.exact ? "" : dim(" [est]");
176
+ const prefix = d.exact ? "" : "~";
161
177
  console.log(
162
178
  ` ${cyn(svr.padEnd(30))} ${String(d.calls).padStart(6)} ` +
163
- `${fmtTok(d.tokens).padStart(10)} ${grn(fmtUsd(d.cost).padStart(14))}`
179
+ `${(prefix + fmtTok(d.tokens)).padStart(10)} ${grn(fmtUsd(d.cost).padStart(14))}${meth}`
164
180
  );
165
181
  }
166
182
 
167
183
  // Per-step
168
184
  console.log(`\n ${"Step detail"}`);
169
- console.log(` ${"─".repeat(68)}`);
185
+ console.log(` ${"─".repeat(72)}`);
170
186
  for (const r of records) {
187
+ const exact = r.token_method === "exact";
188
+ const methTag = exact ? "" : dim(" [est]");
189
+ const cacheNote = (r.cache_write_tokens || r.cache_read_tokens)
190
+ ? dim(` cw=${r.cache_write_tokens||0} cr=${r.cache_read_tokens||0}`) : "";
191
+ const iters = r.iterations > 1 ? dim(` ×${r.iterations}`) : "";
171
192
  console.log(
172
- ` ${(r.server || "?").padEnd(26)} ${(r.tool || "?").padEnd(36)} ` +
173
- `${fmtTok(r.estimated_tokens || 0).padStart(6)} ` +
174
- `${grn(fmtUsd(r.cost_usd || 0))} ${fmtMs(r.latency_ms || 0)} ` +
175
- dim(`[${r.token_method || "?"}]`)
193
+ ` ${(r.server || "?").padEnd(26)} ${(r.tool || "?").padEnd(34)} ` +
194
+ `${fmtTokens(r).padStart(8)} ` +
195
+ `${grn(fmtUsd(r.cost_usd || 0))} ${fmtMs(r.latency_ms || 0)}${methTag}${cacheNote}${iters}`
176
196
  );
177
197
  }
178
198
 
179
199
  // Totals
180
- console.log(`\n ${"─".repeat(68)}`);
181
- console.log(` ${"Total tokens".padEnd(40)} ${fmtTok(totalTokens).padStart(10)}`);
200
+ const totalTokens = totalExactTokens + totalEstTokens;
201
+ const tokenNote = hasExact && hasEst
202
+ ? ` (${fmtTok(totalExactTokens)} exact + ~${fmtTok(totalEstTokens)} est)`
203
+ : hasExact ? " (exact)" : " (estimated)";
204
+ console.log(`\n ${"─".repeat(72)}`);
205
+ console.log(` ${"Total tokens".padEnd(40)} ${fmtTok(totalTokens).padStart(10)}${dim(tokenNote)}`);
182
206
  console.log(` ${b("Total cost".padEnd(40))} ${grn(fmtUsd(totalCost))}`);
183
207
  console.log(dim(` Log: ${sess.file}`));
184
- console.log(`${"═".repeat(68)}\n`);
208
+ console.log(`${"═".repeat(72)}\n`);
185
209
  }
186
210
 
187
211
  // ── Main ───────────────────────────────────────────────────────────────────
@@ -235,16 +259,23 @@ module.exports = async function cost(opts) {
235
259
 
236
260
  if (opts.all) {
237
261
  for (const s of sessions) printSession(s);
238
- const allRecords = sessions.flatMap((s) => s.records);
239
- const grandTotal = allRecords.reduce((a, r) => a + (r.cost_usd || 0), 0);
240
- const grandTokens = allRecords.reduce((a, r) => a + (r.estimated_tokens || 0), 0);
241
- console.log(`${"═".repeat(68)}`);
262
+ const allRecords = sessions.flatMap((s) => s.records);
263
+ const grandTotal = allRecords.reduce((a, r) => a + (r.cost_usd || 0), 0);
264
+ const grandExact = allRecords.filter((r) => r.token_method === "exact")
265
+ .reduce((a, r) => a + (r.estimated_tokens || 0), 0);
266
+ const grandEst = allRecords.filter((r) => r.token_method !== "exact")
267
+ .reduce((a, r) => a + (r.estimated_tokens || 0), 0);
268
+ const grandTokens = grandExact + grandEst;
269
+ const tokenBreakdown = grandExact && grandEst
270
+ ? ` (${fmtTok(grandExact)} exact + ~${fmtTok(grandEst)} est)`
271
+ : grandExact ? " (exact)" : " (estimated)";
272
+ console.log(`${"═".repeat(72)}`);
242
273
  console.log(b(` All sessions — grand total`));
243
- console.log(`${"═".repeat(68)}`);
274
+ console.log(`${"═".repeat(72)}`);
244
275
  console.log(` Sessions : ${sessions.length}`);
245
- console.log(` Total tokens: ${fmtTok(grandTokens)}`);
276
+ console.log(` Total tokens: ${fmtTok(grandTokens)}${dim(tokenBreakdown)}`);
246
277
  console.log(b(` TOTAL COST : ${grn(fmtUsd(grandTotal))}`));
247
- console.log(`${"═".repeat(68)}\n`);
278
+ console.log(`${"═".repeat(72)}\n`);
248
279
  return;
249
280
  }
250
281
 
@@ -80,14 +80,94 @@ module.exports = async function init(opts) {
80
80
 
81
81
  // ── 3. pip install qa-gentic-stlc-agents ──────────────────────────────────
82
82
  info("Installing qa-gentic-stlc-agents (pip)…");
83
- const pip = spawnSync(python, ["-m", "pip", "install", "qa-gentic-stlc-agents>=1.0.1", "--quiet"], {
84
- stdio: "inherit",
85
- encoding: "utf8",
86
- });
87
- if (pip.status !== 0) {
88
- die("pip install failed. Run manually: pip install qa-gentic-stlc-agents");
83
+
84
+ const IS_WIN = process.platform === "win32";
85
+ const VENV_DIR = path.join(os.homedir(), ".qa-stlc", "venv");
86
+ const venvPython = IS_WIN
87
+ ? path.join(VENV_DIR, "Scripts", "python.exe")
88
+ : path.join(VENV_DIR, "bin", "python3");
89
+
90
+ // Helper: check if the package is importable by a given python binary
91
+ function isImportable(pyBin) {
92
+ if (!fs.existsSync(pyBin) && pyBin !== python) return false;
93
+ const r = spawnSync(pyBin, ["-c", "import qa_gentic_stlc_agents"], { encoding: "utf8" });
94
+ return r.status === 0;
95
+ }
96
+
97
+ // Find a Python 3.10–3.13 binary compatible with qa-gentic-stlc-agents.
98
+ // The package declares Requires-Python >=3.10,<3.14 so we must avoid 3.14+.
99
+ function findCompatiblePython(preferred) {
100
+ const candidates = IS_WIN
101
+ ? ["py", "python", "python3"]
102
+ : ["python3.13", "python3.12", "python3.11", "python3.10", preferred];
103
+
104
+ for (const bin of candidates) {
105
+ const r = spawnSync(bin, ["--version"], { encoding: "utf8" });
106
+ if (r.status !== 0) continue;
107
+ const ver = (r.stdout || r.stderr || "").trim();
108
+ const m = ver.match(/Python (\d+)\.(\d+)/);
109
+ if (!m) continue;
110
+ const major = parseInt(m[1]), minor = parseInt(m[2]);
111
+ if (major === 3 && minor >= 10 && minor <= 13) return bin;
112
+ }
113
+ return null;
89
114
  }
90
- ok("qa-gentic-stlc-agents installed.");
115
+
116
+ // Determine which python to use for MCP servers — may be updated to venv python below
117
+ let resolvedPython = python;
118
+
119
+ if (isImportable(python)) {
120
+ // Already importable by the user-supplied / system python (CI, active venv, etc.)
121
+ ok("qa-gentic-stlc-agents already installed — skipping pip install.");
122
+ } else if (isImportable(venvPython)) {
123
+ // Package found in the persistent qa-stlc venv from a previous run
124
+ ok("qa-gentic-stlc-agents found in ~/.qa-stlc/venv — skipping pip install.");
125
+ resolvedPython = venvPython;
126
+ } else {
127
+ // Create (or reuse) a dedicated venv and install there.
128
+ // Bypasses PEP 668 on Mac/Linux Homebrew Python and works on Windows & CI
129
+ // without requiring elevated permissions or breaking the system Python.
130
+
131
+ // Find a Python 3.10–3.13 binary (package does not support 3.14+ yet)
132
+ const compatPython = findCompatiblePython(python);
133
+ if (!compatPython) {
134
+ die(
135
+ "qa-gentic-stlc-agents requires Python 3.10–3.13 but none was found.\n" +
136
+ " Mac: brew install python@3.13\n" +
137
+ " Linux: sudo apt install python3.13\n" +
138
+ " Windows: install Python 3.13 from python.org\n" +
139
+ " Then re-run: qa-stlc init --python python3.13 --vscode --integration ado"
140
+ );
141
+ }
142
+
143
+ const compatPythonVer = (spawnSync(compatPython, ["--version"], { encoding: "utf8" }).stdout || "").trim();
144
+ info(`Using ${compatPython} (${compatPythonVer}) for venv…`);
145
+
146
+ if (!fs.existsSync(VENV_DIR)) {
147
+ info(`Creating Python venv at ${VENV_DIR}…`);
148
+ const mkVenv = spawnSync(compatPython, ["-m", "venv", VENV_DIR], { stdio: "inherit", encoding: "utf8" });
149
+ if (mkVenv.status !== 0) {
150
+ die(`Failed to create venv. Ensure python3-venv is installed:\n ${compatPython} -m venv ${VENV_DIR}`);
151
+ }
152
+ ok("Venv created.");
153
+ }
154
+
155
+ info("Installing qa-gentic-stlc-agents into venv…");
156
+ const pip = spawnSync(
157
+ venvPython,
158
+ ["-m", "pip", "install", "qa-gentic-stlc-agents>=1.0.1", "--quiet"],
159
+ { stdio: "inherit", encoding: "utf8" }
160
+ );
161
+
162
+ if (pip.status !== 0) {
163
+ die(`pip install into venv failed. Try manually:\n ${venvPython} -m pip install qa-gentic-stlc-agents`);
164
+ }
165
+ ok("qa-gentic-stlc-agents installed into ~/.qa-stlc/venv.");
166
+ resolvedPython = venvPython;
167
+ }
168
+
169
+ // Propagate the resolved python so MCP config points to the correct interpreter
170
+ opts.python = resolvedPython;
91
171
 
92
172
  // ── 4. Copy ORCHESTRATION_RULES.md to project root ─────────────────────────
93
173
  info("Installing ORCHESTRATION_RULES.md to project root…");
@@ -115,7 +195,7 @@ module.exports = async function init(opts) {
115
195
  await cmdMcpConfig({
116
196
  vscode: opts.vscode || false,
117
197
  print: false,
118
- python: python,
198
+ python: resolvedPython,
119
199
  playwrightPort: "8931",
120
200
  integration,
121
201
  });
@@ -23,7 +23,9 @@ import asyncio
23
23
  import json
24
24
  import re
25
25
  import sys
26
+ import tempfile
26
27
  import time
28
+ from pathlib import Path
27
29
 
28
30
  from dotenv import load_dotenv
29
31
  from mcp.server import Server
@@ -298,8 +300,9 @@ async def list_tools() -> list[types.Tool]:
298
300
  description=(
299
301
  "Write generated TypeScript/Gherkin files into the Helix-QA directory layout "
300
302
  "with full deduplication and interface adaptation.\n\n"
301
- "Pass the 'files' dict from qa-playwright-generator:generate_playwright_code "
302
- "or scaffold_locator_repository directly.\n\n"
303
+ "Preferred: pass cache_key from generate_playwright_code "
304
+ "the server loads the files from disk automatically. "
305
+ "Alternative: pass the 'files' dict directly.\n\n"
303
306
  "mode='tests_only' (default, safe to run repeatedly):\n"
304
307
  " Writes locators.ts, *.page.ts, *.steps.ts, *.feature only.\n"
305
308
  " Infrastructure files (LocatorHealer.ts etc.) are always skipped.\n"
@@ -326,11 +329,20 @@ async def list_tools() -> list[types.Tool]:
326
329
  "type": "string",
327
330
  "description": "Absolute path to the Helix-QA project root.",
328
331
  },
332
+ "cache_key": {
333
+ "type": "string",
334
+ "description": (
335
+ "Cache key returned by generate_playwright_code or "
336
+ "scaffold_locator_repository. Preferred over passing 'files' directly — "
337
+ "the server loads the files from disk automatically."
338
+ ),
339
+ },
329
340
  "files": {
330
341
  "type": "object",
331
342
  "description": (
332
343
  "Dict of { file_key: file_content } as returned by "
333
- "generate_playwright_code or scaffold_locator_repository."
344
+ "generate_playwright_code or scaffold_locator_repository. "
345
+ "Use cache_key instead whenever possible."
334
346
  ),
335
347
  "additionalProperties": {"type": "string"},
336
348
  },
@@ -351,7 +363,7 @@ async def list_tools() -> list[types.Tool]:
351
363
  ),
352
364
  },
353
365
  },
354
- "required": ["helix_root", "files"],
366
+ "required": ["helix_root"],
355
367
  },
356
368
  ),
357
369
  types.Tool(
@@ -458,8 +470,31 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
458
470
  result["_validation"] = _validate_inspect_result(result)
459
471
 
460
472
  elif name == "write_helix_files":
473
+ # ── Resolve files: cache_key takes priority over inline dict ──
474
+ files = arguments.get("files") or {}
475
+ # Normalise: LLMs sometimes send [{file_name, content}] instead of {path: content}
476
+ if isinstance(files, list):
477
+ files = {
478
+ item.get("file_name") or item.get("path") or item.get("name", ""): item.get("content", "")
479
+ for item in files
480
+ if isinstance(item, dict)
481
+ }
482
+ cache_key = arguments.get("cache_key", "").strip()
483
+ if cache_key and not files:
484
+ _cache_dir = Path(tempfile.gettempdir()) / "stlc_file_cache"
485
+ cache_file = _cache_dir / f"{cache_key}.json"
486
+ if cache_file.exists():
487
+ files = json.loads(cache_file.read_text())
488
+ else:
489
+ result = {
490
+ "success": False,
491
+ "error": f"cache_key '{cache_key}' not found — file does not exist at {cache_file}. "
492
+ "Either pass the files dict directly or call get_generated_files first.",
493
+ "_validation": {"valid": False, "errors": [f"cache_key '{cache_key}' not found"], "warnings": []},
494
+ }
495
+ return track(result, tool_name=name, server="qa-helix-writer", t0=t0)
461
496
  # ── Pre-write input validation ────────────────────────────────
462
- input_validation = _validate_write_inputs(arguments.get("files", {}))
497
+ input_validation = _validate_write_inputs(files)
463
498
  if not input_validation["valid"]:
464
499
  result = {
465
500
  "success": False,
@@ -475,7 +510,7 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
475
510
  result = await asyncio.to_thread(
476
511
  _write_files,
477
512
  arguments["helix_root"],
478
- arguments["files"],
513
+ files,
479
514
  arguments.get("mode", "tests_only"),
480
515
  arguments.get("force_scaffold", False),
481
516
  )