@qa-gentic/agents 1.1.2

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 (52) hide show
  1. package/README.md +203 -0
  2. package/bin/postinstall.js +75 -0
  3. package/bin/qa-stlc.js +76 -0
  4. package/package.json +48 -0
  5. package/skills/qa-stlc/AGENT-BEHAVIOR.md +373 -0
  6. package/skills/qa-stlc/deduplication-protocol.md +303 -0
  7. package/skills/qa-stlc/generate-gherkin.md +550 -0
  8. package/skills/qa-stlc/generate-playwright-code.md +439 -0
  9. package/skills/qa-stlc/generate-test-cases.md +176 -0
  10. package/skills/qa-stlc/write-helix-files.md +349 -0
  11. package/src/cmd-init.js +84 -0
  12. package/src/cmd-mcp-config.js +177 -0
  13. package/src/cmd-skills.js +124 -0
  14. package/src/cmd-verify.js +129 -0
  15. package/src/qa_stlc_agents/__init__.py +0 -0
  16. package/src/qa_stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
  17. package/src/qa_stlc_agents/agent_gherkin_generator/__init__.py +0 -0
  18. package/src/qa_stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  19. package/src/qa_stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
  20. package/src/qa_stlc_agents/agent_gherkin_generator/server.py +502 -0
  21. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__init__.py +0 -0
  22. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  23. package/src/qa_stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
  24. package/src/qa_stlc_agents/agent_gherkin_generator/tools/ado_gherkin.py +854 -0
  25. package/src/qa_stlc_agents/agent_helix_writer/__init__.py +0 -0
  26. package/src/qa_stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
  27. package/src/qa_stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
  28. package/src/qa_stlc_agents/agent_helix_writer/server.py +529 -0
  29. package/src/qa_stlc_agents/agent_helix_writer/tools/__init__.py +0 -0
  30. package/src/qa_stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  31. package/src/qa_stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
  32. package/src/qa_stlc_agents/agent_helix_writer/tools/helix_write.py +622 -0
  33. package/src/qa_stlc_agents/agent_playwright_generator/__init__.py +0 -0
  34. package/src/qa_stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
  35. package/src/qa_stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
  36. package/src/qa_stlc_agents/agent_playwright_generator/server.py +2771 -0
  37. package/src/qa_stlc_agents/agent_playwright_generator/tools/__init__.py +0 -0
  38. package/src/qa_stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  39. package/src/qa_stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
  40. package/src/qa_stlc_agents/agent_playwright_generator/tools/ado_attach.py +62 -0
  41. package/src/qa_stlc_agents/agent_test_case_manager/__init__.py +0 -0
  42. package/src/qa_stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
  43. package/src/qa_stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
  44. package/src/qa_stlc_agents/agent_test_case_manager/server.py +483 -0
  45. package/src/qa_stlc_agents/agent_test_case_manager/tools/__init__.py +0 -0
  46. package/src/qa_stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
  47. package/src/qa_stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
  48. package/src/qa_stlc_agents/agent_test_case_manager/tools/ado_workitem.py +302 -0
  49. package/src/qa_stlc_agents/shared/__init__.py +0 -0
  50. package/src/qa_stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/qa_stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
  52. package/src/qa_stlc_agents/shared/auth.py +119 -0
@@ -0,0 +1,529 @@
1
+ """
2
+ Agent 4: Helix-QA Framework Writer — MCP Server
3
+
4
+ Writes files produced by agent_playwright_generator into the correct directory
5
+ layout of a local Helix-QA project (Playwright + TypeScript + Cucumber + self-healing).
6
+
7
+ No LLM calls. No ADO calls. Pure file-system operations.
8
+
9
+ Four tools:
10
+ inspect_helix_project — Detect framework state before writing anything.
11
+ Returns "absent" | "partial" | "present" and a
12
+ recommended write mode.
13
+ write_helix_files — Write generated files with full deduplication and
14
+ interface adaptation.
15
+ read_helix_file — Read one existing file (pre-write content check).
16
+ list_helix_tree — List all .ts / .feature files grouped by category.
17
+
18
+ Skills: see skills/write-helix-files.md
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import json
24
+ import re
25
+ import sys
26
+
27
+ from dotenv import load_dotenv
28
+ from mcp.server import Server
29
+ from mcp.server.stdio import stdio_server
30
+ from mcp import types
31
+
32
+ from qa_stlc_agents.agent_helix_writer.tools.helix_write import (
33
+ inspect_helix_project as _inspect,
34
+ write_files_to_helix as _write_files,
35
+ read_helix_file as _read_file,
36
+ list_helix_tree as _list_tree,
37
+ update_helix_file as _update_file,
38
+ )
39
+
40
+ load_dotenv()
41
+
42
+ app = Server("qa-helix-writer")
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Pre-output validation helpers
47
+ # ---------------------------------------------------------------------------
48
+
49
+ def _validate_write_inputs(files: dict) -> dict:
50
+ """Validate file dict before writing to Helix-QA project.
51
+
52
+ Checks:
53
+ 1. Files dict is non-empty.
54
+ 2. Every file has non-empty content.
55
+ 3. File keys use consistent naming patterns.
56
+ 4. Basic TS syntax (brace/paren balance) for .ts files.
57
+
58
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
59
+ """
60
+ errors: list[str] = []
61
+ warnings: list[str] = []
62
+
63
+ if not files:
64
+ errors.append("No files provided. Pass the 'files' dict from generate_playwright_code.")
65
+ return {"valid": False, "errors": errors, "warnings": warnings}
66
+
67
+ for file_key, content in files.items():
68
+ if not content or not content.strip():
69
+ errors.append(f"'{file_key}': content is empty.")
70
+ continue
71
+
72
+ # TS syntax balance check
73
+ if file_key.endswith(".ts"):
74
+ s = re.sub(r'`[^`]*`', '``', content)
75
+ s = re.sub(r'"[^"\n]*"', '""', s)
76
+ s = re.sub(r"'[^'\n]*'", "''", s)
77
+ s = re.sub(r'//[^\n]*', '', s)
78
+ s = re.sub(r'/\*.*?\*/', '', s, flags=re.DOTALL)
79
+ brace_delta = s.count('{') - s.count('}')
80
+ if brace_delta != 0:
81
+ errors.append(f"'{file_key}': unbalanced braces (delta={brace_delta:+d})")
82
+ paren_delta = s.count('(') - s.count(')')
83
+ if paren_delta != 0:
84
+ errors.append(
85
+ f"'{file_key}': unbalanced parentheses (delta={paren_delta:+d}). "
86
+ f"Cause: the regex-based string preprocessor strips template literals, "
87
+ f"quoted strings, and comments before counting — any unescaped '(' or ')' "
88
+ f"inside a raw regex step pattern will be miscounted. "
89
+ f"Fix: replace raw regex step patterns (e.g. /^I click (.+)$/) with "
90
+ f"Cucumber expressions (e.g. 'I click {{string}}') so parentheses appear "
91
+ f"only inside well-formed TypeScript function signatures."
92
+ )
93
+ if re.search(r"""from\s+['"]{2}""", content):
94
+ errors.append(f"'{file_key}': empty import path (from \"\")")
95
+
96
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
97
+
98
+
99
+ def _validate_write_result(result: dict) -> dict:
100
+ """Validate write_files_to_helix result before returning to user.
101
+
102
+ Checks:
103
+ 1. At least one file was written (or all were intentionally skipped).
104
+ 2. No unexpected failures.
105
+ 3. Deduplication report is consistent.
106
+
107
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
108
+ """
109
+ errors: list[str] = []
110
+ warnings: list[str] = []
111
+
112
+ if not result.get("success", False) and result.get("error"):
113
+ errors.append(f"Write failed: {result['error']}")
114
+ return {"valid": False, "errors": errors, "warnings": warnings}
115
+
116
+ written = result.get("written", [])
117
+ skipped = result.get("skipped", [])
118
+ summary = result.get("summary", {})
119
+
120
+ if not written and skipped:
121
+ # All files were skipped — check if intentional
122
+ skip_reasons = [s.get("reason", "") for s in skipped]
123
+ infra_skips = [r for r in skip_reasons if "infrastructure" in r.lower() or "already exists" in r.lower()]
124
+ if len(infra_skips) == len(skipped):
125
+ warnings.append("All files were skipped (already exist or infrastructure-only). No new content written.")
126
+ else:
127
+ error_skips = [s for s in skipped if "error" in s.get("reason", "").lower() or "empty" in s.get("reason", "").lower()]
128
+ if error_skips:
129
+ errors.append(
130
+ f"{len(error_skips)} file(s) skipped due to errors: "
131
+ + ", ".join(s.get("file_key", "?") for s in error_skips)
132
+ )
133
+
134
+ # Check for write count mismatches
135
+ if summary.get("requested", 0) > 0:
136
+ write_rate = summary.get("written", 0) / summary["requested"]
137
+ if write_rate == 0 and not skipped:
138
+ errors.append("No files were written and none were skipped — unexpected state.")
139
+ elif write_rate < 0.5 and summary["requested"] > 2:
140
+ warnings.append(
141
+ f"Only {summary.get('written', 0)}/{summary['requested']} files were written. "
142
+ f"Check skipped files for issues."
143
+ )
144
+
145
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
146
+
147
+
148
+ def _validate_inspect_result(result: dict) -> dict:
149
+ """Validate inspect_helix_project result before returning to user.
150
+
151
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
152
+ """
153
+ errors: list[str] = []
154
+ warnings: list[str] = []
155
+
156
+ state = result.get("framework_state", "")
157
+ if state not in ("absent", "partial", "present"):
158
+ errors.append(f"Unknown framework_state: '{state}'")
159
+
160
+ if state == "absent":
161
+ warnings.append(
162
+ "Helix-QA framework is absent. Run scaffold_locator_repository "
163
+ "before writing test files."
164
+ )
165
+ elif state == "partial":
166
+ missing = result.get("missing_infra", [])
167
+ warnings.append(
168
+ f"Helix-QA framework is partially set up. Missing: {missing}. "
169
+ f"Run scaffold_locator_repository to complete the setup."
170
+ )
171
+
172
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
173
+
174
+
175
+ def _validate_read_file_response(result: dict) -> dict:
176
+ """Validate read_helix_file result before returning to user.
177
+
178
+ Checks:
179
+ 1. No error in response.
180
+ 2. File content was actually returned.
181
+ 3. Warns if content is suspiciously short.
182
+
183
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
184
+ """
185
+ errors: list[str] = []
186
+ warnings: list[str] = []
187
+
188
+ if "error" in result:
189
+ return {"valid": True, "errors": [], "warnings": []}
190
+
191
+ content = result.get("content", "")
192
+ if not content and not result.get("found", True):
193
+ warnings.append("File not found on disk. It may not exist yet.")
194
+ elif not content:
195
+ warnings.append("File exists but has no content (empty file).")
196
+ elif len(content.strip().splitlines()) < 3:
197
+ warnings.append(
198
+ "File content is very short (fewer than 3 lines). "
199
+ "It may be a stub or incomplete."
200
+ )
201
+
202
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
203
+
204
+
205
+ def _validate_list_tree_response(result: dict) -> dict:
206
+ """Validate list_helix_tree result before returning to user.
207
+
208
+ Checks:
209
+ 1. No error in response.
210
+ 2. At least one category has files (warns if completely empty).
211
+
212
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
213
+ """
214
+ errors: list[str] = []
215
+ warnings: list[str] = []
216
+
217
+ if "error" in result:
218
+ return {"valid": True, "errors": [], "warnings": []}
219
+
220
+ categories = ["features", "steps", "pages", "locators", "utils_locators"]
221
+ total_files = 0
222
+ for cat in categories:
223
+ total_files += len(result.get(cat, []))
224
+
225
+ if total_files == 0:
226
+ warnings.append(
227
+ "No .ts or .feature files found in the Helix-QA project. "
228
+ "The project may be empty or the helix_root path may be incorrect."
229
+ )
230
+
231
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
232
+
233
+
234
+ def _validate_update_file_result(result: dict) -> dict:
235
+ """Validate update_helix_file result before returning to user.
236
+
237
+ Checks:
238
+ 1. No error in response.
239
+ 2. File was actually written (non-zero bytes).
240
+ 3. Warns when all merge candidates were duplicates (nothing new added).
241
+
242
+ Returns { valid: bool, errors: list[str], warnings: list[str] }.
243
+ """
244
+ errors: list[str] = []
245
+ warnings: list[str] = []
246
+
247
+ if not result.get("success", False):
248
+ errors.append(result.get("error", "update_helix_file returned success=false with no error message"))
249
+ return {"valid": False, "errors": errors, "warnings": warnings}
250
+
251
+ if result.get("bytes", 0) == 0:
252
+ warnings.append("File was written with zero bytes — the content may have been empty after adaptation.")
253
+
254
+ dedup = result.get("deduplication") or {}
255
+ added = dedup.get("added_keys") or dedup.get("added_patterns") or dedup.get("added_methods") or []
256
+ skipped = dedup.get("skipped_keys") or dedup.get("skipped_patterns") or dedup.get("skipped_methods") or []
257
+ if skipped and not added:
258
+ warnings.append(
259
+ f"All {len(skipped)} item(s) in the provided content already exist in the file — "
260
+ "nothing new was merged. If you intended to replace existing entries, "
261
+ "call update_helix_file with force_overwrite=true."
262
+ )
263
+
264
+ return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
265
+
266
+
267
+ @app.list_tools()
268
+ async def list_tools() -> list[types.Tool]:
269
+ return [
270
+ types.Tool(
271
+ name="inspect_helix_project",
272
+ description=(
273
+ "Detect the state of a local Helix-QA project before writing any files. "
274
+ "Returns framework_state ('absent' | 'partial' | 'present'), "
275
+ "which infrastructure files exist or are missing, and a recommended "
276
+ "write mode ('scaffold_and_tests' or 'tests_only'). "
277
+ "Always call this as Step 1 — the result determines what write_helix_files should do:\n"
278
+ " 'absent' / 'partial' → run scaffold_locator_repository first, "
279
+ "then write_helix_files with mode='scaffold_and_tests'.\n"
280
+ " 'present' → call write_helix_files with mode='tests_only' "
281
+ "(infrastructure files are never touched)."
282
+ ),
283
+ inputSchema={
284
+ "type": "object",
285
+ "properties": {
286
+ "helix_root": {
287
+ "type": "string",
288
+ "description": "Absolute path to the Helix-QA project root (contains package.json and src/).",
289
+ },
290
+ },
291
+ "required": ["helix_root"],
292
+ },
293
+ ),
294
+ types.Tool(
295
+ name="write_helix_files",
296
+ description=(
297
+ "Write generated TypeScript/Gherkin files into the Helix-QA directory layout "
298
+ "with full deduplication and interface adaptation.\n\n"
299
+ "Pass the 'files' dict from qa-playwright-generator:generate_playwright_code "
300
+ "or scaffold_locator_repository directly.\n\n"
301
+ "mode='tests_only' (default, safe to run repeatedly):\n"
302
+ " Writes locators.ts, *.page.ts, *.steps.ts, *.feature only.\n"
303
+ " Infrastructure files (LocatorHealer.ts etc.) are always skipped.\n"
304
+ " Existing files are MERGED — new locator keys / step patterns / "
305
+ "async methods are appended; duplicates are skipped.\n\n"
306
+ "mode='scaffold_and_tests':\n"
307
+ " Also writes infrastructure files, but only if they do not yet exist.\n"
308
+ " Use when inspect_helix_project returns 'absent' or 'partial'.\n\n"
309
+ "force_scaffold=true:\n"
310
+ " Overwrite existing infrastructure files. Use only when deliberately "
311
+ "regenerating the healing infrastructure.\n\n"
312
+ "Interface adaptation is applied automatically:\n"
313
+ " repo.updateHealed → repo.setHealed\n"
314
+ " repo.incrementSuccess / incrementFailure / queueSuggestion → removed\n"
315
+ " repo.getBBox → null\n"
316
+ " fixture().logger / .page / .locatorRepository → constructor arguments\n"
317
+ " EnvironmentManager → Helix environment singleton\n"
318
+ " Winston Logger → HealerLogger"
319
+ ),
320
+ inputSchema={
321
+ "type": "object",
322
+ "properties": {
323
+ "helix_root": {
324
+ "type": "string",
325
+ "description": "Absolute path to the Helix-QA project root.",
326
+ },
327
+ "files": {
328
+ "type": "object",
329
+ "description": (
330
+ "Dict of { file_key: file_content } as returned by "
331
+ "generate_playwright_code or scaffold_locator_repository."
332
+ ),
333
+ "additionalProperties": {"type": "string"},
334
+ },
335
+ "mode": {
336
+ "type": "string",
337
+ "enum": ["tests_only", "scaffold_and_tests"],
338
+ "description": (
339
+ "Write mode. Use 'tests_only' when the framework already exists. "
340
+ "Use 'scaffold_and_tests' when inspect_helix_project returns "
341
+ "'absent' or 'partial'. Default: 'tests_only'."
342
+ ),
343
+ },
344
+ "force_scaffold": {
345
+ "type": "boolean",
346
+ "description": (
347
+ "When true and mode='scaffold_and_tests', overwrite existing "
348
+ "infrastructure files. Default: false."
349
+ ),
350
+ },
351
+ },
352
+ "required": ["helix_root", "files"],
353
+ },
354
+ ),
355
+ types.Tool(
356
+ name="read_helix_file",
357
+ description=(
358
+ "Read the content of one file from the Helix-QA project. "
359
+ "Use before writing to check whether a locators, page object, or steps file "
360
+ "already exists — and what locator keys / step patterns / methods it contains — "
361
+ "so you can confirm the merge will not lose hand-edited content."
362
+ ),
363
+ inputSchema={
364
+ "type": "object",
365
+ "properties": {
366
+ "helix_root": {
367
+ "type": "string",
368
+ "description": "Absolute path to the Helix-QA project root.",
369
+ },
370
+ "relative_path": {
371
+ "type": "string",
372
+ "description": (
373
+ "Path relative to helix_root, e.g. "
374
+ "'src/locators/login-page.locators.ts'."
375
+ ),
376
+ },
377
+ },
378
+ "required": ["helix_root", "relative_path"],
379
+ },
380
+ ),
381
+ types.Tool(
382
+ name="list_helix_tree",
383
+ description=(
384
+ "List all .ts and .feature files in the Helix-QA project, grouped by category "
385
+ "(features, steps, pages, locators, utils_locators). "
386
+ "Call this after inspect_helix_project to see exactly which per-feature files "
387
+ "already exist before deciding what write_helix_files needs to produce."
388
+ ),
389
+ inputSchema={
390
+ "type": "object",
391
+ "properties": {
392
+ "helix_root": {
393
+ "type": "string",
394
+ "description": "Absolute path to the Helix-QA project root.",
395
+ },
396
+ },
397
+ "required": ["helix_root"],
398
+ },
399
+ ),
400
+ types.Tool(
401
+ name="update_helix_file",
402
+ description=(
403
+ "Write or merge a single file in the Helix-QA project without touching any other files. "
404
+ "Use this for targeted single-file edits — e.g. adding one missing locator key, "
405
+ "fixing one step pattern, or correcting one page method — instead of re-submitting "
406
+ "the entire files dict to write_helix_files.\n\n"
407
+ "Applies the same Helix interface adapter and merge logic as write_helix_files:\n"
408
+ " locators.ts → new const-object entries appended; duplicate keys skipped.\n"
409
+ " *.steps.ts → new step blocks appended; duplicate regex patterns skipped.\n"
410
+ " *.page.ts → new async methods appended; duplicate method names skipped.\n"
411
+ " *.feature → always overwritten (Gherkin is the source of truth).\n"
412
+ " other .ts → merged or overwritten depending on force_overwrite.\n\n"
413
+ "Set force_overwrite=true to replace the entire file without merging "
414
+ "(use only when you intentionally want to discard existing hand-edited content)."
415
+ ),
416
+ inputSchema={
417
+ "type": "object",
418
+ "properties": {
419
+ "helix_root": {
420
+ "type": "string",
421
+ "description": "Absolute path to the Helix-QA project root.",
422
+ },
423
+ "relative_path": {
424
+ "type": "string",
425
+ "description": (
426
+ "Path of the file to write, relative to helix_root. "
427
+ "E.g. 'src/locators/login.locators.ts' or "
428
+ "'src/test/steps/login.steps.ts'."
429
+ ),
430
+ },
431
+ "content": {
432
+ "type": "string",
433
+ "description": "Full or partial TypeScript / Gherkin content to write or merge into the file.",
434
+ },
435
+ "force_overwrite": {
436
+ "type": "boolean",
437
+ "description": (
438
+ "When true, replace the entire file without merging. "
439
+ "Default: false (merge / append only)."
440
+ ),
441
+ },
442
+ },
443
+ "required": ["helix_root", "relative_path", "content"],
444
+ },
445
+ ),
446
+ ]
447
+
448
+
449
+ @app.call_tool()
450
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
451
+ try:
452
+ if name == "inspect_helix_project":
453
+ result = await asyncio.to_thread(_inspect, arguments["helix_root"])
454
+ # ── Pre-output validation ─────────────────────────────────────
455
+ result["_validation"] = _validate_inspect_result(result)
456
+
457
+ elif name == "write_helix_files":
458
+ # ── Pre-write input validation ────────────────────────────────
459
+ input_validation = _validate_write_inputs(arguments.get("files", {}))
460
+ if not input_validation["valid"]:
461
+ result = {
462
+ "success": False,
463
+ "error": "input_validation_failed",
464
+ "_validation": input_validation,
465
+ "message": (
466
+ "File inputs failed pre-write validation. Fix the errors "
467
+ "below and retry. No files were written to disk."
468
+ ),
469
+ }
470
+ return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
471
+
472
+ result = await asyncio.to_thread(
473
+ _write_files,
474
+ arguments["helix_root"],
475
+ arguments["files"],
476
+ arguments.get("mode", "tests_only"),
477
+ arguments.get("force_scaffold", False),
478
+ )
479
+ # ── Post-write validation ─────────────────────────────────────
480
+ result["_validation"] = _validate_write_result(result)
481
+
482
+ elif name == "read_helix_file":
483
+ result = await asyncio.to_thread(
484
+ _read_file,
485
+ arguments["helix_root"],
486
+ arguments["relative_path"],
487
+ )
488
+ # ── Pre-output validation ─────────────────────────────────────
489
+ result["_validation"] = _validate_read_file_response(result)
490
+
491
+ elif name == "list_helix_tree":
492
+ result = await asyncio.to_thread(_list_tree, arguments["helix_root"])
493
+ # ── Pre-output validation ─────────────────────────────────────
494
+ result["_validation"] = _validate_list_tree_response(result)
495
+
496
+ elif name == "update_helix_file":
497
+ result = await asyncio.to_thread(
498
+ _update_file,
499
+ arguments["helix_root"],
500
+ arguments["relative_path"],
501
+ arguments["content"],
502
+ arguments.get("force_overwrite", False),
503
+ )
504
+ # ── Post-write validation ─────────────────────────────────────
505
+ result["_validation"] = _validate_update_file_result(result)
506
+
507
+ else:
508
+ result = {"error": f"Unknown tool: {name}"}
509
+
510
+ return [types.TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
511
+
512
+ except Exception as exc:
513
+ return [types.TextContent(
514
+ type="text",
515
+ text=json.dumps({"error": str(exc), "tool": name}, indent=2),
516
+ )]
517
+
518
+
519
+ async def _run():
520
+ async with stdio_server() as (r, w):
521
+ await app.run(r, w, app.create_initialization_options())
522
+
523
+
524
+ def main():
525
+ asyncio.run(_run())
526
+
527
+
528
+ if __name__ == "__main__":
529
+ main()