synapse-hub 2.6.0 → 3.0.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 +4 -0
- package/.agents/agents/live-explorer.md +2 -0
- package/.agents/agents/orchestrator.md +16 -0
- package/.agents/agents/pom-crafter.md +10 -0
- package/.agents/agents/quality-gatekeeper.md +5 -0
- package/.agents/agents/test-runner.md +8 -0
- 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 +8 -1
- 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/lean-impact-scoreboard/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/repo-quality-audit/SKILL.md +1 -1
- package/.agents/skills/review-spec/SKILL.md +60 -0
- package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
- package/.agents/skills/senior-code-review/SKILL.md +1 -1
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
- package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
- 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/CLAUDE.md +2 -2
- package/README.en.md +70 -291
- package/README.md +68 -303
- package/docs/ARCHITECTURE.md +54 -8
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/QUALITY_GATES.md +113 -0
- package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
- package/package.json +1 -1
- package/scripts/package_dist.py +2 -1
- package/skills_manifest.json +92 -11
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +2 -2
- package/templates/ai_bridges/copilot-instructions.md +2 -2
- package/tools/TOOLS_RUNBOOK.md +1 -1
- package/tools/__init__.py +18 -1
- package/tools/_openapi_parser.py +158 -0
- package/tools/agent_canary.py +42 -30
- package/tools/agentless_patcher.py +417 -0
- package/tools/analyze_project.py +12 -11
- package/tools/api_drift_sentinel.py +4 -4
- package/tools/api_fuzzer.py +8 -8
- package/tools/auto_heal.py +23 -5
- package/tools/auto_heal_reflexion.py +2 -2
- package/tools/autogen_qa_broker.py +318 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +236 -0
- package/tools/bug_reporter.py +11 -13
- package/tools/bump_version.py +75 -5
- package/tools/cache_resolver.py +78 -83
- package/tools/check_env.py +9 -1
- package/tools/check_version_sync.py +40 -3
- package/tools/ci_healing_hook.py +5 -7
- package/tools/cli.py +5 -7
- package/tools/config_decoupler.py +2 -3
- package/tools/core/__init__.py +52 -0
- package/tools/count_tokens.py +22 -20
- package/tools/dashboard_generator.py +628 -999
- package/tools/db_sandbox.py +69 -53
- package/tools/debt_harvester.py +3 -3
- package/tools/distill_log.py +3 -3
- package/tools/dspy_qa_signatures.py +3 -4
- package/tools/export_postman_collection.py +8 -8
- package/tools/flaky_detector.py +2 -2
- package/tools/fuzz_payloads.py +5 -5
- package/tools/generate_allure_report.py +1 -1
- package/tools/generate_jmx_plan.py +4 -4
- package/tools/gherkin_suite.py +1 -1
- package/tools/git_pre_commit_guard.py +2 -2
- package/tools/goldfish_review.py +2 -2
- package/tools/heal/__init__.py +28 -0
- package/tools/heal_pr_automator.py +29 -4
- package/tools/heuristic_locator_matcher.py +2 -2
- package/tools/infer_linter.py +8 -8
- package/tools/interview_story.py +0 -1
- package/tools/jtl_analyzer.py +11 -11
- package/tools/lint/__init__.py +30 -0
- package/tools/lint_ai_docs.py +42 -0
- package/tools/lookup_tc.py +1 -1
- package/tools/matrix_sharder.py +15 -13
- package/tools/mcp/__init__.py +16 -0
- package/tools/mcp_excel_server.py +2 -2
- package/tools/mcp_server.py +3 -4
- package/tools/md_to_xlsx.py +26 -26
- package/tools/micro_mutator.py +11 -12
- package/tools/mipro_assertion_tuner.py +207 -0
- package/tools/network_mock_shield.py +27 -27
- package/tools/newman_analyzer.py +2 -3
- package/tools/omni_emitter.py +5 -8
- package/tools/omni_parser.py +5 -5
- package/tools/omni_transpiler.py +5 -5
- package/tools/output_formatter.py +19 -0
- package/tools/perf/__init__.py +24 -0
- package/tools/pinpoint_failure.py +13 -1
- package/tools/project_state.py +4 -2
- package/tools/qa_event_stream.py +336 -0
- package/tools/repo_map.py +8 -8
- package/tools/report/__init__.py +32 -0
- package/tools/reversible_sandbox.py +5 -2
- package/tools/safety_firewall.py +2 -2
- package/tools/senior_code_review.py +2 -2
- package/tools/setup_project.py +10 -12
- package/tools/skill_collision_guard.py +3 -3
- package/tools/skill_vault.py +12 -3
- package/tools/skillspector.py +5 -7
- package/tools/smart_visual_masker.py +4 -6
- package/tools/spec_drift_sentinel.py +1 -2
- package/tools/swe_qa_aci.py +369 -0
- package/tools/synapse_qa_bench.py +3 -3
- package/tools/sync_agent_rules.py +18 -4
- package/tools/telemetry_correlator.py +3 -2
- package/tools/test_memory_os.py +419 -0
- package/tools/textgrad_mutation_optimizer.py +3 -3
- package/tools/tms_sync_bridge.py +5 -5
- package/tools/tot_exploratory_tester.py +239 -0
- package/tools/traffic_session_converter.py +9 -9
- package/tools/universal_ir.py +2 -3
- package/tools/update_sentinel.py +6 -9
- package/tools/verify_all.py +10 -0
- package/tools/visual_diff_canvas.py +1 -2
- package/tools/visual_som_helper.py +15 -13
- package/tools/watch_mode.py +1 -1
- package/tools/wcag_audit.py +22 -1
- package/tools/welcome_page.py +5 -6
|
@@ -0,0 +1,336 @@
|
|
|
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
|
+
Immutable QA Event Stream Engine (OpenHands EventStream Pattern)
|
|
8
|
+
----------------------------------------------------------------
|
|
9
|
+
Provides a deterministic, append-only event stream for test automation sessions:
|
|
10
|
+
1. Immutable Event Logging: Sequential JSON Lines recording of all actor actions and observations.
|
|
11
|
+
2. Cryptographic Hash Chain: SHA-256 hash chaining ensures audit integrity and tamper resistance.
|
|
12
|
+
3. Reversible Checkpoints: Enables safe rollback to prior verified states on failures.
|
|
13
|
+
4. Stream Verification: Verifies stream schema, timestamp monotonicity, and hash chain integrity.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python tools/qa_event_stream.py init-stream --session-id "E2E_001" --out-file .agent_memory/timeline.jsonl
|
|
17
|
+
python tools/qa_event_stream.py record-event --stream .agent_memory/timeline.jsonl --event-type ACTION_CLICK --actor live-explorer --payload '{"target": "#btn"}'
|
|
18
|
+
python tools/qa_event_stream.py verify-stream --stream .agent_memory/timeline.jsonl
|
|
19
|
+
python tools/qa_event_stream.py rollback --stream .agent_memory/timeline.jsonl --to-checkpoint CHECKPOINT_PRE_CHECKOUT
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import json
|
|
25
|
+
import hashlib
|
|
26
|
+
import argparse
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from typing import Dict, Any, Optional
|
|
29
|
+
|
|
30
|
+
# Enforce UTF-8 stdout encoding
|
|
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
|
+
GENESIS_HASH = "0" * 64
|
|
39
|
+
VALID_EVENT_TYPES = {
|
|
40
|
+
"STREAM_INIT",
|
|
41
|
+
"ACTION_NAVIGATE",
|
|
42
|
+
"ACTION_CLICK",
|
|
43
|
+
"ACTION_TYPE",
|
|
44
|
+
"OBSERVATION_DOM",
|
|
45
|
+
"ASSERTION_EVALUATED",
|
|
46
|
+
"CHECKPOINT",
|
|
47
|
+
"ERROR_OCCURRED",
|
|
48
|
+
"ROLLBACK_EXECUTED",
|
|
49
|
+
"TEST_COMPLETED"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class QaEventStream:
|
|
54
|
+
"""Immutable Event Stream with SHA-256 Hash Chaining."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, repo_root: Optional[str] = None):
|
|
57
|
+
self.repo_root = repo_root or os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
58
|
+
|
|
59
|
+
def _compute_hash(self, prev_hash: str, session_id: str, event_type: str,
|
|
60
|
+
actor: str, payload_str: str, timestamp: str) -> str:
|
|
61
|
+
raw = f"{prev_hash}|{session_id}|{event_type}|{actor}|{payload_str}|{timestamp}"
|
|
62
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
63
|
+
|
|
64
|
+
def init_stream(self, session_id: str, out_file: str) -> Dict[str, Any]:
|
|
65
|
+
"""Initializes an append-only timeline stream file."""
|
|
66
|
+
resolved = os.path.join(self.repo_root, out_file) if not os.path.isabs(out_file) else out_file
|
|
67
|
+
os.makedirs(os.path.dirname(os.path.abspath(resolved)), exist_ok=True)
|
|
68
|
+
|
|
69
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
70
|
+
init_payload = {"session_id": session_id, "initialized_at": now}
|
|
71
|
+
init_payload_str = json.dumps(init_payload, sort_keys=True)
|
|
72
|
+
event_hash = self._compute_hash(GENESIS_HASH, session_id, "STREAM_INIT", "system", init_payload_str, now)
|
|
73
|
+
|
|
74
|
+
entry = {
|
|
75
|
+
"index": 0,
|
|
76
|
+
"session_id": session_id,
|
|
77
|
+
"event_type": "STREAM_INIT",
|
|
78
|
+
"actor": "system",
|
|
79
|
+
"payload": init_payload,
|
|
80
|
+
"timestamp": now,
|
|
81
|
+
"prev_hash": GENESIS_HASH,
|
|
82
|
+
"event_hash": event_hash
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
with open(resolved, "w", encoding="utf-8", newline="\n") as f:
|
|
86
|
+
f.write(json.dumps(entry) + "\n")
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"status": "PASS",
|
|
90
|
+
"session_id": session_id,
|
|
91
|
+
"out_file": resolved,
|
|
92
|
+
"genesis_hash": event_hash
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def record_event(self, stream_file: str, event_type: str, actor: str,
|
|
96
|
+
payload: Any, checkpoint_id: Optional[str] = None) -> Dict[str, Any]:
|
|
97
|
+
"""Appends an event to the stream with continuous cryptographic hash chain."""
|
|
98
|
+
resolved = os.path.join(self.repo_root, stream_file) if not os.path.isabs(stream_file) else stream_file
|
|
99
|
+
if not os.path.exists(resolved):
|
|
100
|
+
return {"status": "FAIL", "error": f"Stream file not found: {stream_file}"}
|
|
101
|
+
|
|
102
|
+
# Read last line to get index and prev_hash
|
|
103
|
+
last_entry = None
|
|
104
|
+
total_count = 0
|
|
105
|
+
with open(resolved, "r", encoding="utf-8", errors="replace") as f:
|
|
106
|
+
for line in f:
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if stripped:
|
|
109
|
+
try:
|
|
110
|
+
last_entry = json.loads(stripped)
|
|
111
|
+
total_count += 1
|
|
112
|
+
except Exception as err:
|
|
113
|
+
sys.stderr.write(f"Notice: Skip malformed stream line: {err}\n")
|
|
114
|
+
|
|
115
|
+
if not last_entry:
|
|
116
|
+
return {"status": "FAIL", "error": "Stream file is empty or corrupted"}
|
|
117
|
+
|
|
118
|
+
prev_hash = last_entry.get("event_hash", GENESIS_HASH)
|
|
119
|
+
session_id = last_entry.get("session_id", "default_session")
|
|
120
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
121
|
+
|
|
122
|
+
if checkpoint_id:
|
|
123
|
+
if isinstance(payload, dict):
|
|
124
|
+
payload["checkpoint_id"] = checkpoint_id
|
|
125
|
+
else:
|
|
126
|
+
payload = {"data": payload, "checkpoint_id": checkpoint_id}
|
|
127
|
+
|
|
128
|
+
payload_str = json.dumps(payload, sort_keys=True) if not isinstance(payload, str) else payload
|
|
129
|
+
event_hash = self._compute_hash(prev_hash, session_id, event_type, actor, payload_str, now)
|
|
130
|
+
|
|
131
|
+
new_entry = {
|
|
132
|
+
"index": total_count,
|
|
133
|
+
"session_id": session_id,
|
|
134
|
+
"event_type": event_type,
|
|
135
|
+
"actor": actor,
|
|
136
|
+
"payload": payload if not isinstance(payload, str) else json.loads(payload_str),
|
|
137
|
+
"timestamp": now,
|
|
138
|
+
"prev_hash": prev_hash,
|
|
139
|
+
"event_hash": event_hash
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
with open(resolved, "a", encoding="utf-8", newline="\n") as f:
|
|
143
|
+
f.write(json.dumps(new_entry) + "\n")
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"status": "PASS",
|
|
147
|
+
"index": total_count,
|
|
148
|
+
"event_type": event_type,
|
|
149
|
+
"event_hash": event_hash,
|
|
150
|
+
"prev_hash": prev_hash
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def verify_stream(self, stream_file: str) -> Dict[str, Any]:
|
|
154
|
+
"""Verifies hash chain integrity and JSON schema compliance (Gate 12)."""
|
|
155
|
+
resolved = os.path.join(self.repo_root, stream_file) if not os.path.isabs(stream_file) else stream_file
|
|
156
|
+
if not os.path.exists(resolved):
|
|
157
|
+
return {"status": "FAIL", "verified": False, "error": f"Stream file not found: {stream_file}"}
|
|
158
|
+
|
|
159
|
+
events = []
|
|
160
|
+
with open(resolved, "r", encoding="utf-8", errors="replace") as f:
|
|
161
|
+
for line in f:
|
|
162
|
+
stripped = line.strip()
|
|
163
|
+
if stripped:
|
|
164
|
+
try:
|
|
165
|
+
events.append(json.loads(stripped))
|
|
166
|
+
except Exception as err:
|
|
167
|
+
return {"status": "FAIL", "verified": False, "error": f"JSON decode error: {err}"}
|
|
168
|
+
|
|
169
|
+
if not events:
|
|
170
|
+
return {"status": "FAIL", "verified": False, "error": "Stream is empty"}
|
|
171
|
+
|
|
172
|
+
expected_prev = GENESIS_HASH
|
|
173
|
+
for idx, ev in enumerate(events):
|
|
174
|
+
if ev.get("index") != idx:
|
|
175
|
+
return {
|
|
176
|
+
"status": "FAIL",
|
|
177
|
+
"verified": False,
|
|
178
|
+
"error": f"Index mismatch at position {idx}: expected {idx}, got {ev.get('index')}"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if ev.get("prev_hash") != expected_prev:
|
|
182
|
+
return {
|
|
183
|
+
"status": "FAIL",
|
|
184
|
+
"verified": False,
|
|
185
|
+
"error": f"Broken hash chain at index {idx}: expected prev_hash {expected_prev}, got {ev.get('prev_hash')}"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
payload_str = json.dumps(ev.get("payload"), sort_keys=True)
|
|
189
|
+
recalc = self._compute_hash(
|
|
190
|
+
expected_prev,
|
|
191
|
+
ev.get("session_id", ""),
|
|
192
|
+
ev.get("event_type", ""),
|
|
193
|
+
ev.get("actor", ""),
|
|
194
|
+
payload_str,
|
|
195
|
+
ev.get("timestamp", "")
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if recalc != ev.get("event_hash"):
|
|
199
|
+
return {
|
|
200
|
+
"status": "FAIL",
|
|
201
|
+
"verified": False,
|
|
202
|
+
"error": f"Hash tamper detected at index {idx}: declared {ev.get('event_hash')}, computed {recalc}"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expected_prev = ev["event_hash"]
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"status": "PASS",
|
|
209
|
+
"verified": True,
|
|
210
|
+
"events_verified": len(events),
|
|
211
|
+
"final_event_hash": expected_prev,
|
|
212
|
+
"integrity": "CRYPTOGRAPHICALLY_VERIFIED"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
def rollback(self, stream_file: str, to_checkpoint: str) -> Dict[str, Any]:
|
|
216
|
+
"""Rolls back execution state to a designated checkpoint in the stream."""
|
|
217
|
+
resolved = os.path.join(self.repo_root, stream_file) if not os.path.isabs(stream_file) else stream_file
|
|
218
|
+
if not os.path.exists(resolved):
|
|
219
|
+
return {"status": "FAIL", "error": f"Stream file not found: {stream_file}"}
|
|
220
|
+
|
|
221
|
+
target_checkpoint = None
|
|
222
|
+
checkpoint_index = -1
|
|
223
|
+
events = []
|
|
224
|
+
|
|
225
|
+
with open(resolved, "r", encoding="utf-8", errors="replace") as f:
|
|
226
|
+
for line in f:
|
|
227
|
+
stripped = line.strip()
|
|
228
|
+
if stripped:
|
|
229
|
+
try:
|
|
230
|
+
ev = json.loads(stripped)
|
|
231
|
+
events.append(ev)
|
|
232
|
+
p = ev.get("payload", {})
|
|
233
|
+
if isinstance(p, dict) and p.get("checkpoint_id") == to_checkpoint:
|
|
234
|
+
target_checkpoint = ev
|
|
235
|
+
checkpoint_index = ev.get("index", -1)
|
|
236
|
+
except Exception as err:
|
|
237
|
+
sys.stderr.write(f"Notice: Skip malformed stream line during rollback: {err}\n")
|
|
238
|
+
|
|
239
|
+
if not target_checkpoint:
|
|
240
|
+
return {
|
|
241
|
+
"status": "FAIL",
|
|
242
|
+
"error": f"Checkpoint '{to_checkpoint}' not found in event stream."
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
rolled_back_count = len(events) - (checkpoint_index + 1)
|
|
246
|
+
|
|
247
|
+
# Record ROLLBACK_EXECUTED event in the immutable stream
|
|
248
|
+
rollback_payload = {
|
|
249
|
+
"target_checkpoint": to_checkpoint,
|
|
250
|
+
"target_index": checkpoint_index,
|
|
251
|
+
"events_rewound": rolled_back_count,
|
|
252
|
+
"restored_state": target_checkpoint.get("payload")
|
|
253
|
+
}
|
|
254
|
+
rec_res = self.record_event(
|
|
255
|
+
stream_file=stream_file,
|
|
256
|
+
event_type="ROLLBACK_EXECUTED",
|
|
257
|
+
actor="system",
|
|
258
|
+
payload=rollback_payload
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"status": "PASS",
|
|
263
|
+
"to_checkpoint": to_checkpoint,
|
|
264
|
+
"target_index": checkpoint_index,
|
|
265
|
+
"events_rewound": rolled_back_count,
|
|
266
|
+
"new_event_hash": rec_res.get("event_hash"),
|
|
267
|
+
"restored_state": target_checkpoint.get("payload")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main():
|
|
272
|
+
parser = argparse.ArgumentParser(description="QA Event Stream Engine (OpenHands Pattern)")
|
|
273
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Subcommand to execute")
|
|
274
|
+
|
|
275
|
+
# init-stream
|
|
276
|
+
init_p = subparsers.add_parser("init-stream", help="Initialize a new event stream")
|
|
277
|
+
init_p.add_argument("--session-id", required=True, help="Unique test session ID")
|
|
278
|
+
init_p.add_argument("--out-file", required=True, help="Path to write JSONL stream")
|
|
279
|
+
|
|
280
|
+
# record-event
|
|
281
|
+
rec_p = subparsers.add_parser("record-event", help="Append an event to the stream")
|
|
282
|
+
rec_p.add_argument("--stream", required=True, help="Path to stream file")
|
|
283
|
+
rec_p.add_argument("--event-type", required=True, help="Event type (ACTION_CLICK, CHECKPOINT, etc.)")
|
|
284
|
+
rec_p.add_argument("--actor", required=True, help="Actor name (live-explorer, test-runner, etc.)")
|
|
285
|
+
rec_p.add_argument("--payload", required=True, help="JSON payload string")
|
|
286
|
+
rec_p.add_argument("--checkpoint-id", help="Optional checkpoint ID")
|
|
287
|
+
|
|
288
|
+
# verify-stream
|
|
289
|
+
ver_p = subparsers.add_parser("verify-stream", help="Verify cryptographic hash chain integrity")
|
|
290
|
+
ver_p.add_argument("--stream", required=True, help="Path to stream file")
|
|
291
|
+
|
|
292
|
+
# rollback
|
|
293
|
+
rb_p = subparsers.add_parser("rollback", help="Roll back state to a checkpoint")
|
|
294
|
+
rb_p.add_argument("--stream", required=True, help="Path to stream file")
|
|
295
|
+
rb_p.add_argument("--to-checkpoint", required=True, help="Target checkpoint ID")
|
|
296
|
+
|
|
297
|
+
args = parser.parse_args()
|
|
298
|
+
engine = QaEventStream()
|
|
299
|
+
|
|
300
|
+
if args.subcommand == "init-stream":
|
|
301
|
+
res = engine.init_stream(session_id=args.session_id, out_file=args.out_file)
|
|
302
|
+
print(json.dumps(res, indent=2))
|
|
303
|
+
sys.exit(0 if res["status"] == "PASS" else 1)
|
|
304
|
+
|
|
305
|
+
elif args.subcommand == "record-event":
|
|
306
|
+
try:
|
|
307
|
+
parsed_payload = json.loads(args.payload)
|
|
308
|
+
except Exception:
|
|
309
|
+
parsed_payload = args.payload
|
|
310
|
+
res = engine.record_event(
|
|
311
|
+
stream_file=args.stream,
|
|
312
|
+
event_type=args.event_type,
|
|
313
|
+
actor=args.actor,
|
|
314
|
+
payload=parsed_payload,
|
|
315
|
+
checkpoint_id=args.checkpoint_id
|
|
316
|
+
)
|
|
317
|
+
print(json.dumps(res, indent=2))
|
|
318
|
+
sys.exit(0 if res["status"] == "PASS" else 1)
|
|
319
|
+
|
|
320
|
+
elif args.subcommand == "verify-stream":
|
|
321
|
+
res = engine.verify_stream(stream_file=args.stream)
|
|
322
|
+
print(json.dumps(res, indent=2))
|
|
323
|
+
sys.exit(0 if res["status"] == "PASS" else 1)
|
|
324
|
+
|
|
325
|
+
elif args.subcommand == "rollback":
|
|
326
|
+
res = engine.rollback(stream_file=args.stream, to_checkpoint=args.to_checkpoint)
|
|
327
|
+
print(json.dumps(res, indent=2))
|
|
328
|
+
sys.exit(0 if res["status"] == "PASS" else 1)
|
|
329
|
+
|
|
330
|
+
else:
|
|
331
|
+
parser.print_help()
|
|
332
|
+
sys.exit(1)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
main()
|
package/tools/repo_map.py
CHANGED
|
@@ -199,13 +199,13 @@ def build_repo_map(target_repo=None, output_txt=None, output_json=None):
|
|
|
199
199
|
# Render text map
|
|
200
200
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
201
201
|
out_lines = [
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
"# ==============================================================================",
|
|
203
|
+
"# ENTERPRISE AUTOMATION REPOSITORY MAP (Aider-Style AST Compression)",
|
|
204
204
|
f"# Generated : {timestamp}",
|
|
205
205
|
f"# Target : {repo}",
|
|
206
206
|
f"# Scope : {len(classes)} Classes ({total_raw_lines} source lines, {total_raw_bytes / 1024:.1f} KB)",
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
"# Purpose : Token-budgeted index (< 600 tokens) for instant method lookup",
|
|
208
|
+
"# ==============================================================================\n"
|
|
209
209
|
]
|
|
210
210
|
|
|
211
211
|
total_methods = 0
|
|
@@ -294,9 +294,9 @@ def main():
|
|
|
294
294
|
return 1
|
|
295
295
|
|
|
296
296
|
stats = build_repo_map(target_repo=target_repo)
|
|
297
|
-
print(
|
|
298
|
-
print(
|
|
299
|
-
print(
|
|
297
|
+
print("\n======================================================================")
|
|
298
|
+
print(" Enterprise Repository Map Compressor (Aider-Style AST Map) ")
|
|
299
|
+
print("======================================================================")
|
|
300
300
|
print(f" Target Repository : {stats['target_repo']}")
|
|
301
301
|
print(f" Indexed Classes : {stats['classes_count']} Page Classes & Helpers")
|
|
302
302
|
print(f" Extracted Methods : {stats['methods_count']} API signatures")
|
|
@@ -304,7 +304,7 @@ def main():
|
|
|
304
304
|
print(f" Compressed Map Size : {stats['compressed_bytes'] / 1024:.1f} KB (~{stats['estimated_tokens']} tokens)")
|
|
305
305
|
print(f" Compression Ratio : {stats['compression_ratio_pct']:.1f}% reduction")
|
|
306
306
|
print(f" Artifact Generated : {stats['output_file']}")
|
|
307
|
-
print(
|
|
307
|
+
print("======================================================================\n")
|
|
308
308
|
|
|
309
309
|
if args.print:
|
|
310
310
|
with open(stats['output_file'], "r", encoding="utf-8") as f:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synapse Hub — Reporting & Telemetry Engines (tools.report)
|
|
3
|
+
----------------------------------------------------------
|
|
4
|
+
Domain package grouping all HTML dashboard, spreadsheet, JTL/Newman
|
|
5
|
+
metric parser, and visual reporting tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tools import (
|
|
9
|
+
bug_reporter,
|
|
10
|
+
dashboard_generator,
|
|
11
|
+
generate_allure_report,
|
|
12
|
+
generate_test_reports,
|
|
13
|
+
jtl_analyzer,
|
|
14
|
+
md_to_xlsx,
|
|
15
|
+
newman_analyzer,
|
|
16
|
+
output_formatter,
|
|
17
|
+
telemetry_correlator,
|
|
18
|
+
welcome_page,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"bug_reporter",
|
|
23
|
+
"dashboard_generator",
|
|
24
|
+
"generate_allure_report",
|
|
25
|
+
"generate_test_reports",
|
|
26
|
+
"jtl_analyzer",
|
|
27
|
+
"md_to_xlsx",
|
|
28
|
+
"newman_analyzer",
|
|
29
|
+
"output_formatter",
|
|
30
|
+
"telemetry_correlator",
|
|
31
|
+
"welcome_page",
|
|
32
|
+
]
|
|
@@ -17,11 +17,10 @@ Reference:
|
|
|
17
17
|
|
|
18
18
|
import os
|
|
19
19
|
import sys
|
|
20
|
-
import json
|
|
21
20
|
import shlex
|
|
22
21
|
import subprocess
|
|
23
22
|
from dataclasses import dataclass, field
|
|
24
|
-
from typing import List, Optional,
|
|
23
|
+
from typing import List, Optional, Dict, Any
|
|
25
24
|
|
|
26
25
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
27
26
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -120,6 +119,10 @@ class ExecutionSandbox:
|
|
|
120
119
|
|
|
121
120
|
return results
|
|
122
121
|
|
|
122
|
+
def stack_size(self) -> int:
|
|
123
|
+
"""Returns the number of pending undo actions in the stack."""
|
|
124
|
+
return len(self.undo_stack)
|
|
125
|
+
|
|
123
126
|
def __enter__(self):
|
|
124
127
|
return self
|
|
125
128
|
|
package/tools/safety_firewall.py
CHANGED
|
@@ -245,7 +245,7 @@ def detect_action_loop(action_history: List[str], repeat_threshold: int = 4) ->
|
|
|
245
245
|
"""
|
|
246
246
|
Detects infinite action loops and repeating cycle patterns among agent tool calls.
|
|
247
247
|
Inspired by agentblazo/blazo deterministic detection rules.
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
Checks:
|
|
250
250
|
1. Consecutive identical actions (>= repeat_threshold)
|
|
251
251
|
2. 2-action repeating cycle: [A, B, A, B]
|
|
@@ -411,7 +411,7 @@ def main():
|
|
|
411
411
|
if all_authorized:
|
|
412
412
|
print(f"\n✅ All actions cleared by Safety Firewall{' (Dry-Run)' if args.dry_run else ''}.\n")
|
|
413
413
|
else:
|
|
414
|
-
print(
|
|
414
|
+
print("\n❌ Pre-execution Blocked: Destructive actions intercepted without --force or ALLOW_DESTRUCTIVE=1.\n")
|
|
415
415
|
|
|
416
416
|
return 0 if all_authorized else 1
|
|
417
417
|
|
|
@@ -28,7 +28,7 @@ import re
|
|
|
28
28
|
import json
|
|
29
29
|
import subprocess
|
|
30
30
|
import argparse
|
|
31
|
-
from typing import Dict, List, Any,
|
|
31
|
+
from typing import Dict, List, Any, Tuple
|
|
32
32
|
|
|
33
33
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
34
34
|
try:
|
|
@@ -83,7 +83,7 @@ def get_git_diff(repo_dir: str, target: str = "uncommitted") -> str:
|
|
|
83
83
|
check=True
|
|
84
84
|
)
|
|
85
85
|
return res.stdout
|
|
86
|
-
except Exception
|
|
86
|
+
except Exception:
|
|
87
87
|
return ""
|
|
88
88
|
|
|
89
89
|
|
package/tools/setup_project.py
CHANGED
|
@@ -24,7 +24,6 @@ import json
|
|
|
24
24
|
import shutil
|
|
25
25
|
import argparse
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from datetime import datetime
|
|
28
27
|
|
|
29
28
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
30
29
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
@@ -39,9 +38,8 @@ HUB_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
|
39
38
|
TEMPLATES_DIR = os.path.join(HUB_ROOT, "templates")
|
|
40
39
|
sys.path.insert(0, SCRIPT_DIR)
|
|
41
40
|
|
|
42
|
-
import analyze_project
|
|
43
|
-
import
|
|
44
|
-
import project_state
|
|
41
|
+
import analyze_project # noqa: E402
|
|
42
|
+
import project_state # noqa: E402
|
|
45
43
|
|
|
46
44
|
GREEN = "\033[92m"
|
|
47
45
|
RED = "\033[91m"
|
|
@@ -412,7 +410,7 @@ def inject_antigravity_global_plugin():
|
|
|
412
410
|
try:
|
|
413
411
|
os.unlink(dst)
|
|
414
412
|
except OSError:
|
|
415
|
-
|
|
413
|
+
_ = False
|
|
416
414
|
if sys.platform == "win32":
|
|
417
415
|
try:
|
|
418
416
|
import _winapi
|
|
@@ -420,7 +418,7 @@ def inject_antigravity_global_plugin():
|
|
|
420
418
|
_winapi.CreateJunction(src, dst)
|
|
421
419
|
return
|
|
422
420
|
except (OSError, ImportError, AttributeError):
|
|
423
|
-
|
|
421
|
+
_ = False
|
|
424
422
|
shutil.copytree(src, dst, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.tmp"))
|
|
425
423
|
|
|
426
424
|
sync_dir(os.path.join(HUB_ROOT, ".agents", "skills"), os.path.join(target_plugin_dir, "skills"))
|
|
@@ -612,26 +610,26 @@ def run_setup(target_repo, dry_run=False, profile="auto", no_global=True, no_git
|
|
|
612
610
|
if dry_run:
|
|
613
611
|
print(f"Operational Mode : {YELLOW}DRY-RUN (Simulating without modifying files on disk){RESET}\n")
|
|
614
612
|
else:
|
|
615
|
-
print(
|
|
613
|
+
print("Operational Mode : APPLY CHANGES\n")
|
|
616
614
|
|
|
617
615
|
if dry_run:
|
|
618
616
|
print(f"{YELLOW}[DRY-RUN PREVIEW]{RESET}")
|
|
619
617
|
print(f" • Profile: {profile.upper()}")
|
|
620
618
|
print(f" • Would generate: {os.path.join(target_repo, 'PROJECT_AI_GUIDE.md')}")
|
|
621
|
-
print(
|
|
619
|
+
print(" • Would deploy AI bridges: .cursor/rules/synapse-qa.mdc, .github/copilot-instructions.md, .windsurfrules")
|
|
622
620
|
print(f" • Would install pre-push hook: {os.path.join(target_repo, '.git', 'hooks', 'pre-push')}")
|
|
623
|
-
print(
|
|
621
|
+
print(" • Would initialize: Central Agent Memory (~/.agent_memory/global_cache.db)")
|
|
624
622
|
print(f"\n{GREEN}✅ Dry-run completed successfully. Zero changes written to disk.{RESET}\n")
|
|
625
623
|
return 0
|
|
626
624
|
|
|
627
625
|
# 2. Ensure PROJECT_AI_GUIDE.md & PROJECT_STATE.md
|
|
628
626
|
print(f"{BOLD}[1/5] Checking Project AI Guide & State Anchor...{RESET}")
|
|
629
|
-
|
|
627
|
+
ensure_project_ai_guide(target_repo, stack_info, profile=profile)
|
|
630
628
|
project_state.init_state(target_repo)
|
|
631
629
|
|
|
632
630
|
# 3. Deploy Universal AI Bridges (Cursor, Claude Code, GitHub Copilot, Windsurf)
|
|
633
631
|
print(f"\n{BOLD}[2/5] Deploying Universal AI Bridges (Cursor, Claude, Copilot, Windsurf)...{RESET}")
|
|
634
|
-
|
|
632
|
+
install_universal_ai_bridges(target_repo)
|
|
635
633
|
|
|
636
634
|
# 4. Inject Antigravity Global Plugin
|
|
637
635
|
plugin_injected = False
|
|
@@ -687,7 +685,7 @@ def run_setup(target_repo, dry_run=False, profile="auto", no_global=True, no_git
|
|
|
687
685
|
print(f" Target Framework : {stack_info['framework']}")
|
|
688
686
|
print(f" Active Profile : {profile.upper()}")
|
|
689
687
|
print(f" AI Project Guide : {os.path.join(target_repo, 'PROJECT_AI_GUIDE.md')}")
|
|
690
|
-
print(
|
|
688
|
+
print(" Universal Bridges : Cursor (.cursor/rules), Claude Code, Copilot, Windsurf")
|
|
691
689
|
print(f" Antigravity Plugin : {'ACTIVE (~/.gemini/config/plugins/synapse-hub)' if plugin_injected else 'STANDALONE'}")
|
|
692
690
|
print(f" AST Method Map : {os.path.join(target_repo, 'repo_map.txt')}")
|
|
693
691
|
print(f" Health Report : {os.path.join(target_repo, 'PROJECT_HEALTH_REPORT.md')}")
|
|
@@ -23,8 +23,8 @@ import sys
|
|
|
23
23
|
import re
|
|
24
24
|
import math
|
|
25
25
|
import json
|
|
26
|
+
from typing import Dict, List, Tuple, Any
|
|
26
27
|
import argparse
|
|
27
|
-
from typing import Dict, List, Tuple
|
|
28
28
|
from collections import Counter
|
|
29
29
|
|
|
30
30
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
@@ -136,7 +136,7 @@ class SkillCollisionGuard:
|
|
|
136
136
|
fallback_name = os.path.basename(os.path.dirname(path))
|
|
137
137
|
return fallback_name, ""
|
|
138
138
|
|
|
139
|
-
def audit(self) -> Tuple[bool, List[Dict[str,
|
|
139
|
+
def audit(self) -> Tuple[bool, List[Dict[str, Any]]]:
|
|
140
140
|
"""
|
|
141
141
|
Computes pairwise similarity.
|
|
142
142
|
Returns (is_clean, report_items).
|
|
@@ -211,7 +211,7 @@ def run_guard(skills_dir: str = ".agents/skills", threshold: float = 0.75, warn_
|
|
|
211
211
|
print("=" * 70 + "\n")
|
|
212
212
|
return 1
|
|
213
213
|
|
|
214
|
-
print(
|
|
214
|
+
print("\n✅ [PASS] 0 routing collisions detected. All skill descriptions uniquely segmented.\n")
|
|
215
215
|
print("=" * 70 + "\n")
|
|
216
216
|
return 0
|
|
217
217
|
|
package/tools/skill_vault.py
CHANGED
|
@@ -22,7 +22,7 @@ import json
|
|
|
22
22
|
import re
|
|
23
23
|
import difflib
|
|
24
24
|
import argparse
|
|
25
|
-
from dataclasses import dataclass
|
|
25
|
+
from dataclasses import dataclass
|
|
26
26
|
from typing import List, Optional, Dict, Any, Set
|
|
27
27
|
|
|
28
28
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
@@ -101,6 +101,15 @@ class PageClassPrimitive:
|
|
|
101
101
|
tags: str = "" # Comma-separated domain tags, e.g. "auth,sso,login"
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
class ManagedConnection(sqlite3.Connection):
|
|
105
|
+
"""SQLite connection that automatically commits/rollbacks and closes on context exit."""
|
|
106
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
107
|
+
try:
|
|
108
|
+
super().__exit__(exc_type, exc_val, exc_tb)
|
|
109
|
+
finally:
|
|
110
|
+
self.close()
|
|
111
|
+
|
|
112
|
+
|
|
104
113
|
class SkillVault:
|
|
105
114
|
"""Persistent library of verified, self-contained Page Class methods."""
|
|
106
115
|
|
|
@@ -113,8 +122,8 @@ class SkillVault:
|
|
|
113
122
|
self.db_path = db_path
|
|
114
123
|
self._init_schema()
|
|
115
124
|
|
|
116
|
-
def _get_connection(self) ->
|
|
117
|
-
conn = sqlite3.connect(self.db_path)
|
|
125
|
+
def _get_connection(self) -> ManagedConnection:
|
|
126
|
+
conn = sqlite3.connect(self.db_path, factory=ManagedConnection)
|
|
118
127
|
conn.row_factory = sqlite3.Row
|
|
119
128
|
return conn
|
|
120
129
|
|
package/tools/skillspector.py
CHANGED
|
@@ -42,17 +42,15 @@ Usage:
|
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
44
|
import ast
|
|
45
|
-
import fnmatch
|
|
46
45
|
import hashlib
|
|
47
46
|
import json
|
|
48
47
|
import os
|
|
49
48
|
import re
|
|
50
49
|
import sys
|
|
51
|
-
import unicodedata
|
|
52
50
|
from dataclasses import dataclass, field
|
|
53
51
|
from enum import Enum
|
|
54
52
|
from pathlib import Path
|
|
55
|
-
from typing import Any, Dict, List,
|
|
53
|
+
from typing import Any, Dict, List, Tuple
|
|
56
54
|
|
|
57
55
|
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
58
56
|
if hasattr(sys.stdout, "reconfigure"):
|
|
@@ -529,9 +527,9 @@ def scan_text_patterns(file_path: str, content: str) -> List[Finding]:
|
|
|
529
527
|
# Line number offset indexing
|
|
530
528
|
line_offsets = []
|
|
531
529
|
curr = 0
|
|
532
|
-
for
|
|
530
|
+
for line_str in lines:
|
|
533
531
|
line_offsets.append(curr)
|
|
534
|
-
curr += len(
|
|
532
|
+
curr += len(line_str)
|
|
535
533
|
|
|
536
534
|
def get_line_num(char_idx: int) -> int:
|
|
537
535
|
for idx, offset in enumerate(line_offsets):
|
|
@@ -925,7 +923,7 @@ def scan_file(file_path: str) -> List[Finding]:
|
|
|
925
923
|
try:
|
|
926
924
|
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
927
925
|
content = f.read()
|
|
928
|
-
except Exception
|
|
926
|
+
except Exception:
|
|
929
927
|
return []
|
|
930
928
|
|
|
931
929
|
findings: List[Finding] = []
|
|
@@ -1013,7 +1011,7 @@ def format_terminal(report: ScanReport) -> str:
|
|
|
1013
1011
|
out.append(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
1014
1012
|
out.append(f"📁 Target Scope : {report.target_path}")
|
|
1015
1013
|
out.append(f"📄 Scanned Files : {report.scanned_files}")
|
|
1016
|
-
out.append(
|
|
1014
|
+
out.append("🛡️ Engine Source : NVIDIA/SkillSpector Spec (71 Patterns, 17 Categories)")
|
|
1017
1015
|
|
|
1018
1016
|
rec_color = GREEN if report.recommendation == Recommendation.SAFE else (YELLOW if report.recommendation == Recommendation.CAUTION else RED)
|
|
1019
1017
|
out.append(f"\n{BOLD}🎯 AUDIT VERDICT :{RESET} {rec_color}{BOLD}{report.recommendation.value}{RESET}")
|