synapse-hub 2.5.0 → 2.6.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 +13 -0
- package/.agents/agents/orchestrator.md +4 -2
- package/.agents/agents/pom-crafter.md +12 -0
- package/.agents/agents/quality-gatekeeper.md +15 -2
- package/.agents/agents/test-runner.md +11 -0
- package/.agents/rules/context-engineering.md +22 -102
- package/.agents/rules/mobile_appium_rules.md +19 -115
- 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/token-and-context-optimizer/references/context_engineering_guide.md +49 -0
- package/AGENTS.md +6 -1
- package/NOTICE +40 -4
- package/README.en.md +24 -8
- package/README.md +27 -11
- package/docs/ARCHITECTURE.md +37 -9
- package/docs/adopted-ecosystem-tools.md +9 -0
- package/docs/research/agent_skill_training_analysis.md +1357 -0
- package/package.json +1 -1
- package/skills_manifest.json +2 -2
- package/synapse.config.json +3 -2
- package/templates/welcome.html.tmpl +34 -25
- package/tools/TOOLS_RUNBOOK.md +129 -0
- package/tools/__init__.py +1 -1
- package/tools/auto_heal.py +10 -0
- package/tools/auto_heal_reflexion.py +172 -0
- package/tools/browser_helpers.js +55 -0
- package/tools/cli.py +3 -3
- package/tools/dashboard_generator.py +26 -10
- package/tools/dspy_qa_signatures.py +357 -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/reversible_sandbox.py +157 -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/synapse_qa_bench.py +190 -0
- package/tools/textgrad_mutation_optimizer.py +234 -0
- package/tools/verify_all.py +35 -4
- package/tools/watch_mode.py +1 -1
- package/tools/welcome_page.py +2 -2
package/tools/lint_ai_docs.py
CHANGED
|
@@ -118,7 +118,7 @@ def find_doc_files(repo_root):
|
|
|
118
118
|
for pattern in md_patterns:
|
|
119
119
|
for f in glob.glob(pattern, recursive=True):
|
|
120
120
|
rel = os.path.relpath(f, repo_root)
|
|
121
|
-
if
|
|
121
|
+
if any(ignored in rel for ignored in ("node_modules", ".git", "allure-results", "teamwork", "external_repos")):
|
|
122
122
|
continue
|
|
123
123
|
doc_files.append(rel)
|
|
124
124
|
return sorted(doc_files)
|
|
@@ -23,7 +23,7 @@ if hasattr(sys.stdout, 'reconfigure'):
|
|
|
23
23
|
except Exception:
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
|
-
SERVER_VERSION = "2.
|
|
26
|
+
SERVER_VERSION = "2.6.0"
|
|
27
27
|
|
|
28
28
|
NS_MAIN = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
|
|
29
29
|
NS_REL = "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}"
|
package/tools/mcp_server.py
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
Reversible Execution Sandbox & Automated Teardown Engine (GoEx Pattern)
|
|
8
|
+
------------------------------------------------------------------------
|
|
9
|
+
Maintains a LIFO undo-stack of stateful mutations (DB inserts, file modifications,
|
|
10
|
+
auth session cookies). Guarantees 100% idempotent teardown and zero database leakage
|
|
11
|
+
even when test execution crashes or is cancelled.
|
|
12
|
+
|
|
13
|
+
Reference:
|
|
14
|
+
- Shishir Patil et al. (Gorilla / GoEx): Runtime Reversible Sandboxed Execution.
|
|
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 shlex
|
|
22
|
+
import subprocess
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import List, Optional, Callable, Dict, Any
|
|
25
|
+
|
|
26
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
27
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
28
|
+
try:
|
|
29
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
30
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ReversibleAction:
|
|
37
|
+
action_type: str # e.g., "DB_INSERT", "FS_WRITE", "AUTH_SESSION", "STATE_MUTATION"
|
|
38
|
+
target_resource: str # e.g., "users table", "tmp/test_export.csv", "Bearer token"
|
|
39
|
+
undo_command: str # Exact SQL DELETE, shell command, or callback instruction
|
|
40
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExecutionSandbox:
|
|
44
|
+
"""Manages an undo stack of stateful mutations for guaranteed autonomous cleanup."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, auto_rollback_on_exit: bool = True):
|
|
47
|
+
self.undo_stack: List[ReversibleAction] = []
|
|
48
|
+
self.auto_rollback_on_exit = auto_rollback_on_exit
|
|
49
|
+
|
|
50
|
+
def record_mutation(self, action_type: str, target_resource: str, undo_command: str, metadata: Optional[Dict[str, Any]] = None):
|
|
51
|
+
"""Pushes a rollback action onto the LIFO undo stack."""
|
|
52
|
+
action = ReversibleAction(
|
|
53
|
+
action_type=action_type,
|
|
54
|
+
target_resource=target_resource,
|
|
55
|
+
undo_command=undo_command,
|
|
56
|
+
metadata=metadata or {}
|
|
57
|
+
)
|
|
58
|
+
self.undo_stack.append(action)
|
|
59
|
+
|
|
60
|
+
def execute_stateful_action(self, action_type: str, target_resource: str, forward_cmd: str, undo_cmd: str) -> bool:
|
|
61
|
+
"""Executes a forward shell command and immediately records its reversible undo counterpart."""
|
|
62
|
+
try:
|
|
63
|
+
args = shlex.split(forward_cmd) if isinstance(forward_cmd, str) else forward_cmd
|
|
64
|
+
res = subprocess.run(args, text=True, capture_output=True)
|
|
65
|
+
if res.returncode == 0:
|
|
66
|
+
self.record_mutation(action_type, target_resource, undo_cmd, {"forward_cmd": forward_cmd})
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def rollback_all(self, silent: bool = False) -> List[Dict[str, Any]]:
|
|
73
|
+
"""Unwinds the undo stack in strictly reverse (LIFO) order."""
|
|
74
|
+
results = []
|
|
75
|
+
while self.undo_stack:
|
|
76
|
+
action = self.undo_stack.pop()
|
|
77
|
+
success = True
|
|
78
|
+
err_msg = ""
|
|
79
|
+
cmd = action.undo_command.strip()
|
|
80
|
+
|
|
81
|
+
if cmd.startswith("FILE_DELETE:"):
|
|
82
|
+
fpath = cmd.split(":", 1)[1].strip()
|
|
83
|
+
if os.path.exists(fpath):
|
|
84
|
+
try:
|
|
85
|
+
os.remove(fpath)
|
|
86
|
+
except OSError as e:
|
|
87
|
+
success = False
|
|
88
|
+
err_msg = str(e)
|
|
89
|
+
elif cmd.startswith("DIR_DELETE:"):
|
|
90
|
+
dpath = cmd.split(":", 1)[1].strip()
|
|
91
|
+
if os.path.isdir(dpath):
|
|
92
|
+
try:
|
|
93
|
+
import shutil
|
|
94
|
+
shutil.rmtree(dpath, ignore_errors=True)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
success = False
|
|
97
|
+
err_msg = str(e)
|
|
98
|
+
elif cmd:
|
|
99
|
+
try:
|
|
100
|
+
args = shlex.split(cmd) if isinstance(cmd, str) else cmd
|
|
101
|
+
res = subprocess.run(args, text=True, capture_output=True)
|
|
102
|
+
success = (res.returncode == 0)
|
|
103
|
+
if not success:
|
|
104
|
+
err_msg = res.stderr.strip()
|
|
105
|
+
except Exception as e:
|
|
106
|
+
success = False
|
|
107
|
+
err_msg = str(e)
|
|
108
|
+
|
|
109
|
+
result_entry = {
|
|
110
|
+
"action_type": action.action_type,
|
|
111
|
+
"target_resource": action.target_resource,
|
|
112
|
+
"undo_command": action.undo_command,
|
|
113
|
+
"success": success,
|
|
114
|
+
"error": err_msg
|
|
115
|
+
}
|
|
116
|
+
results.append(result_entry)
|
|
117
|
+
if not silent:
|
|
118
|
+
status_icon = "✅" if success else "⚠️"
|
|
119
|
+
print(f" {status_icon} [ROLLBACK] {action.action_type} on '{action.target_resource}': {action.undo_command}")
|
|
120
|
+
|
|
121
|
+
return results
|
|
122
|
+
|
|
123
|
+
def __enter__(self):
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
127
|
+
if exc_type is not None or self.auto_rollback_on_exit:
|
|
128
|
+
self.rollback_all(silent=True)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
GLOBAL_SANDBOX = ExecutionSandbox(auto_rollback_on_exit=False)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def record_mutation(action_type: str, target: str, undo_cmd: str):
|
|
135
|
+
GLOBAL_SANDBOX.record_mutation(action_type, target, undo_cmd)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def rollback_all():
|
|
139
|
+
return GLOBAL_SANDBOX.rollback_all()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
import argparse
|
|
144
|
+
parser = argparse.ArgumentParser(description="GoEx Reversible Teardown Sandbox Stack")
|
|
145
|
+
parser.add_argument("--demo", action="store_true", help="Run reversible stack demonstration")
|
|
146
|
+
parser.add_argument("--status", action="store_true", help="Display active undo stack count")
|
|
147
|
+
args = parser.parse_args()
|
|
148
|
+
|
|
149
|
+
if args.demo:
|
|
150
|
+
print("Running GoEx Reversible Sandbox demo...")
|
|
151
|
+
with ExecutionSandbox(auto_rollback_on_exit=True) as sb:
|
|
152
|
+
sb.record_mutation("fs_create", "test_demo.tmp", "echo rolled back")
|
|
153
|
+
print(f"Stack size inside sandbox: {sb.stack_size()}")
|
|
154
|
+
print("Exited sandbox context, stack rolled back cleanly.")
|
|
155
|
+
else:
|
|
156
|
+
print(f"Active global undo stack size: {GLOBAL_SANDBOX.stack_size()}")
|
|
157
|
+
|
package/tools/safety_firewall.py
CHANGED
|
@@ -19,6 +19,11 @@ import json
|
|
|
19
19
|
import argparse
|
|
20
20
|
from typing import List, Dict, Any, Tuple
|
|
21
21
|
|
|
22
|
+
try:
|
|
23
|
+
import reversible_sandbox
|
|
24
|
+
except ImportError:
|
|
25
|
+
reversible_sandbox = None
|
|
26
|
+
|
|
22
27
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
23
28
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
24
29
|
try:
|
|
@@ -302,8 +307,31 @@ def main():
|
|
|
302
307
|
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
303
308
|
parser.add_argument("--history", default="", help="Comma-separated or JSON list of historical action names to check for infinite loops")
|
|
304
309
|
parser.add_argument("--check-loop", action="store_true", help="Explicitly audit action history for cycle/infinite loops")
|
|
310
|
+
parser.add_argument("--rollback-all", action="store_true", help="Unwind all recorded reversible actions via GoEx teardown stack")
|
|
311
|
+
parser.add_argument("--record-undo", default="", help="Undo command to push onto reversible sandbox stack")
|
|
312
|
+
parser.add_argument("--undo-target", default="STATE_MUTATION", help="Target resource or type for recorded undo action")
|
|
305
313
|
args = parser.parse_args()
|
|
306
314
|
|
|
315
|
+
# Reversible Execution Sandbox teardown (GoEx pattern)
|
|
316
|
+
if args.rollback_all:
|
|
317
|
+
if reversible_sandbox:
|
|
318
|
+
res = reversible_sandbox.rollback_all()
|
|
319
|
+
if args.json:
|
|
320
|
+
print(json.dumps(res, indent=2))
|
|
321
|
+
return 0
|
|
322
|
+
else:
|
|
323
|
+
print("❌ reversible_sandbox module not available.")
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
if args.record_undo:
|
|
327
|
+
if reversible_sandbox:
|
|
328
|
+
reversible_sandbox.record_mutation("STATE_MUTATION", args.undo_target, args.record_undo)
|
|
329
|
+
print(f"✅ [GOEX SANDBOX] Reversible action registered for '{args.undo_target}': {args.record_undo}")
|
|
330
|
+
return 0
|
|
331
|
+
else:
|
|
332
|
+
print("❌ reversible_sandbox module not available.")
|
|
333
|
+
return 1
|
|
334
|
+
|
|
307
335
|
# Loop check mode (Blazo-inspired)
|
|
308
336
|
if args.history or args.check_loop:
|
|
309
337
|
hist_items = []
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
Tier-2 Skill Routing & Collision Guard (Addy Osmani Pattern)
|
|
8
|
+
------------------------------------------------------------
|
|
9
|
+
Computes pairwise cosine similarity across all SKILL.md descriptions.
|
|
10
|
+
Operates 100% offline with zero LLM tokens using a lightweight suffix-stripping
|
|
11
|
+
stemmer heuristic and TF-IDF word frequency representation.
|
|
12
|
+
|
|
13
|
+
Enforces:
|
|
14
|
+
- Warning when description similarity >= 0.50 (semantic proximity)
|
|
15
|
+
- Critical Error (Quality Gate Failure) when similarity >= 0.75 (ambiguous routing)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python tools/skill_collision_guard.py [--dir .agents/skills] [--threshold 0.75] [--json]
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import re
|
|
24
|
+
import math
|
|
25
|
+
import json
|
|
26
|
+
import argparse
|
|
27
|
+
from typing import Dict, List, Tuple
|
|
28
|
+
from collections import Counter
|
|
29
|
+
|
|
30
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
31
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
32
|
+
try:
|
|
33
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
34
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
STOPWORDS = {
|
|
39
|
+
"a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are",
|
|
40
|
+
"aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both",
|
|
41
|
+
"but", "by", "can", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't",
|
|
42
|
+
"doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't",
|
|
43
|
+
"has", "hasn't", "have", "haven't", "having", "he", "her", "here", "hers", "herself", "him",
|
|
44
|
+
"himself", "his", "how", "i", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself",
|
|
45
|
+
"let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on",
|
|
46
|
+
"once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same",
|
|
47
|
+
"shan't", "she", "should", "shouldn't", "so", "some", "such", "than", "that", "the", "their",
|
|
48
|
+
"theirs", "them", "themselves", "then", "there", "these", "they", "this", "those", "through",
|
|
49
|
+
"to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "were", "weren't", "what",
|
|
50
|
+
"when", "where", "which", "while", "who", "whom", "why", "with", "won't", "would", "wouldn't",
|
|
51
|
+
"you", "your", "yours", "yourself", "yourselves", "use", "when", "allows", "provides", "designed"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def strip_suffix(word: str) -> str:
|
|
56
|
+
"""Lightweight suffix-stripping heuristic (Addy Osmani Tier-2 standard)."""
|
|
57
|
+
if len(word) <= 4:
|
|
58
|
+
return word
|
|
59
|
+
for suffix in ("ing", "tion", "ment", "ness", "able", "ible", "ize", "ise", "ies", "es", "ed", "ly", "al", "s"):
|
|
60
|
+
if word.endswith(suffix) and len(word) - len(suffix) >= 3:
|
|
61
|
+
return word[:-len(suffix)]
|
|
62
|
+
return word
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def tokenize(text: str) -> List[str]:
|
|
66
|
+
"""Tokenizes text, strips punctuation, removes stopwords and applies suffix stripping."""
|
|
67
|
+
words = re.findall(r"\b[a-zA-Z0-9_\-]+\b", text.lower())
|
|
68
|
+
tokens = []
|
|
69
|
+
for w in words:
|
|
70
|
+
cleaned = re.sub(r"^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$", "", w)
|
|
71
|
+
if len(cleaned) >= 2 and cleaned not in STOPWORDS:
|
|
72
|
+
tokens.append(strip_suffix(cleaned))
|
|
73
|
+
return tokens
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_tf_vector(tokens: List[str]) -> Dict[str, float]:
|
|
77
|
+
"""Builds normalized term-frequency vector."""
|
|
78
|
+
counts = Counter(tokens)
|
|
79
|
+
total = len(tokens)
|
|
80
|
+
if total == 0:
|
|
81
|
+
return {}
|
|
82
|
+
return {word: count / total for word, count in counts.items()}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cosine_similarity(vec1: Dict[str, float], vec2: Dict[str, float]) -> float:
|
|
86
|
+
"""Computes cosine similarity between two term-frequency vectors."""
|
|
87
|
+
intersection = set(vec1.keys()) & set(vec2.keys())
|
|
88
|
+
numerator = sum(vec1[x] * vec2[x] for x in intersection)
|
|
89
|
+
sum1 = sum(v ** 2 for v in vec1.values())
|
|
90
|
+
sum2 = sum(v ** 2 for v in vec2.values())
|
|
91
|
+
denominator = math.sqrt(sum1) * math.sqrt(sum2)
|
|
92
|
+
if not denominator:
|
|
93
|
+
return 0.0
|
|
94
|
+
return float(numerator) / denominator
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SkillCollisionGuard:
|
|
98
|
+
"""Audits skill descriptions across repository for routing collisions and ambiguous triggers."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, skills_dir: str = ".agents/skills", threshold: float = 0.75, warn_threshold: float = 0.50):
|
|
101
|
+
self.skills_dir = skills_dir
|
|
102
|
+
self.threshold = threshold
|
|
103
|
+
self.warn_threshold = warn_threshold
|
|
104
|
+
|
|
105
|
+
def load_descriptions(self) -> Dict[str, str]:
|
|
106
|
+
"""Loads all SKILL.md files and extracts their description strings."""
|
|
107
|
+
descriptions = {}
|
|
108
|
+
if not os.path.exists(self.skills_dir):
|
|
109
|
+
return descriptions
|
|
110
|
+
|
|
111
|
+
for root, _, files in os.walk(self.skills_dir):
|
|
112
|
+
for file in files:
|
|
113
|
+
if file.lower() == "skill.md":
|
|
114
|
+
full_path = os.path.join(root, file)
|
|
115
|
+
skill_name, desc = self._extract_frontmatter(full_path)
|
|
116
|
+
if skill_name and desc:
|
|
117
|
+
descriptions[skill_name] = desc
|
|
118
|
+
return descriptions
|
|
119
|
+
|
|
120
|
+
def _extract_frontmatter(self, path: str) -> Tuple[str, str]:
|
|
121
|
+
try:
|
|
122
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
123
|
+
content = f.read()
|
|
124
|
+
except OSError:
|
|
125
|
+
return "", ""
|
|
126
|
+
|
|
127
|
+
match = re.search(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
|
|
128
|
+
if match:
|
|
129
|
+
fm = match.group(1)
|
|
130
|
+
name_m = re.search(r"^name:\s*(.+)$", fm, re.MULTILINE)
|
|
131
|
+
desc_m = re.search(r"^description:\s*[\"']?(.*?)[\"']?$", fm, re.MULTILINE)
|
|
132
|
+
name = name_m.group(1).strip() if name_m else os.path.basename(os.path.dirname(path))
|
|
133
|
+
desc = desc_m.group(1).strip() if desc_m else ""
|
|
134
|
+
return name, desc
|
|
135
|
+
|
|
136
|
+
fallback_name = os.path.basename(os.path.dirname(path))
|
|
137
|
+
return fallback_name, ""
|
|
138
|
+
|
|
139
|
+
def audit(self) -> Tuple[bool, List[Dict[str, any]]]:
|
|
140
|
+
"""
|
|
141
|
+
Computes pairwise similarity.
|
|
142
|
+
Returns (is_clean, report_items).
|
|
143
|
+
"""
|
|
144
|
+
descriptions = self.load_descriptions()
|
|
145
|
+
names = sorted(list(descriptions.keys()))
|
|
146
|
+
vectors = {name: build_tf_vector(tokenize(descriptions[name])) for name in names}
|
|
147
|
+
|
|
148
|
+
findings = []
|
|
149
|
+
has_critical = False
|
|
150
|
+
|
|
151
|
+
for i in range(len(names)):
|
|
152
|
+
for j in range(i + 1, len(names)):
|
|
153
|
+
name_a = names[i]
|
|
154
|
+
name_b = names[j]
|
|
155
|
+
sim = cosine_similarity(vectors[name_a], vectors[name_b])
|
|
156
|
+
|
|
157
|
+
if sim >= self.threshold:
|
|
158
|
+
has_critical = True
|
|
159
|
+
findings.append({
|
|
160
|
+
"level": "CRITICAL",
|
|
161
|
+
"skill_a": name_a,
|
|
162
|
+
"skill_b": name_b,
|
|
163
|
+
"similarity": round(sim, 3),
|
|
164
|
+
"message": f"Critical Collision: '{name_a}' and '{name_b}' have cosine similarity {sim:.3f} >= {self.threshold}"
|
|
165
|
+
})
|
|
166
|
+
elif sim >= self.warn_threshold:
|
|
167
|
+
findings.append({
|
|
168
|
+
"level": "WARNING",
|
|
169
|
+
"skill_a": name_a,
|
|
170
|
+
"skill_b": name_b,
|
|
171
|
+
"similarity": round(sim, 3),
|
|
172
|
+
"message": f"Proximity Warning: '{name_a}' and '{name_b}' have similarity {sim:.3f} >= {self.warn_threshold}"
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return (not has_critical), findings
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def run_guard(skills_dir: str = ".agents/skills", threshold: float = 0.75, warn_threshold: float = 0.50, output_json: bool = False) -> int:
|
|
179
|
+
guard = SkillCollisionGuard(skills_dir, threshold=threshold, warn_threshold=warn_threshold)
|
|
180
|
+
passed, findings = guard.audit()
|
|
181
|
+
|
|
182
|
+
if output_json:
|
|
183
|
+
print(json.dumps({
|
|
184
|
+
"skills_dir": skills_dir,
|
|
185
|
+
"passed": passed,
|
|
186
|
+
"threshold": threshold,
|
|
187
|
+
"warn_threshold": warn_threshold,
|
|
188
|
+
"findings": findings
|
|
189
|
+
}, indent=2))
|
|
190
|
+
return 0 if passed else 1
|
|
191
|
+
|
|
192
|
+
print("\n" + "=" * 70)
|
|
193
|
+
print(" TIER-2 SKILL ROUTING & COLLISION GUARD (ADDY OSMANI PATTERN)")
|
|
194
|
+
print("=" * 70)
|
|
195
|
+
print(f"Skills Directory : {skills_dir}")
|
|
196
|
+
print(f"Collision Threshold: {threshold} (FAIL) | Warning Threshold: {warn_threshold} (WARN)")
|
|
197
|
+
|
|
198
|
+
criticals = [f for f in findings if f["level"] == "CRITICAL"]
|
|
199
|
+
warnings = [f for f in findings if f["level"] == "WARNING"]
|
|
200
|
+
|
|
201
|
+
if warnings:
|
|
202
|
+
print(f"\n⚠️ {len(warnings)} Near-Neighbor Warnings detected (similarity >= {warn_threshold}):")
|
|
203
|
+
for w in warnings:
|
|
204
|
+
print(f" - {w['message']}")
|
|
205
|
+
|
|
206
|
+
if criticals:
|
|
207
|
+
print(f"\n❌ {len(criticals)} CRITICAL COLLISION(S) detected (similarity >= {threshold}):")
|
|
208
|
+
for c in criticals:
|
|
209
|
+
print(f" [FAIL] {c['message']}")
|
|
210
|
+
print("\n[DoD BLOCKED] High similarity causes routing ambiguity and context contamination.")
|
|
211
|
+
print("=" * 70 + "\n")
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
print(f"\n✅ [PASS] 0 routing collisions detected. All skill descriptions uniquely segmented.\n")
|
|
215
|
+
print("=" * 70 + "\n")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main():
|
|
220
|
+
parser = argparse.ArgumentParser(description="Tier-2 Skill Routing & Collision Guard")
|
|
221
|
+
parser.add_argument("--dir", default=".agents/skills", help="Directory containing skill packages")
|
|
222
|
+
parser.add_argument("--threshold", type=float, default=0.75, help="Critical similarity threshold (default: 0.75)")
|
|
223
|
+
parser.add_argument("--warn-threshold", type=float, default=0.50, help="Warning similarity threshold (default: 0.50)")
|
|
224
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
225
|
+
args = parser.parse_args()
|
|
226
|
+
|
|
227
|
+
return run_guard(skills_dir=args.dir, threshold=args.threshold, warn_threshold=args.warn_threshold, output_json=args.json)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
sys.exit(main())
|