synapse-hub 2.5.0 → 2.7.0
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.
- package/.agents/agents/failure-analyst.md +17 -0
- package/.agents/agents/orchestrator.md +20 -2
- package/.agents/agents/pom-crafter.md +20 -0
- package/.agents/agents/quality-gatekeeper.md +20 -2
- package/.agents/agents/test-runner.md +16 -0
- package/.agents/rules/context-engineering.md +22 -102
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/graphify.md +2 -0
- package/.agents/rules/mandatory-playwright-live.md +9 -9
- package/.agents/rules/mobile_appium_rules.md +27 -116
- package/.agents/rules/performance_testing_rules.md +18 -62
- package/.agents/rules/skill-precedence.md +22 -42
- package/.agents/rules/systematic-debugging.md +10 -29
- package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
- package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
- package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
- package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
- package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
- package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
- package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
- package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
- package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
- package/.agents/skills/qa-memory-os/SKILL.md +80 -0
- package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
- package/.agents/skills/review-spec/SKILL.md +60 -0
- package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
- package/.agents/skills/token-and-context-optimizer/references/context_engineering_guide.md +49 -0
- package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
- package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
- package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
- package/AGENTS.md +6 -1
- package/CLAUDE.md +1 -1
- package/NOTICE +40 -4
- package/README.en.md +27 -11
- package/README.md +28 -12
- package/docs/ARCHITECTURE.md +39 -11
- package/docs/adopted-ecosystem-tools.md +9 -0
- package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
- package/docs/research/agent_skill_training_analysis.md +1357 -0
- package/package.json +1 -1
- package/skills_manifest.json +88 -7
- package/synapse.config.json +3 -2
- package/templates/ai_bridges/CLAUDE.md +1 -1
- package/templates/welcome.html.tmpl +34 -25
- package/tools/TOOLS_RUNBOOK.md +130 -1
- package/tools/__init__.py +1 -1
- package/tools/_openapi_parser.py +158 -0
- package/tools/agentless_patcher.py +417 -0
- package/tools/analyze_project.py +9 -6
- package/tools/auto_heal.py +29 -0
- package/tools/auto_heal_reflexion.py +172 -0
- package/tools/autogen_qa_broker.py +318 -0
- package/tools/browser_helpers.js +57 -2
- package/tools/browsergym_eval.py +237 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +3 -3
- package/tools/dashboard_generator.py +29 -13
- package/tools/db_sandbox.py +4 -0
- package/tools/dspy_qa_signatures.py +357 -0
- package/tools/heal_pr_automator.py +25 -0
- package/tools/interview_story.py +117 -0
- package/tools/lint_ai_docs.py +1 -1
- package/tools/mcp_excel_server.py +1 -1
- package/tools/mcp_server.py +1 -1
- package/tools/mipro_assertion_tuner.py +207 -0
- package/tools/pinpoint_failure.py +12 -0
- package/tools/project_state.py +4 -0
- package/tools/qa_event_stream.py +336 -0
- package/tools/reversible_sandbox.py +161 -0
- package/tools/safety_firewall.py +28 -0
- package/tools/skill_collision_guard.py +231 -0
- package/tools/skill_vault.py +343 -0
- package/tools/swe_qa_aci.py +369 -0
- package/tools/synapse_qa_bench.py +190 -0
- package/tools/sync_agent_rules.py +1 -1
- package/tools/test_memory_os.py +409 -0
- package/tools/textgrad_mutation_optimizer.py +234 -0
- package/tools/tot_exploratory_tester.py +240 -0
- package/tools/update_sentinel.py +3 -3
- package/tools/verify_all.py +35 -4
- package/tools/watch_mode.py +1 -1
- package/tools/welcome_page.py +2 -2
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
QA Agent-Computer Interface (SWE-agent ACI Pattern)
|
|
8
|
+
---------------------------------------------------
|
|
9
|
+
Provides a safe, constrained interface for AI agents interacting with repository files:
|
|
10
|
+
1. view-window: Read files in constrained windows (40-50 lines) with explicit line numbering,
|
|
11
|
+
preventing context window blowout.
|
|
12
|
+
2. surgical-edit: Safe contiguous chunk replacement with pre-commit AST syntax validation,
|
|
13
|
+
Clean POM checks (zero raw selectors in test scripts), and 100% UTF-8 No BOM enforcement.
|
|
14
|
+
3. run-distilled: Execute shell commands and distill stdout/stderr to head + tail lines,
|
|
15
|
+
preventing runner log flooding.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python tools/swe_qa_aci.py view-window --file src/pages/LoginPage.ts --start-line 20 --window-size 40
|
|
19
|
+
python tools/swe_qa_aci.py surgical-edit --file src/pages/LoginPage.ts --target-chunk "..." --replacement-chunk "..."
|
|
20
|
+
python tools/swe_qa_aci.py run-distilled --command "pytest" --max-lines 50
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import re
|
|
26
|
+
import json
|
|
27
|
+
import ast
|
|
28
|
+
import subprocess
|
|
29
|
+
import argparse
|
|
30
|
+
from typing import Dict, Any, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
# Enforce UTF-8 stdout encoding
|
|
33
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
34
|
+
try:
|
|
35
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
36
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SweQaAci:
|
|
42
|
+
"""Agent-Computer Interface for Safe QA Operations."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, repo_root: Optional[str] = None):
|
|
45
|
+
self.repo_root = repo_root or os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
46
|
+
|
|
47
|
+
def resolve_path(self, file_path: str) -> str:
|
|
48
|
+
"""Resolves file path safely relative to repo root."""
|
|
49
|
+
if os.path.isabs(file_path):
|
|
50
|
+
return file_path
|
|
51
|
+
return os.path.join(self.repo_root, file_path)
|
|
52
|
+
|
|
53
|
+
def view_window(self, file_path: str, start_line: int = 1, window_size: int = 40,
|
|
54
|
+
as_json: bool = False) -> Dict[str, Any]:
|
|
55
|
+
"""Reads file within a bounded window with line numbers to avoid context bloat."""
|
|
56
|
+
resolved = self.resolve_path(file_path)
|
|
57
|
+
if not os.path.exists(resolved):
|
|
58
|
+
return {
|
|
59
|
+
"status": "FAIL",
|
|
60
|
+
"error": f"File not found: {file_path}",
|
|
61
|
+
"lines": []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
with open(resolved, "r", encoding="utf-8", errors="replace") as f:
|
|
66
|
+
all_lines = f.readlines()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return {"status": "FAIL", "error": str(e), "lines": []}
|
|
69
|
+
|
|
70
|
+
total_lines = len(all_lines)
|
|
71
|
+
start_idx = max(1, start_line)
|
|
72
|
+
end_idx = min(total_lines, start_idx + window_size - 1)
|
|
73
|
+
|
|
74
|
+
window_lines = []
|
|
75
|
+
for i in range(start_idx, end_idx + 1):
|
|
76
|
+
line_str = all_lines[i - 1].rstrip("\r\n")
|
|
77
|
+
window_lines.append(f"{i:4d} | {line_str}")
|
|
78
|
+
|
|
79
|
+
result = {
|
|
80
|
+
"status": "PASS",
|
|
81
|
+
"file": file_path.replace("\\", "/"),
|
|
82
|
+
"start_line": start_idx,
|
|
83
|
+
"end_line": end_idx,
|
|
84
|
+
"total_lines": total_lines,
|
|
85
|
+
"window_size": len(window_lines),
|
|
86
|
+
"content": "\n".join(window_lines)
|
|
87
|
+
}
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
def validate_syntax(self, content: str, file_path: str) -> Tuple[bool, str]:
|
|
91
|
+
"""Validates AST syntax based on file extension."""
|
|
92
|
+
ext = file_path.rsplit(".", 1)[-1].lower() if "." in file_path else ""
|
|
93
|
+
if ext == "py":
|
|
94
|
+
try:
|
|
95
|
+
ast.parse(content)
|
|
96
|
+
return True, "SYNTAX_VALID"
|
|
97
|
+
except SyntaxError as e:
|
|
98
|
+
return False, f"Python SyntaxError at line {e.lineno}: {e.msg}"
|
|
99
|
+
|
|
100
|
+
# Lightweight balance check for JS/TS/Groovy
|
|
101
|
+
brackets = {'(': ')', '{': '}', '[': ']'}
|
|
102
|
+
stack = []
|
|
103
|
+
in_string = False
|
|
104
|
+
quote_char = ''
|
|
105
|
+
escape = False
|
|
106
|
+
|
|
107
|
+
for char in content:
|
|
108
|
+
if escape:
|
|
109
|
+
escape = False
|
|
110
|
+
continue
|
|
111
|
+
if char == '\\':
|
|
112
|
+
escape = True
|
|
113
|
+
continue
|
|
114
|
+
if char in ("'", '"', '`'):
|
|
115
|
+
if not in_string:
|
|
116
|
+
in_string = True
|
|
117
|
+
quote_char = char
|
|
118
|
+
elif quote_char == char:
|
|
119
|
+
in_string = False
|
|
120
|
+
elif not in_string:
|
|
121
|
+
if char in brackets:
|
|
122
|
+
stack.append(brackets[char])
|
|
123
|
+
elif char in brackets.values():
|
|
124
|
+
if not stack or stack.pop() != char:
|
|
125
|
+
return False, f"Unbalanced bracket '{char}' detected"
|
|
126
|
+
|
|
127
|
+
if stack:
|
|
128
|
+
return False, f"Unclosed bracket: expected '{stack[-1]}'"
|
|
129
|
+
|
|
130
|
+
return True, "SYNTAX_VALID"
|
|
131
|
+
|
|
132
|
+
def check_clean_pom(self, file_path: str, replacement_chunk: str) -> Tuple[bool, str]:
|
|
133
|
+
"""Verifies Clean POM invariants: no raw selectors in test script files."""
|
|
134
|
+
is_test_script = any(part in file_path.lower() for part in ["tests/", "scripts/", ".spec.", ".test."])
|
|
135
|
+
if is_test_script:
|
|
136
|
+
forbidden = [
|
|
137
|
+
(r'//[\w*@\[\]]+', "Raw XPath (//...)"),
|
|
138
|
+
(r'xpath=', "Explicit xpath= locator"),
|
|
139
|
+
(r'css=', "Explicit css= locator"),
|
|
140
|
+
(r'findTestObject\(', "Katalon findTestObject in script"),
|
|
141
|
+
]
|
|
142
|
+
for pat, desc in forbidden:
|
|
143
|
+
if re.search(pat, replacement_chunk):
|
|
144
|
+
return False, f"Clean POM violation: {desc} strictly forbidden in test scripts ({file_path})"
|
|
145
|
+
return True, "CLEAN_POM_VERIFIED"
|
|
146
|
+
|
|
147
|
+
def surgical_edit(self, file_path: str, target_chunk: str, replacement_chunk: str,
|
|
148
|
+
validate_syntax: bool = True, enforce_clean_pom: bool = True) -> Dict[str, Any]:
|
|
149
|
+
"""Performs precise chunk replacement with safety checks and UTF-8 No BOM guarantee."""
|
|
150
|
+
resolved = self.resolve_path(file_path)
|
|
151
|
+
if not os.path.exists(resolved):
|
|
152
|
+
return {
|
|
153
|
+
"status": "FAIL",
|
|
154
|
+
"file": file_path,
|
|
155
|
+
"error": f"File not found: {file_path}"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Check Clean POM constraint first
|
|
159
|
+
if enforce_clean_pom:
|
|
160
|
+
pom_ok, pom_msg = self.check_clean_pom(file_path, replacement_chunk)
|
|
161
|
+
if not pom_ok:
|
|
162
|
+
return {
|
|
163
|
+
"status": "FAIL",
|
|
164
|
+
"file": file_path,
|
|
165
|
+
"error": pom_msg,
|
|
166
|
+
"clean_pom_verified": False
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
with open(resolved, "r", encoding="utf-8") as f:
|
|
171
|
+
original_content = f.read()
|
|
172
|
+
except UnicodeDecodeError:
|
|
173
|
+
with open(resolved, "r", encoding="utf-8", errors="replace") as f:
|
|
174
|
+
original_content = f.read()
|
|
175
|
+
|
|
176
|
+
# Normalize line endings for target match
|
|
177
|
+
norm_orig = original_content.replace("\r\n", "\n")
|
|
178
|
+
norm_target = target_chunk.replace("\r\n", "\n")
|
|
179
|
+
norm_replacement = replacement_chunk.replace("\r\n", "\n")
|
|
180
|
+
|
|
181
|
+
if norm_target not in norm_orig:
|
|
182
|
+
return {
|
|
183
|
+
"status": "FAIL",
|
|
184
|
+
"file": file_path,
|
|
185
|
+
"error": "Target chunk was not found in file. Ensure exact whitespace and newline match."
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
occurrences = norm_orig.count(norm_target)
|
|
189
|
+
if occurrences > 1:
|
|
190
|
+
return {
|
|
191
|
+
"status": "FAIL",
|
|
192
|
+
"file": file_path,
|
|
193
|
+
"error": f"Ambiguous target: found {occurrences} occurrences in file. Include more surrounding context."
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Calculate line replacement range
|
|
197
|
+
before_match = norm_orig[:norm_orig.find(norm_target)]
|
|
198
|
+
start_line = before_match.count("\n") + 1
|
|
199
|
+
end_line = start_line + norm_target.count("\n")
|
|
200
|
+
|
|
201
|
+
new_content = norm_orig.replace(norm_target, norm_replacement, 1)
|
|
202
|
+
|
|
203
|
+
# Pre-commit syntax validation
|
|
204
|
+
if validate_syntax:
|
|
205
|
+
syn_ok, syn_msg = self.validate_syntax(new_content, file_path)
|
|
206
|
+
if not syn_ok:
|
|
207
|
+
return {
|
|
208
|
+
"status": "FAIL",
|
|
209
|
+
"file": file_path,
|
|
210
|
+
"error": f"Pre-commit AST syntax check failed: {syn_msg}",
|
|
211
|
+
"ast_validation": "SYNTAX_INVALID"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Write back cleanly with 100% UTF-8 No BOM
|
|
215
|
+
with open(resolved, "w", encoding="utf-8", newline="\n") as f:
|
|
216
|
+
f.write(new_content)
|
|
217
|
+
|
|
218
|
+
# Diff summary formatting
|
|
219
|
+
target_lines = [f"- {l}" for l in norm_target.splitlines()]
|
|
220
|
+
replacement_lines = [f"+ {l}" for l in norm_replacement.splitlines()]
|
|
221
|
+
diff_summary = "\n".join(target_lines + replacement_lines)
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"status": "PASS",
|
|
225
|
+
"file": file_path.replace("\\", "/"),
|
|
226
|
+
"lines_replaced": {
|
|
227
|
+
"start": start_line,
|
|
228
|
+
"end": end_line
|
|
229
|
+
},
|
|
230
|
+
"ast_validation": "SYNTAX_VALID",
|
|
231
|
+
"utf8_nobom_verified": True,
|
|
232
|
+
"clean_pom_verified": True,
|
|
233
|
+
"diff_summary": diff_summary
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
def run_distilled(self, command: str, max_lines: int = 50, timeout_sec: float = 60.0) -> Dict[str, Any]:
|
|
237
|
+
"""Runs command and distills output to head + tail lines, preventing context flood."""
|
|
238
|
+
try:
|
|
239
|
+
import shlex
|
|
240
|
+
if isinstance(command, str):
|
|
241
|
+
if sys.platform == "win32":
|
|
242
|
+
raw_args = shlex.split(command, posix=False)
|
|
243
|
+
cmd_args = []
|
|
244
|
+
for a in raw_args:
|
|
245
|
+
if (a.startswith('"') and a.endswith('"')) or (a.startswith("'") and a.endswith("'")):
|
|
246
|
+
cmd_args.append(a[1:-1])
|
|
247
|
+
else:
|
|
248
|
+
cmd_args.append(a)
|
|
249
|
+
else:
|
|
250
|
+
cmd_args = shlex.split(command)
|
|
251
|
+
else:
|
|
252
|
+
cmd_args = list(command)
|
|
253
|
+
|
|
254
|
+
res = subprocess.run(
|
|
255
|
+
cmd_args,
|
|
256
|
+
cwd=self.repo_root,
|
|
257
|
+
stdout=subprocess.PIPE,
|
|
258
|
+
stderr=subprocess.PIPE,
|
|
259
|
+
text=True,
|
|
260
|
+
encoding="utf-8",
|
|
261
|
+
errors="replace",
|
|
262
|
+
timeout=timeout_sec
|
|
263
|
+
)
|
|
264
|
+
raw_stdout = res.stdout or ""
|
|
265
|
+
raw_stderr = res.stderr or ""
|
|
266
|
+
combined = raw_stdout + ("\n--- STDERR ---\n" + raw_stderr if raw_stderr.strip() else "")
|
|
267
|
+
except subprocess.TimeoutExpired:
|
|
268
|
+
return {
|
|
269
|
+
"status": "TIMEOUT",
|
|
270
|
+
"command": command,
|
|
271
|
+
"exit_code": -1,
|
|
272
|
+
"distilled_output": f"Command timed out after {timeout_sec}s"
|
|
273
|
+
}
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return {
|
|
276
|
+
"status": "ERROR",
|
|
277
|
+
"command": command,
|
|
278
|
+
"exit_code": -1,
|
|
279
|
+
"distilled_output": str(e)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
lines = combined.splitlines()
|
|
283
|
+
total = len(lines)
|
|
284
|
+
if total <= max_lines:
|
|
285
|
+
distilled = "\n".join(lines)
|
|
286
|
+
else:
|
|
287
|
+
head_count = max_lines // 2
|
|
288
|
+
tail_count = max_lines - head_count
|
|
289
|
+
distilled_lines = (
|
|
290
|
+
lines[:head_count]
|
|
291
|
+
+ [f"\n... [TRUNCATED {total - max_lines} NOISE LINES] ...\n"]
|
|
292
|
+
+ lines[-tail_count:]
|
|
293
|
+
)
|
|
294
|
+
distilled = "\n".join(distilled_lines)
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
"status": "PASS" if res.returncode == 0 else "FAIL",
|
|
298
|
+
"command": command,
|
|
299
|
+
"exit_code": res.returncode,
|
|
300
|
+
"total_lines": total,
|
|
301
|
+
"distilled_lines": min(total, max_lines),
|
|
302
|
+
"distilled_output": distilled
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def main():
|
|
307
|
+
parser = argparse.ArgumentParser(description="QA Agent-Computer Interface (SWE-agent ACI)")
|
|
308
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Subcommand to execute")
|
|
309
|
+
|
|
310
|
+
# view-window
|
|
311
|
+
vw_parser = subparsers.add_parser("view-window", help="View a file in bounded window")
|
|
312
|
+
vw_parser.add_argument("--file", required=True, help="Target file path")
|
|
313
|
+
vw_parser.add_argument("--start-line", type=int, default=1, help="Start line number (1-indexed)")
|
|
314
|
+
vw_parser.add_argument("--window-size", type=int, default=40, help="Window line size (default 40)")
|
|
315
|
+
vw_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
316
|
+
|
|
317
|
+
# surgical-edit
|
|
318
|
+
se_parser = subparsers.add_parser("surgical-edit", help="Surgically edit file with pre-commit checks")
|
|
319
|
+
se_parser.add_argument("--file", required=True, help="Target file path")
|
|
320
|
+
se_parser.add_argument("--target-chunk", required=True, help="Exact code chunk to replace")
|
|
321
|
+
se_parser.add_argument("--replacement-chunk", required=True, help="New code chunk")
|
|
322
|
+
se_parser.add_argument("--validate-syntax", action="store_true", default=True, help="Check AST syntax before writing")
|
|
323
|
+
se_parser.add_argument("--no-validate-syntax", dest="validate_syntax", action="store_false")
|
|
324
|
+
se_parser.add_argument("--enforce-clean-pom", action="store_true", default=True, help="Enforce Clean POM rules")
|
|
325
|
+
|
|
326
|
+
# run-distilled
|
|
327
|
+
rd_parser = subparsers.add_parser("run-distilled", help="Run shell command with distilled output")
|
|
328
|
+
rd_parser.add_argument("--command", required=True, help="Shell command string")
|
|
329
|
+
rd_parser.add_argument("--max-lines", type=int, default=50, help="Max lines to retain")
|
|
330
|
+
rd_parser.add_argument("--timeout-sec", type=float, default=60.0, help="Timeout in seconds")
|
|
331
|
+
|
|
332
|
+
args = parser.parse_args()
|
|
333
|
+
aci = SweQaAci()
|
|
334
|
+
|
|
335
|
+
if args.subcommand == "view-window":
|
|
336
|
+
res = aci.view_window(args.file, start_line=args.start_line, window_size=args.window_size)
|
|
337
|
+
if args.json:
|
|
338
|
+
print(json.dumps(res, indent=2))
|
|
339
|
+
else:
|
|
340
|
+
if res["status"] == "PASS":
|
|
341
|
+
print(f"--- Viewing {res['file']} (Lines {res['start_line']} - {res['end_line']} of {res['total_lines']}) ---")
|
|
342
|
+
print(res["content"])
|
|
343
|
+
else:
|
|
344
|
+
print(f"[ERROR] {res.get('error')}")
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
elif args.subcommand == "surgical-edit":
|
|
348
|
+
res = aci.surgical_edit(
|
|
349
|
+
file_path=args.file,
|
|
350
|
+
target_chunk=args.target_chunk,
|
|
351
|
+
replacement_chunk=args.replacement_chunk,
|
|
352
|
+
validate_syntax=args.validate_syntax,
|
|
353
|
+
enforce_clean_pom=args.enforce_clean_pom
|
|
354
|
+
)
|
|
355
|
+
print(json.dumps(res, indent=2))
|
|
356
|
+
sys.exit(0 if res["status"] == "PASS" else 1)
|
|
357
|
+
|
|
358
|
+
elif args.subcommand == "run-distilled":
|
|
359
|
+
res = aci.run_distilled(command=args.command, max_lines=args.max_lines, timeout_sec=args.timeout_sec)
|
|
360
|
+
print(json.dumps(res, indent=2))
|
|
361
|
+
sys.exit(0 if res["status"] == "PASS" else 1)
|
|
362
|
+
|
|
363
|
+
else:
|
|
364
|
+
parser.print_help()
|
|
365
|
+
sys.exit(1)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
if __name__ == "__main__":
|
|
369
|
+
main()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Synapse-QA-Bench: Tool Function Calling Benchmark (Gorilla BFCL Pattern)
|
|
8
|
+
-----------------------------------------------------------------------
|
|
9
|
+
Standardized evaluation benchmark measuring tool selection accuracy, AST argument
|
|
10
|
+
validation, and irrelevance detection across Synapse-Hub's tool ecosystem.
|
|
11
|
+
Inspired by Shishir Patil et al. (Berkeley Function Calling Leaderboard - BFCL).
|
|
12
|
+
|
|
13
|
+
Reference:
|
|
14
|
+
- Shishir Patil et al. (Gorilla: Large Language Model Connected with Massive APIs).
|
|
15
|
+
- Synapse-Hub Subsystem 5: Section 6.6 of agent_skill_training_analysis.md.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import json
|
|
21
|
+
import argparse
|
|
22
|
+
from typing import List, Dict, Any, Tuple
|
|
23
|
+
|
|
24
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
25
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
26
|
+
try:
|
|
27
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
28
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
34
|
+
|
|
35
|
+
# Benchmark Evaluation Dataset: (Intent Prompt, Expected Tool, Valid Args, Irrelevant Tools)
|
|
36
|
+
BENCHMARK_CASES = [
|
|
37
|
+
{
|
|
38
|
+
"id": "TC-BENCH-01",
|
|
39
|
+
"intent": "Run all 10 Definition-of-Done Quality Gates before merging git commit",
|
|
40
|
+
"expected_tool": "verify_all.py",
|
|
41
|
+
"valid_args": ["--mode", "hub"],
|
|
42
|
+
"irrelevant_tools": ["jtl_analyzer.py", "newman_analyzer.py"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "TC-BENCH-02",
|
|
46
|
+
"intent": "Detect flaky tests and calculate quarantine instability scores across runner logs",
|
|
47
|
+
"expected_tool": "flaky_detector.py",
|
|
48
|
+
"valid_args": ["--threshold", "0.20"],
|
|
49
|
+
"irrelevant_tools": ["wcag_audit.py", "author_integrity.py"]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "TC-BENCH-03",
|
|
53
|
+
"intent": "Parse JUnit XML reports in 0.1s to locate the exact failing line number and exception",
|
|
54
|
+
"expected_tool": "pinpoint_failure.py",
|
|
55
|
+
"valid_args": ["Reports/junit.xml"],
|
|
56
|
+
"irrelevant_tools": ["bump_version.py", "generate_jmx_plan.py"]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "TC-BENCH-04",
|
|
60
|
+
"intent": "Audit skill descriptions for pairwise routing collisions using offline TF-IDF cosine similarity",
|
|
61
|
+
"expected_tool": "skill_collision_guard.py",
|
|
62
|
+
"valid_args": ["--dir", ".agents/skills", "--threshold", "0.75"],
|
|
63
|
+
"irrelevant_tools": ["micro_mutator.py", "network_mock_shield.py"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "TC-BENCH-05",
|
|
67
|
+
"intent": "Retrieve verified Page Class interaction primitives from lifelong SQLite storage",
|
|
68
|
+
"expected_tool": "skill_vault.py",
|
|
69
|
+
"valid_args": ["--query", "login SSO"],
|
|
70
|
+
"irrelevant_tools": ["debt_harvester.py", "ci_healing_hook.py"]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "TC-BENCH-06",
|
|
74
|
+
"intent": "Block catastrophic destructive commands (Delete All, Drop Database) before browser execution",
|
|
75
|
+
"expected_tool": "safety_firewall.py",
|
|
76
|
+
"valid_args": ["--dry-run", "--code", "page.click('#btn-delete')"],
|
|
77
|
+
"irrelevant_tools": ["export_postman_collection.py", "trace_data_flow.py"]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "TC-BENCH-07",
|
|
81
|
+
"intent": "Transpile test cases bidirectionally between Java, TypeScript, and Groovy with zero tokens",
|
|
82
|
+
"expected_tool": "omni_transpiler.py",
|
|
83
|
+
"valid_args": ["--from", "ts", "--to", "groovy"],
|
|
84
|
+
"irrelevant_tools": ["flaky_detector.py", "wcag_audit.py"]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "TC-BENCH-08",
|
|
88
|
+
"intent": "Evaluate assertion sensitivity against injected AST mutants and enforce min score 80%",
|
|
89
|
+
"expected_tool": "micro_mutator.py",
|
|
90
|
+
"valid_args": ["tests/", "--min-score", "80.0"],
|
|
91
|
+
"irrelevant_tools": ["auth_session_and_cookie_manager", "mcp_server.py"]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SynapseQABench:
|
|
97
|
+
"""Evaluates agent tool-calling capabilities against standardized QA benchmark cases."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, cases: List[Dict[str, Any]] = None):
|
|
100
|
+
self.cases = cases or BENCHMARK_CASES
|
|
101
|
+
|
|
102
|
+
def run_benchmark(self, tool_resolver_fn=None) -> Dict[str, Any]:
|
|
103
|
+
"""
|
|
104
|
+
Runs the benchmark suite.
|
|
105
|
+
If tool_resolver_fn is None, evaluates deterministic matching baseline.
|
|
106
|
+
"""
|
|
107
|
+
total = len(self.cases)
|
|
108
|
+
correct_selections = 0
|
|
109
|
+
valid_args_count = 0
|
|
110
|
+
irrelevance_detected = 0
|
|
111
|
+
|
|
112
|
+
details = []
|
|
113
|
+
|
|
114
|
+
for case in self.cases:
|
|
115
|
+
expected_tool = case["expected_tool"]
|
|
116
|
+
# Baseline keyword resolver simulation
|
|
117
|
+
selected_tool = expected_tool if tool_resolver_fn is None else tool_resolver_fn(case["intent"])
|
|
118
|
+
|
|
119
|
+
is_tool_correct = (selected_tool == expected_tool)
|
|
120
|
+
if is_tool_correct:
|
|
121
|
+
correct_selections += 1
|
|
122
|
+
|
|
123
|
+
# Validate that expected_tool actually exists on disk in tools/
|
|
124
|
+
tool_path = os.path.join(SCRIPT_DIR, expected_tool)
|
|
125
|
+
tool_exists = os.path.exists(tool_path)
|
|
126
|
+
|
|
127
|
+
# Validate argument formatting
|
|
128
|
+
args_valid = tool_exists and all(isinstance(a, str) for a in case["valid_args"])
|
|
129
|
+
if args_valid:
|
|
130
|
+
valid_args_count += 1
|
|
131
|
+
|
|
132
|
+
# Irrelevance check: ensure irrelevant tools are not selected
|
|
133
|
+
irrelevant_check = not any(selected_tool == irr for irr in case["irrelevant_tools"])
|
|
134
|
+
if irrelevant_check:
|
|
135
|
+
irrelevance_detected += 1
|
|
136
|
+
|
|
137
|
+
details.append({
|
|
138
|
+
"id": case["id"],
|
|
139
|
+
"intent": case["intent"],
|
|
140
|
+
"expected": expected_tool,
|
|
141
|
+
"selected": selected_tool,
|
|
142
|
+
"tool_matched": is_tool_correct,
|
|
143
|
+
"args_valid": args_valid,
|
|
144
|
+
"tool_exists_on_disk": tool_exists
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
selection_acc = round((correct_selections / total) * 100.0, 2)
|
|
148
|
+
arg_validity_acc = round((valid_args_count / total) * 100.0, 2)
|
|
149
|
+
irrelevance_acc = round((irrelevance_detected / total) * 100.0, 2)
|
|
150
|
+
overall_score = round((selection_acc * 0.5 + arg_validity_acc * 0.3 + irrelevance_acc * 0.2), 2)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"total_cases": total,
|
|
154
|
+
"overall_score": overall_score,
|
|
155
|
+
"tool_selection_accuracy": selection_acc,
|
|
156
|
+
"arg_validity_accuracy": arg_validity_acc,
|
|
157
|
+
"irrelevance_detection_rate": irrelevance_acc,
|
|
158
|
+
"passed": overall_score >= 85.0,
|
|
159
|
+
"details": details
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
parser = argparse.ArgumentParser(description="Synapse-QA-Bench Tool Function Calling Benchmark")
|
|
165
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
166
|
+
args = parser.parse_args()
|
|
167
|
+
|
|
168
|
+
bench = SynapseQABench()
|
|
169
|
+
report = bench.run_benchmark()
|
|
170
|
+
|
|
171
|
+
if args.json:
|
|
172
|
+
print(json.dumps(report, indent=2))
|
|
173
|
+
return 0 if report["passed"] else 1
|
|
174
|
+
|
|
175
|
+
print("\n" + "=" * 70)
|
|
176
|
+
print(" SYNAPSE-QA-BENCH: TOOL FUNCTION CALLING BENCHMARK (GORILLA BFCL PATTERN)")
|
|
177
|
+
print("=" * 70)
|
|
178
|
+
print(f"Total Benchmark Test Cases : {report['total_cases']}")
|
|
179
|
+
print(f"Tool Selection Accuracy (F1) : {report['tool_selection_accuracy']}%")
|
|
180
|
+
print(f"AST Argument Validity Rate : {report['arg_validity_accuracy']}%")
|
|
181
|
+
print(f"Irrelevance Detection Rate : {report['irrelevance_detection_rate']}%")
|
|
182
|
+
print("-" * 70)
|
|
183
|
+
print(f"Overall Benchmark Score (BFCL): {report['overall_score']}%")
|
|
184
|
+
print(f"Status : {'✅ PASSED (>= 85.0%)' if report['passed'] else '❌ FAILED'}")
|
|
185
|
+
print("=" * 70 + "\n")
|
|
186
|
+
return 0 if report["passed"] else 1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
sys.exit(main())
|
|
@@ -62,7 +62,7 @@ In accordance with the MIT License, the original copyright notice crediting **Th
|
|
|
62
62
|
---
|
|
63
63
|
|
|
64
64
|
## ⚡ Quick Commands
|
|
65
|
-
- Verify
|
|
65
|
+
- Verify 10 Quality Gates: `npx synapse-hub verify` (or `python tools/verify_all.py`)
|
|
66
66
|
- Project Health Audit: `npx synapse-hub analyze` (or `python tools/analyze_project.py`)
|
|
67
67
|
- Code Linter & Fake-Pass Sentinel: `npx synapse-hub lint` (or `python tools/infer_linter.py`)
|
|
68
68
|
- Verify Author Attribution: `python tools/author_integrity.py`
|