synapse-hub 2.2.3 → 2.4.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.
Files changed (317) hide show
  1. package/.agents/agents/failure-analyst.md +11 -0
  2. package/.agents/agents/live-explorer.md +28 -0
  3. package/.agents/agents/orchestrator.md +20 -4
  4. package/.agents/agents/pom-crafter.md +12 -0
  5. package/.agents/agents/quality-gatekeeper.md +19 -4
  6. package/.agents/agents/test-runner.md +11 -0
  7. package/.agents/rules/communication-style.md +40 -0
  8. package/.agents/rules/context-engineering.md +23 -0
  9. package/.agents/rules/git-push-protocol.md +11 -17
  10. package/.agents/rules/graphify.md +14 -14
  11. package/.agents/rules/lessons-learned.md +3 -3
  12. package/.agents/rules/mandatory-playwright-live.md +29 -29
  13. package/.agents/rules/mobile_appium_rules.md +132 -0
  14. package/.agents/rules/performance_testing_rules.md +74 -0
  15. package/.agents/rules/skill-precedence.md +1 -1
  16. package/.agents/rules/systematic-debugging.md +6 -6
  17. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
  18. package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
  19. package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
  20. package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
  21. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  22. package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
  23. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
  24. package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
  25. package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
  26. package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
  27. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +4 -1
  28. package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
  29. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
  30. package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
  31. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  32. package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
  33. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  34. package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
  35. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  36. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  37. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
  38. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
  39. package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +9 -7
  40. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
  41. package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
  42. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  43. package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
  44. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  45. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
  46. package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
  47. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  48. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  49. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  50. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  51. package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
  52. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
  53. package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
  54. package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
  55. package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
  56. package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
  57. package/.agents/skills/github-actions-templates/SKILL.md +47 -304
  58. package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
  59. package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
  60. package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
  61. package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
  62. package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
  63. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  64. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  65. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  66. package/.agents/skills/lam-script-generator/SKILL.md +42 -1
  67. package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
  68. package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
  69. package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
  70. package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
  71. package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
  72. package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
  73. package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
  74. package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
  75. package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
  76. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  77. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  78. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  79. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  80. package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
  81. package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
  82. package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
  83. package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
  84. package/.agents/skills/security-dast-sentinel/SKILL.md +57 -135
  85. package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
  86. package/.agents/skills/self-healing-locator/SKILL.md +24 -10
  87. package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
  88. package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
  89. package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
  90. package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
  91. package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
  92. package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
  93. package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
  94. package/.agents/skills/systematic-debugging/SKILL.md +100 -0
  95. package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
  96. package/.agents/skills/test-driven-development/SKILL.md +114 -0
  97. package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
  98. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  99. package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
  100. package/.agents/skills/verification-before-completion/SKILL.md +100 -0
  101. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
  102. package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
  103. package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
  104. package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
  105. package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
  106. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  107. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  108. package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
  109. package/.agents/templates/completion_report.md +57 -57
  110. package/.agents/workflows/analyze_requirement_document.md +18 -0
  111. package/.agents/workflows/export_testcases_excel.md +32 -0
  112. package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
  113. package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
  114. package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
  115. package/.agents/workflows/generate_requirements_from_website.md +25 -0
  116. package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
  117. package/.agents/workflows/graphify.md +10 -10
  118. package/.agents/workflows/verify.md +2 -2
  119. package/.cursor/rules/language.mdc +7 -7
  120. package/.cursor/rules/synapse-qa.mdc +23 -0
  121. package/.cursorrules +7 -0
  122. package/.pre-commit-hooks.yaml +14 -0
  123. package/.windsurfrules +7 -0
  124. package/AGENTS.md +54 -15
  125. package/CLAUDE.md +35 -12
  126. package/LICENSE +30 -0
  127. package/NOTICE +180 -0
  128. package/README.en.md +325 -0
  129. package/README.md +247 -512
  130. package/action.yml +51 -0
  131. package/benchmarks/qa_eval/README.md +71 -0
  132. package/benchmarks/qa_eval/build_dataset.py +677 -0
  133. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
  134. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
  135. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
  136. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
  137. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
  138. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
  139. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
  140. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
  141. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
  142. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
  143. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
  144. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
  145. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
  146. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
  147. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
  148. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
  149. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
  150. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
  151. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
  152. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
  153. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
  154. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
  155. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
  156. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
  157. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
  158. package/benchmarks/qa_eval/eval_runner.py +418 -0
  159. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
  160. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
  161. package/benchmarks/qa_eval/reports/eval_results.json +105 -0
  162. package/bin/agent-qa.js +10 -3
  163. package/docs/ARCHITECTURE.md +107 -0
  164. package/docs/CI_CD_INTEGRATION.md +94 -0
  165. package/docs/CLI_REFERENCE.md +157 -0
  166. package/docs/CONTRIBUTING.md +91 -0
  167. package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
  168. package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
  169. package/docs/adopted-ecosystem-tools.md +87 -0
  170. package/docs/schemas/synapse.config.schema.json +64 -0
  171. package/examples/cypress-ecommerce-suite/README.md +45 -0
  172. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
  173. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
  174. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
  175. package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
  176. package/examples/cypress-ecommerce-suite/package.json +13 -0
  177. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
  178. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
  179. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
  180. package/examples/katalon-suite/Profiles/default.glbl +12 -0
  181. package/examples/katalon-suite/README.md +18 -0
  182. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
  183. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
  184. package/examples/katalon-suite/katalon_suite.prj +21 -0
  185. package/examples/playwright-crm-suite/README.md +40 -0
  186. package/examples/playwright-crm-suite/package.json +14 -0
  187. package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
  188. package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
  189. package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
  190. package/examples/playwright-crm-suite/playwright.config.ts +27 -0
  191. package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
  192. package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
  193. package/examples/plugins/sample-custom-plugin/README.md +14 -0
  194. package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
  195. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
  196. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
  197. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
  198. package/examples/practices/README.md +22 -0
  199. package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
  200. package/examples/practices/requirements/crm/requirements_customers.md +462 -0
  201. package/examples/practices/requirements/crm/requirements_login.md +202 -0
  202. package/examples/practices/requirements/crm/requirements_projects.md +396 -0
  203. package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
  204. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
  205. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  206. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
  207. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  208. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  209. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
  210. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
  211. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  212. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  213. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
  214. package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
  215. package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
  216. package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
  217. package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
  218. package/examples/pytest-api-suite/README.md +45 -0
  219. package/examples/pytest-api-suite/conftest.py +31 -0
  220. package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
  221. package/examples/pytest-api-suite/requirements.txt +2 -0
  222. package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
  223. package/examples/selenium-suite/README.md +18 -0
  224. package/examples/selenium-suite/pages/base_page.py +27 -0
  225. package/examples/selenium-suite/pages/login_page.py +29 -0
  226. package/examples/selenium-suite/pages/order_page.py +25 -0
  227. package/examples/selenium-suite/pytest.ini +6 -0
  228. package/examples/selenium-suite/requirements.txt +2 -0
  229. package/examples/selenium-suite/tests/test_login.py +10 -0
  230. package/examples/selenium-suite/tests/test_order.py +10 -0
  231. package/package.json +22 -7
  232. package/scripts/generate_skill_examples.py +244 -0
  233. package/scripts/install.ps1 +112 -0
  234. package/scripts/install_global_plugin.ps1 +94 -0
  235. package/scripts/install_global_plugin.sh +49 -0
  236. package/scripts/lint_skills.ps1 +57 -0
  237. package/scripts/package_dist.py +323 -0
  238. package/scripts/setup.sh +59 -0
  239. package/scripts/update_hub.ps1 +19 -0
  240. package/scripts/update_hub.sh +15 -0
  241. package/skills_manifest.json +209 -6
  242. package/synapse.config.json +30 -0
  243. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
  244. package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
  245. package/templates/ai_bridges/CLAUDE.md +33 -16
  246. package/templates/ai_bridges/copilot-instructions.md +14 -21
  247. package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
  248. package/templates/ai_bridges/windsurfrules +21 -21
  249. package/templates/ci/github-actions-synapse.yml +71 -4
  250. package/templates/welcome.html.tmpl +1674 -0
  251. package/tools/.infer_baseline.json +1102 -0
  252. package/tools/TOOLS_RUNBOOK.md +92 -130
  253. package/tools/__init__.py +26 -0
  254. package/tools/agent_canary.py +501 -501
  255. package/tools/analyze_project.py +31 -17
  256. package/tools/api_fuzzer.py +278 -6
  257. package/tools/author_integrity.py +6 -6
  258. package/tools/auto_heal.py +1 -1
  259. package/tools/browser_helpers.js +105 -7
  260. package/tools/bug_reporter.py +31 -1
  261. package/tools/bump_version.py +23 -3
  262. package/tools/cache_resolver.py +248 -14
  263. package/tools/check_env.py +55 -14
  264. package/tools/check_version_sync.py +153 -0
  265. package/tools/ci_healing_hook.py +1 -1
  266. package/tools/cli.py +263 -13
  267. package/tools/count_tokens.py +1257 -0
  268. package/tools/dashboard_generator.py +2638 -0
  269. package/tools/db_sandbox.py +198 -20
  270. package/tools/e2e_flow_prober.ps1 +12 -64
  271. package/tools/e2e_flow_prober.py +99 -0
  272. package/tools/e2e_flow_prober.sh +4 -0
  273. package/tools/export_postman_collection.py +29 -7
  274. package/tools/flaky_detector.py +4 -3
  275. package/tools/generate_allure_report.ps1 +12 -29
  276. package/tools/generate_allure_report.py +77 -0
  277. package/tools/generate_allure_report.sh +4 -0
  278. package/tools/generate_jmx_plan.py +299 -68
  279. package/tools/generate_test_reports.ps1 +12 -134
  280. package/tools/generate_test_reports.py +133 -0
  281. package/tools/generate_test_reports.sh +4 -0
  282. package/tools/gherkin_suite.py +758 -0
  283. package/tools/git_pre_commit_guard.py +12 -6
  284. package/tools/goldfish_review.py +366 -0
  285. package/tools/heal_pr_automator.py +1 -1
  286. package/tools/heuristic_locator_matcher.py +170 -21
  287. package/tools/infer_linter.py +612 -70
  288. package/tools/jtl_analyzer.py +1 -1
  289. package/tools/lint_ai_docs.py +64 -0
  290. package/tools/matrix_sharder.py +1 -1
  291. package/tools/mcp_excel_server.ps1 +12 -250
  292. package/tools/mcp_excel_server.py +249 -0
  293. package/tools/mcp_excel_server.sh +4 -0
  294. package/tools/mcp_server.py +368 -144
  295. package/tools/md_to_xlsx.py +307 -0
  296. package/tools/micro_mutator.py +300 -69
  297. package/tools/omni_transpiler.py +2 -2
  298. package/tools/output_formatter.py +128 -0
  299. package/tools/pinpoint_failure.py +88 -88
  300. package/tools/project_state.py +2 -2
  301. package/tools/repo_map.py +1 -1
  302. package/tools/safety_firewall.py +197 -33
  303. package/tools/setup_project.py +294 -30
  304. package/tools/skillspector.py +8 -8
  305. package/tools/spec_drift_sentinel.py +439 -0
  306. package/tools/sync_agent_rules.py +248 -0
  307. package/tools/telemetry_correlator.py +45 -0
  308. package/tools/test_impact.py +51 -12
  309. package/tools/tms_sync_bridge.py +21 -5
  310. package/tools/universal_ir.py +3 -0
  311. package/tools/update_sentinel.py +87 -15
  312. package/tools/verify_all.ps1 +13 -0
  313. package/tools/verify_all.py +463 -79
  314. package/tools/visual_diff_canvas.py +1 -0
  315. package/tools/visual_som_helper.py +1 -1
  316. package/tools/watch_mode.py +305 -0
  317. package/tools/welcome_page.py +291 -0
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enterprise Standardized Output Formatter & Exit Code Specification
4
+ ------------------------------------------------------------------
5
+ Provides centralized terminal styling, NO_COLOR standard compliance,
6
+ and deterministic exit codes across all Synapse Hub CLI tools.
7
+
8
+ Exit Code Standards:
9
+ 0: EXIT_SUCCESS (Operation passed, DoD compliant)
10
+ 1: EXIT_FAILURE (Test failure, linter defect, gate rejection)
11
+ 2: EXIT_USAGE_ERROR (Invalid CLI arguments, syntax error)
12
+ 3: EXIT_SECURITY_BLOCK (Destructive action blocked, secret leaked, MIT license altered)
13
+ 4: EXIT_ENVIRONMENT_ERROR (Missing required tool, incompatible runtime)
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import json
19
+ from typing import Any, Dict, Optional
20
+
21
+ # Standard Exit Codes
22
+ EXIT_SUCCESS = 0
23
+ EXIT_FAILURE = 1
24
+ EXIT_USAGE_ERROR = 2
25
+ EXIT_SECURITY_BLOCK = 3
26
+ EXIT_ENVIRONMENT_ERROR = 4
27
+
28
+
29
+ def is_color_enabled() -> bool:
30
+ """
31
+ Determines if ANSI color codes should be rendered.
32
+ Honors https://no-color.org specification (NO_COLOR env var).
33
+ """
34
+ if os.environ.get("NO_COLOR") is not None:
35
+ return False
36
+ if os.environ.get("TERM") == "dumb":
37
+ return False
38
+ if os.environ.get("FORCE_COLOR") in ("1", "true", "TRUE"):
39
+ return True
40
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
41
+
42
+
43
+ class Colors:
44
+ """ANSI color codes with dynamic NO_COLOR enforcement."""
45
+ ESC = "\033"
46
+ CYAN = "\033[96m"
47
+ GREEN = "\033[92m"
48
+ YELLOW = "\033[93m"
49
+ RED = "\033[91m"
50
+ BOLD = "\033[1m"
51
+ DIM = "\033[2m"
52
+ RESET = "\033[0m"
53
+
54
+ @staticmethod
55
+ def _c(code: str) -> str:
56
+ return code if is_color_enabled() else ""
57
+
58
+ @classmethod
59
+ def cyan(cls, text: str) -> str:
60
+ return f"{cls._c(cls.CYAN)}{text}{cls._c(cls.RESET)}"
61
+
62
+ @classmethod
63
+ def green(cls, text: str) -> str:
64
+ return f"{cls._c(cls.GREEN)}{text}{cls._c(cls.RESET)}"
65
+
66
+ @classmethod
67
+ def yellow(cls, text: str) -> str:
68
+ return f"{cls._c(cls.YELLOW)}{text}{cls._c(cls.RESET)}"
69
+
70
+ @classmethod
71
+ def red(cls, text: str) -> str:
72
+ return f"{cls._c(cls.RED)}{text}{cls._c(cls.RESET)}"
73
+
74
+ @classmethod
75
+ def bold(cls, text: str) -> str:
76
+ return f"{cls._c(cls.BOLD)}{text}{cls._c(cls.RESET)}"
77
+
78
+ @classmethod
79
+ def dim(cls, text: str) -> str:
80
+ return f"{cls._c(cls.DIM)}{text}{cls._c(cls.RESET)}"
81
+
82
+
83
+ class OutputFormatter:
84
+ """Standardized console & machine-readable output renderer."""
85
+
86
+ def __init__(self, quiet: bool = False, json_mode: bool = False):
87
+ self.quiet = quiet
88
+ self.json_mode = json_mode
89
+
90
+ def header(self, title: str):
91
+ if not self.quiet and not self.json_mode:
92
+ border = "=" * 70
93
+ print(f"\n{Colors.cyan(border)}")
94
+ print(f" {Colors.bold(title)}")
95
+ print(f"{Colors.cyan(border)}\n")
96
+
97
+ def step(self, current: int, total: int, title: str):
98
+ if not self.quiet and not self.json_mode:
99
+ print(f"{Colors.bold(f'[{current}/{total}]')} {title}")
100
+
101
+ def pass_msg(self, message: str):
102
+ if not self.json_mode:
103
+ print(f"{Colors.green('✅ [PASS]')} {message}")
104
+
105
+ def fail_msg(self, message: str):
106
+ if not self.json_mode:
107
+ print(f"{Colors.red('❌ [FAIL]')} {message}")
108
+
109
+ def warn_msg(self, message: str):
110
+ if not self.quiet and not self.json_mode:
111
+ print(f"{Colors.yellow('⚠️ [WARN]')} {message}")
112
+
113
+ def info_msg(self, message: str):
114
+ if not self.quiet and not self.json_mode:
115
+ print(f"{Colors.cyan('ℹ️ [INFO]')} {message}")
116
+
117
+ def print_data(self, data: Any, format_type: str = "text"):
118
+ if format_type == "json" or self.json_mode:
119
+ if isinstance(data, (dict, list)):
120
+ print(json.dumps(data, indent=2, ensure_ascii=False))
121
+ else:
122
+ print(str(data))
123
+ else:
124
+ print(str(data))
125
+
126
+
127
+ # Global default instance
128
+ console = OutputFormatter()
@@ -1,88 +1,88 @@
1
- #!/usr/bin/env python3
2
- """
3
- Fast Zero-Token Failure Pinpointer for Katalon & Jenkins Reports.
4
- Scans the latest Reports/**/JUnit_Report.xml, extracts failed Test Cases,
5
- exact failed lines in Groovy/Script, and the root-cause exception.
6
- Outputs < 100 tokens.
7
-
8
- Usage:
9
- python3 tools/pinpoint_failure.py
10
- python3 tools/pinpoint_failure.py path/to/JUnit_Report.xml
11
- """
12
- import os
13
- import sys
14
- import glob
15
- import xml.etree.ElementTree as ET
16
- import re
17
-
18
- def find_latest_report():
19
- reports = glob.glob("Reports/**/JUnit_Report.xml", recursive=True)
20
- if not reports:
21
- return None
22
- reports.sort(key=lambda x: os.path.getmtime(x), reverse=True)
23
- return reports[0]
24
-
25
- def parse_failures(xml_path):
26
- if not os.path.exists(xml_path):
27
- print(f"Error: {xml_path} does not exist.")
28
- return []
29
-
30
- tree = ET.parse(xml_path)
31
- root = tree.getroot()
32
- failures = []
33
-
34
- suites = [root] if root.tag == "testsuite" else root.findall("testsuite")
35
-
36
- for suite in suites:
37
- for tc in suite.findall("testcase"):
38
- tc_name = tc.get("name", "Unknown")
39
- tc_time = tc.get("time", "0")
40
- fail = tc.find("failure")
41
- err = tc.find("error")
42
- target = fail if fail is not None else err
43
-
44
- if target is not None:
45
- msg = target.get("message", "")
46
- text = target.text or ""
47
- combined = (msg + "\n" + text).replace("&#xa;", "\n")
48
-
49
- # Extract Root Cause Reason
50
- reason_match = re.search(r"Reason:\s*(.+?)(?=\n\s*at|\Z)", combined, re.DOTALL)
51
- if reason_match:
52
- reason = reason_match.group(1).strip()
53
- else:
54
- lines = [l.strip() for l in combined.split("\n") if l.strip()]
55
- reason = lines[0] if lines else "Unknown Error"
56
-
57
- # Extract Failed Line in Groovy Page / Script
58
- Enterprise_lines = re.findall(r"(?:Enterprise\.pages\.[\w]+|TC-[\w\s\u00C0-\u1EF9]+|Script\d+)\.groovy:\d+|(?:TC-[\w\s\u00C0-\u1EF9]+:\d+)", combined)
59
-
60
- failures.append({
61
- "tc": tc_name.split("/")[-1],
62
- "time": f"{float(tc_time):.1f}s",
63
- "reason": reason.split("\n")[0][:180],
64
- "failed_at": Enterprise_lines[:2] if Enterprise_lines else ["See JUnit report"]
65
- })
66
-
67
- return failures
68
-
69
- def main():
70
- xml_path = sys.argv[1] if len(sys.argv) > 1 else find_latest_report()
71
- if not xml_path:
72
- print("No JUnit_Report.xml found in Reports/.")
73
- sys.exit(0)
74
-
75
- print(f"=== Report: {xml_path} ===")
76
- failures = parse_failures(xml_path)
77
- if not failures:
78
- print("✅ 100% PASSED: Zero failures found in report.")
79
- return
80
-
81
- print(f"❌ Found {len(failures)} Failed Test Case(s):")
82
- for i, f in enumerate(failures, 1):
83
- print(f"[{i}] {f['tc']} ({f['time']})")
84
- print(f" Cause: {f['reason']}")
85
- print(f" Line: {', '.join(f['failed_at'])}")
86
-
87
- if __name__ == "__main__":
88
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fast Zero-Token Failure Pinpointer for Katalon & Jenkins Reports.
4
+ Scans the latest Reports/**/JUnit_Report.xml, extracts failed Test Cases,
5
+ exact failed lines in Groovy/Script, and the root-cause exception.
6
+ Outputs < 100 tokens.
7
+
8
+ Usage:
9
+ python3 tools/pinpoint_failure.py
10
+ python3 tools/pinpoint_failure.py path/to/JUnit_Report.xml
11
+ """
12
+ import os
13
+ import sys
14
+ import glob
15
+ import xml.etree.ElementTree as ET
16
+ import re
17
+
18
+ def find_latest_report():
19
+ reports = glob.glob("Reports/**/JUnit_Report.xml", recursive=True)
20
+ if not reports:
21
+ return None
22
+ reports.sort(key=lambda x: os.path.getmtime(x), reverse=True)
23
+ return reports[0]
24
+
25
+ def parse_failures(xml_path):
26
+ if not os.path.exists(xml_path):
27
+ print(f"Error: {xml_path} does not exist.")
28
+ return []
29
+
30
+ tree = ET.parse(xml_path)
31
+ root = tree.getroot()
32
+ failures = []
33
+
34
+ suites = [root] if root.tag == "testsuite" else root.findall("testsuite")
35
+
36
+ for suite in suites:
37
+ for tc in suite.findall("testcase"):
38
+ tc_name = tc.get("name", "Unknown")
39
+ tc_time = tc.get("time", "0")
40
+ fail = tc.find("failure")
41
+ err = tc.find("error")
42
+ target = fail if fail is not None else err
43
+
44
+ if target is not None:
45
+ msg = target.get("message", "")
46
+ text = target.text or ""
47
+ combined = (msg + "\n" + text).replace("&#xa;", "\n")
48
+
49
+ # Extract Root Cause Reason
50
+ reason_match = re.search(r"Reason:\s*(.+?)(?=\n\s*at|\Z)", combined, re.DOTALL)
51
+ if reason_match:
52
+ reason = reason_match.group(1).strip()
53
+ else:
54
+ lines = [l.strip() for l in combined.split("\n") if l.strip()]
55
+ reason = lines[0] if lines else "Unknown Error"
56
+
57
+ # Extract Failed Line in Groovy Page / Script
58
+ Enterprise_lines = re.findall(r"(?:Enterprise\.pages\.[\w]+|TC-[\w\s\u00C0-\u1EF9]+|Script\d+)\.groovy:\d+|(?:TC-[\w\s\u00C0-\u1EF9]+:\d+)", combined)
59
+
60
+ failures.append({
61
+ "tc": tc_name.split("/")[-1],
62
+ "time": f"{float(tc_time):.1f}s",
63
+ "reason": reason.split("\n")[0][:180],
64
+ "failed_at": Enterprise_lines[:2] if Enterprise_lines else ["See JUnit report"]
65
+ })
66
+
67
+ return failures
68
+
69
+ def main():
70
+ xml_path = sys.argv[1] if len(sys.argv) > 1 else find_latest_report()
71
+ if not xml_path:
72
+ print("No JUnit_Report.xml found in Reports/.")
73
+ sys.exit(0)
74
+
75
+ print(f"=== Report: {xml_path} ===")
76
+ failures = parse_failures(xml_path)
77
+ if not failures:
78
+ print("✅ 100% PASSED: Zero failures found in report.")
79
+ return
80
+
81
+ print(f"❌ Found {len(failures)} Failed Test Case(s):")
82
+ for i, f in enumerate(failures, 1):
83
+ print(f"[{i}] {f['tc']} ({f['time']})")
84
+ print(f" Cause: {f['reason']}")
85
+ print(f" Line: {', '.join(f['failed_at'])}")
86
+
87
+ if __name__ == "__main__":
88
+ main()
@@ -56,7 +56,7 @@ def init_state(target_dir, goal="Initial Autonomous Setup"):
56
56
  - Failing Locator / Line: `[None]`
57
57
 
58
58
  ## 4. Quality Verification
59
- - 7 Quality Gates: `READY`
59
+ - 9 Quality Gates: `READY`
60
60
  - Clean POM Integrity: `VERIFIED`
61
61
  """
62
62
  with open(state_file, "w", encoding="utf-8") as f:
@@ -146,7 +146,7 @@ def update_state(target_dir, goal=None, completed_task=None, pending_task=None,
146
146
  {watch_block}
147
147
 
148
148
  ## 4. Quality Verification
149
- - 7 Quality Gates: `{current_gate}`
149
+ - 9 Quality Gates: `{current_gate}`
150
150
  - Clean POM Integrity: `VERIFIED`
151
151
  """
152
152
  with open(state_file, "w", encoding="utf-8") as f:
package/tools/repo_map.py CHANGED
@@ -221,7 +221,7 @@ def build_repo_map(target_repo=None, output_txt=None, output_json=None):
221
221
 
222
222
  rendered_text = "\n".join(out_lines)
223
223
  compressed_bytes = len(rendered_text.encode("utf-8"))
224
- ratio = (1.0 - (compressed_bytes / max(1, total_raw_bytes))) * 100
224
+ ratio = (1.0 - (compressed_bytes / total_raw_bytes)) * 100 if total_raw_bytes > 0 else 0.0
225
225
  est_tokens = len(rendered_text) // 4
226
226
 
227
227
  # 1. Save to primary destination (tools/repo_map.txt in Hub)
@@ -29,27 +29,52 @@ if hasattr(sys.stdout, 'reconfigure'):
29
29
 
30
30
  DANGEROUS_ACTION_KEYWORDS = [
31
31
  r"\bdelete\b",
32
- r"\bremove\b",
33
32
  r"\bpurge\b",
34
33
  r"\bwipe\b",
35
34
  r"\bdrop\b",
36
35
  r"\bpay\b",
37
36
  r"\bpurchase\b",
38
- r"\bcheckout\b",
39
- r"\bterminate\b",
37
+ r"\bplace\s+order\b",
38
+ r"\bconfirm\s+(?:payment|order|delete|deletion|purge|wipe|destroy)\b",
39
+ r"\bsubmit\s+order\b",
40
+ r"\barchive\s+(?:all|data|system|database|account)\b",
41
+ r"\brevoke\s+access\b",
42
+ r"\bcancel\s+subscription\b",
43
+ r"\berase\s+all\s+data\b",
44
+ r"\bterminate(?:\s+account)?\b",
40
45
  r"\bdestroy\b",
41
- r"\breset\b",
42
- r"\btransfer\b",
43
- r"\bformat\b",
46
+ r"\bformat\s+(?:disk|drive|system|database|table|partition)\b",
47
+ # Multilingual & Vietnamese destructive keywords with strict word boundaries
48
+ r"(?<!\w)x[oóòỏõọ][aáàảãạ](?!\w)",
49
+ r"(?<!\w)x[oóòỏõọ]a\s+t[aáàảãạ]t\s+c[aảáàãạ](?!\w)",
50
+ r"(?<!\w)h[uủúùũụ]y\s+b[oỏóòõọ](?!\w)",
51
+ r"(?<!\w)h[uủúùũụ]y\s+t[aàáảãạ]i\s+kho[aảáàãạ]n(?!\w)",
52
+ r"(?<!\w)thanh\s+to[aáàảãạ]n(?!\w)",
53
+ r"(?<!\w)r[uúùủũụ]t\s+ti[eềéèẻẽẹ]n(?!\w)",
54
+ r"(?<!\w)n[aạáàảã]p\s+ti[eềéèẻẽẹ]n(?!\w)",
44
55
  ]
45
56
 
57
+ # Explicit whitelist of benign user-flow patterns that require exact fullmatch to authorize
58
+ BENIGN_ACTIONS_WHITELIST = [
59
+ re.compile(r"(?i)^\s*(?:checkout|proceed\s+to\s+checkout)\s*$"),
60
+ re.compile(r"(?i)^\s*(?:reset\s+password)\s*$"),
61
+ re.compile(r"(?i)^\s*(?:remove\s+from\s+cart)\s*$"),
62
+ re.compile(r"(?i)^\s*(?:xoay\s+[aảáàãạ]nh)\s*$"),
63
+ ]
64
+
65
+ def _split_words_and_camel(s: str) -> str:
66
+ """Expands camelCase and PascalCase into separated words for keyword inspection."""
67
+ return re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', s)
68
+
46
69
  DANGEROUS_SELECTOR_PATTERNS = [
47
- r"\[data-action\*=['\"]?delete['\"]?\]",
48
- r"\[data-action\*=['\"]?pay['\"]?\]",
70
+ r"\[data-action\*=['\"]?(?:delete|pay|confirm)['\"]?\]",
49
71
  r"\.btn-danger\b",
50
72
  r"\.p-button-danger\b",
51
- r"\[aria-label\*=['\"]?delete['\"]?\]",
52
- r"button[#\.\-_a-zA-Z0-9]*[_\-]*(?:del|delete|purge|remove|wipe|drop)",
73
+ r"#(?:btn-confirm|btn-delete|btn-purge)\b",
74
+ r"\[aria-label\*=['\"]?(?:delete|pay|confirm)['\"]?\]",
75
+ r"button[#\.\-_a-zA-Z0-9]*[_\-]*(?:del|delete|purge|wipe|drop|confirm|archive)",
76
+ r":(?:has-text|text)\s*\(\s*(['\"])[^'\"]*(?:delete|wipe|purge|pay|confirm|archive|x[oóòỏõọ][aáàảãạ]|h[uủúùũụ]y\s+b[oỏóòõọ]|thanh\s*to[aáàảãạ]n)[^'\"]*\1\s*\)",
77
+ r"text=(['\"])[^'\"]*(?:delete|wipe|purge|pay|confirm|archive|x[oóòỏõọ][aáàảãạ]|h[uủúùũụ]y\s+b[oỏóòõọ]|thanh\s*to[aáàảãạ]n)\1",
53
78
  ]
54
79
 
55
80
  COMPILED_TEXT_REGEX = re.compile("|".join(DANGEROUS_ACTION_KEYWORDS), re.IGNORECASE)
@@ -57,16 +82,20 @@ COMPILED_SEL_REGEX = re.compile("|".join(DANGEROUS_SELECTOR_PATTERNS), re.IGNORE
57
82
 
58
83
  # Pattern to extract interaction statements from Playwright / Selenium / Groovy
59
84
  INTERACTION_CODE_PATTERNS = [
60
- # page.click('selector'), page.fill('selector', 'val')
61
- re.compile(r"""(?:page|frame)\.(?P<act>click|dblclick|fill|type|press|check)\s*\(\s*['"](?P<sel>[^'"]+)['"](?:\s*,\s*['"](?P<text>[^'"]*)['"])?""", re.IGNORECASE),
62
- # page.locator('selector').click()
63
- re.compile(r"""(?:page|frame)\.locator\s*\(\s*['"](?P<sel>[^'"]+)['"]\s*\)\.(?P<act>click|fill|press)""", re.IGNORECASE),
85
+ # page.click('selector'), page.fill('selector', 'val') - supports nested quotes
86
+ re.compile(r"""(?:page|frame)\.(?P<act>click|dblclick|fill|type|press|check)\s*\(\s*(?P<q>['"])(?P<sel>(?:(?!(?P=q)|\\).|\\.)*)(?P=q)(?:\s*,\s*['"](?P<text>[^'"]*)['"])?""", re.IGNORECASE),
87
+ # page.locator('selector').click() or page.locator(VAR).click()
88
+ re.compile(r"""(?:page|frame)\.locator\s*\(\s*(?:(?P<q>['"])(?P<sel>(?:(?!(?P=q)|\\).|\\.)*)(?P=q)|(?P<var_arg>[a-zA-Z0-9_]+))\s*\)\.(?P<act>click|fill|press)""", re.IGNORECASE),
64
89
  # page.getByText('text').click()
65
- re.compile(r"""(?:page|frame)\.getByText\s*\(\s*['"](?P<text>[^'"]+)['"]\s*\)\.(?P<act>click)""", re.IGNORECASE),
90
+ re.compile(r"""(?:page|frame)\.getByText\s*\(\s*(?P<q>['"])(?P<text>(?:(?!(?P=q)|\\).|\\.)*)(?P=q)\s*\)\.(?P<act>click)""", re.IGNORECASE),
66
91
  # page.getByRole('button', { name: 'Delete' }).click()
67
92
  re.compile(r"""(?:page|frame)\.getByRole\s*\(\s*['"](?P<role>[^'"]+)['"]\s*,\s*\{[^}]*name:\s*['"](?P<text>[^'"]+)['"]""", re.IGNORECASE),
68
93
  # WebUI.click(findTestObject(...)) or WebUI.clickOffset
69
94
  re.compile(r"""WebUI\.(?P<act>click|setText)\s*\([^)]*['"](?P<sel>[^'"]*)['"]""", re.IGNORECASE),
95
+ # Variable locator call: e.g. deleteBtn.click(), btn.click()
96
+ re.compile(r"""\b(?!(?:page|frame|WebUI)\b)(?P<var>[a-zA-Z0-9_]+)\.(?P<act>click|dblclick|fill|press|tap)\s*\(""", re.IGNORECASE),
97
+ # Clean POM method invocation: e.g. customerPage.deleteAllCustomers(), accountPO.purgeAccount(userId)
98
+ re.compile(r"""\b(?!(?:page|frame|WebUI)\b)(?P<page>[a-zA-Z0-9_]*(?:Page|PO|View|Service|Component|[a-z]))\.(?P<act>[a-zA-Z0-9_]+)\s*\((?P<args>[^)]*)\)""", re.IGNORECASE),
70
99
  ]
71
100
 
72
101
 
@@ -77,19 +106,36 @@ def is_action_dangerous(action_type: str, text: str = "", selector: str = "") ->
77
106
  """
78
107
  text_clean = text.strip() if text else ""
79
108
  sel_clean = selector.strip() if selector else ""
109
+ act_clean = action_type.strip() if action_type else "click"
80
110
 
81
- # Check text content
82
- if text_clean and COMPILED_TEXT_REGEX.search(text_clean):
83
- match = COMPILED_TEXT_REGEX.search(text_clean).group(0)
84
- return True, f"Element text contains dangerous keyword: '{match}'"
85
-
86
- # Check selector
87
- if sel_clean and COMPILED_SEL_REGEX.search(sel_clean):
88
- return True, f"Element selector matches destructive pattern: '{sel_clean}'"
89
-
90
- # Specific dangerous action types
91
- if action_type.lower() in ("drop_database", "wipe_environment", "execute_payment"):
111
+ # 1. Action type verification
112
+ act_expanded = _split_words_and_camel(act_clean)
113
+ if act_clean.lower() in ("drop_database", "wipe_environment", "execute_payment"):
92
114
  return True, f"Action type '{action_type}' is classified as irreversible"
115
+ match_act = COMPILED_TEXT_REGEX.search(act_clean) or COMPILED_TEXT_REGEX.search(act_expanded)
116
+ if match_act:
117
+ return True, f"Action method contains dangerous verb: '{match_act.group(0)}'"
118
+
119
+ # 2. Check text (Dangerous check runs FIRST; benign whitelist requires strict fullmatch)
120
+ if text_clean:
121
+ text_expanded = _split_words_and_camel(text_clean)
122
+ match = COMPILED_TEXT_REGEX.search(text_clean) or COMPILED_TEXT_REGEX.search(text_expanded)
123
+ if match:
124
+ # Check strict benign whitelist fullmatch before flagging
125
+ if any(p.fullmatch(text_clean) for p in BENIGN_ACTIONS_WHITELIST):
126
+ pass
127
+ else:
128
+ return True, f"Element text contains dangerous keyword: '{match.group(0)}'"
129
+
130
+ # 3. Check selector
131
+ if sel_clean:
132
+ if not any(p.fullmatch(sel_clean) for p in BENIGN_ACTIONS_WHITELIST):
133
+ if COMPILED_SEL_REGEX.search(sel_clean):
134
+ return True, f"Element selector matches destructive pattern: '{sel_clean}'"
135
+ sel_expanded = _split_words_and_camel(sel_clean)
136
+ match = COMPILED_TEXT_REGEX.search(sel_clean) or COMPILED_TEXT_REGEX.search(sel_expanded)
137
+ if match:
138
+ return True, f"Element selector contains dangerous keyword: '{match.group(0)}' in '{sel_clean}'"
93
139
 
94
140
  return False, "Action passed safety firewall inspection"
95
141
 
@@ -117,38 +163,132 @@ def inspect_and_authorize(action_type: str, text: str = "", selector: str = "",
117
163
  def inspect_code_snippet(code: str, force: bool = False) -> List[Dict[str, Any]]:
118
164
  """
119
165
  Parses a code snippet or script and checks all discovered UI interaction statements.
166
+ Tracks variable locator assignments across lines to detect chained and indirect invocations.
120
167
  """
121
168
  inspections = []
122
169
  lines = code.splitlines()
170
+ tracked_vars: Dict[str, str] = {}
171
+
172
+ var_assign_re = re.compile(
173
+ r"""(?:const|let|var|def|\$)?\s*(?P<var>[a-zA-Z0-9_]+)\s*=\s*(?:(?:page|frame)\.(?:locator|getByTestId|getByText)\s*\(\s*(?:(?P<q1>['"])(?P<val1>(?:(?!(?P=q1)|\\).|\\.)*)(?P=q1)|(?P<arg_var>[a-zA-Z0-9_]+))\s*\)|(?:page|frame)\.getByRole\s*\(\s*['"][^'"]+['"]\s*,\s*\{[^}]*name:\s*(?P<q_role>['"])(?P<val_role>(?:(?!(?P=q_role)|\\).|\\.)*)(?P=q_role)|(?P<q2>['"])(?P<val2>(?:(?!(?P=q2)|\\).|\\.)*)(?P=q2))""",
174
+ re.IGNORECASE
175
+ )
123
176
 
124
177
  for line_idx, line in enumerate(lines, start=1):
125
178
  stripped = line.strip()
126
179
  if not stripped or stripped.startswith(("//", "#", "/*", "*")):
127
180
  continue
128
181
 
182
+ # Track variable definitions
183
+ m_assign = var_assign_re.search(stripped)
184
+ if m_assign:
185
+ var_name = m_assign.group("var")
186
+ val = m_assign.group("val1") or m_assign.group("val2") or m_assign.group("val_role") or ""
187
+ if not val and m_assign.group("arg_var"):
188
+ arg_v = m_assign.group("arg_var")
189
+ val = tracked_vars.get(arg_v, arg_v)
190
+ # Recursively unwrap aliases
191
+ while val in tracked_vars:
192
+ val = tracked_vars[val]
193
+ if var_name and val:
194
+ tracked_vars[var_name] = val
195
+
129
196
  matched = False
130
197
  for pattern in INTERACTION_CODE_PATTERNS:
198
+ found_any = False
131
199
  for match in pattern.finditer(line):
132
- matched = True
133
200
  groups = match.groupdict()
134
201
  act = groups.get("act") or "click"
135
202
  sel = groups.get("sel") or ""
136
203
  text = groups.get("text") or ""
204
+ var_arg = groups.get("var_arg")
205
+ var_name = groups.get("var")
206
+
207
+ if var_arg:
208
+ sel = tracked_vars.get(var_arg, var_arg)
209
+ elif not sel and groups.get("page"):
210
+ sel = f"{groups.get('page')}.{act}"
211
+ elif not sel and var_name:
212
+ sel = tracked_vars.get(var_name, var_name)
213
+
214
+ # Recursively unwrap selector if it matches an alias
215
+ while sel in tracked_vars:
216
+ sel = tracked_vars[sel]
217
+
137
218
  auth = inspect_and_authorize(act, text=text, selector=sel, force=force)
138
219
  auth["line"] = line_idx
139
220
  auth["code"] = stripped
140
221
  inspections.append(auth)
222
+ found_any = True
223
+ if found_any:
224
+ matched = True
225
+ break
141
226
 
142
- # Fallback: line itself contains dangerous keyword and an action verb
143
- if not matched and COMPILED_TEXT_REGEX.search(stripped) and any(w in stripped.lower() for w in ("click", "press", "submit", "post", "delete")):
144
- auth = inspect_and_authorize("dispatch", text=stripped, selector="", force=force)
145
- auth["line"] = line_idx
146
- auth["code"] = stripped
147
- inspections.append(auth)
227
+ # Fallback: line itself contains dangerous keyword and an action verb (ignore pure assignments)
228
+ if not matched and not m_assign:
229
+ line_expanded = _split_words_and_camel(stripped)
230
+ if (COMPILED_TEXT_REGEX.search(stripped) or COMPILED_TEXT_REGEX.search(line_expanded)) and any(w in stripped.lower() for w in ("click", "press", "submit", "post", "delete", "purge", "wipe", "drop", "pay")):
231
+ auth = inspect_and_authorize("dispatch", text=stripped, selector="", force=force)
232
+ auth["line"] = line_idx
233
+ auth["code"] = stripped
234
+ inspections.append(auth)
148
235
 
149
236
  return inspections
150
237
 
151
238
 
239
+ def detect_action_loop(action_history: List[str], repeat_threshold: int = 4) -> Dict[str, Any]:
240
+ """
241
+ Detects infinite action loops and repeating cycle patterns among agent tool calls.
242
+ Inspired by agentblazo/blazo deterministic detection rules.
243
+
244
+ Checks:
245
+ 1. Consecutive identical actions (>= repeat_threshold)
246
+ 2. 2-action repeating cycle: [A, B, A, B]
247
+ 3. 3-action repeating cycle: [A, B, C, A, B, C]
248
+ """
249
+ if not action_history or len(action_history) < 2:
250
+ return {"loop_detected": False, "reason": "History too short", "severity": "none", "pattern": []}
251
+
252
+ names = [a.strip() for a in action_history if a.strip()]
253
+
254
+ # 1. Check consecutive identical repeats
255
+ i = 0
256
+ while i < len(names):
257
+ name = names[i]
258
+ end = i + 1
259
+ while end < len(names) and names[end] == name:
260
+ end += 1
261
+ length = end - i
262
+ if length >= repeat_threshold:
263
+ return {
264
+ "loop_detected": True,
265
+ "type": "consecutive_repeat",
266
+ "severity": "critical",
267
+ "message": f"Possible loop: action '{name}' repeated {length} times in a row",
268
+ "pattern": [name] * length
269
+ }
270
+ i = end
271
+
272
+ # 2. Check 2-action and 3-action trailing cycles
273
+ for block in (2, 3):
274
+ if len(names) < block * 2:
275
+ continue
276
+ start = len(names) - block * 2
277
+ left_block = names[start : start + block]
278
+ right_block = names[start + block : start + block * 2]
279
+ if left_block == right_block and len(set(left_block)) >= 2:
280
+ pattern_str = " -> ".join(left_block)
281
+ return {
282
+ "loop_detected": True,
283
+ "type": f"cycle_{block}",
284
+ "severity": "critical",
285
+ "message": f"Possible loop: cycle pattern [{pattern_str}] repeated",
286
+ "pattern": left_block
287
+ }
288
+
289
+ return {"loop_detected": False, "reason": "No loop patterns detected", "severity": "none", "pattern": []}
290
+
291
+
152
292
  def main():
153
293
  parser = argparse.ArgumentParser(description="Action Safety Firewall & Blast Radius Containment Engine")
154
294
  parser.add_argument("--action", default="click", help="Action type (click, fill, submit)")
@@ -160,8 +300,32 @@ def main():
160
300
  parser.add_argument("--dry-run", action="store_true", help="Execute in pre-execution dry-run mode (outputs validation report)")
161
301
  parser.add_argument("--force", action="store_true", help="Force bypass safety firewall")
162
302
  parser.add_argument("--json", action="store_true", help="Output results in JSON format")
303
+ parser.add_argument("--history", default="", help="Comma-separated or JSON list of historical action names to check for infinite loops")
304
+ parser.add_argument("--check-loop", action="store_true", help="Explicitly audit action history for cycle/infinite loops")
163
305
  args = parser.parse_args()
164
306
 
307
+ # Loop check mode (Blazo-inspired)
308
+ if args.history or args.check_loop:
309
+ hist_items = []
310
+ if args.history:
311
+ if args.history.strip().startswith("[") and args.history.strip().endswith("]"):
312
+ try:
313
+ hist_items = json.loads(args.history)
314
+ except Exception:
315
+ hist_items = [h.strip() for h in args.history.split(",") if h.strip()]
316
+ else:
317
+ hist_items = [h.strip() for h in args.history.split(",") if h.strip()]
318
+
319
+ loop_res = detect_action_loop(hist_items)
320
+ if args.json:
321
+ print(json.dumps(loop_res, indent=2))
322
+ else:
323
+ if loop_res["loop_detected"]:
324
+ print(f"❌ [LOOP_DETECTED] {loop_res['message']} (Severity: {loop_res['severity'].upper()})")
325
+ else:
326
+ print("✅ [LOOP_CLEAR] No infinite loop or cycle pattern detected in action history.")
327
+ return 1 if loop_res["loop_detected"] else 0
328
+
165
329
  # Determine input source: explicit stdin flag/arg, --file, --code, or parameters
166
330
  code_content = ""
167
331
  if args.code and args.code != "-":