0xray 2.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.
Files changed (739) hide show
  1. package/.opencode/agents/api-design.yml +31 -0
  2. package/.opencode/agents/architect.yml +15 -0
  3. package/.opencode/agents/architecture-patterns.yml +14 -0
  4. package/.opencode/agents/auto-format.yml +14 -0
  5. package/.opencode/agents/backend-engineer.yml +12 -0
  6. package/.opencode/agents/boot-orchestrator.yml +14 -0
  7. package/.opencode/agents/bug-triage-specialist.yml +15 -0
  8. package/.opencode/agents/code-analyzer.yml +12 -0
  9. package/.opencode/agents/code-reviewer.yml +14 -0
  10. package/.opencode/agents/content-creator.yml +12 -0
  11. package/.opencode/agents/database-engineer.yml +12 -0
  12. package/.opencode/agents/devops-engineer.yml +12 -0
  13. package/.opencode/agents/framework-compliance-audit.yml +14 -0
  14. package/.opencode/agents/frontend-engineer.yml +12 -0
  15. package/.opencode/agents/frontend-ui-ux-engineer.yml +12 -0
  16. package/.opencode/agents/git-workflow.yml +15 -0
  17. package/.opencode/agents/growth-strategist.yml +12 -0
  18. package/.opencode/agents/hermes-agent.yml +12 -0
  19. package/.opencode/agents/inference-improve.yml +12 -0
  20. package/.opencode/agents/lint.yml +14 -0
  21. package/.opencode/agents/log-monitor.yml +12 -0
  22. package/.opencode/agents/mobile-developer.yml +12 -0
  23. package/.opencode/agents/model-health-check.yml +12 -0
  24. package/.opencode/agents/multimodal-looker.yml +12 -0
  25. package/.opencode/agents/performance-analysis.yml +12 -0
  26. package/.opencode/agents/performance-engineer.yml +12 -0
  27. package/.opencode/agents/performance-optimization.yml +12 -0
  28. package/.opencode/agents/processor-pipeline.yml +14 -0
  29. package/.opencode/agents/project-analysis.yml +12 -0
  30. package/.opencode/agents/refactorer.yml +12 -0
  31. package/.opencode/agents/researcher.yml +12 -0
  32. package/.opencode/agents/security-auditor.yml +12 -0
  33. package/.opencode/agents/security-scan.yml +12 -0
  34. package/.opencode/agents/seo-consultant.yml +12 -0
  35. package/.opencode/agents/session-management.yml +12 -0
  36. package/.opencode/agents/state-manager.yml +12 -0
  37. package/.opencode/agents/storyteller.yml +12 -0
  38. package/.opencode/agents/strategist.yml +12 -0
  39. package/.opencode/agents/tech-writer.yml +12 -0
  40. package/.opencode/agents/testing-best-practices.yml +12 -0
  41. package/.opencode/agents/testing-lead.yml +12 -0
  42. package/.opencode/agents/ui-ux-design.yml +12 -0
  43. package/.opencode/codex.codex +8 -0
  44. package/.opencode/commands/auto-format.md +99 -0
  45. package/.opencode/commands/auto-summary-capture.md +90 -0
  46. package/.opencode/commands/dependency-audit.md +184 -0
  47. package/.opencode/commands/enforcer-daily-scan.md +137 -0
  48. package/.opencode/commands/framework-compliance-audit.md +205 -0
  49. package/.opencode/commands/interactive-validator.md +75 -0
  50. package/.opencode/commands/job-summary-logger.md +68 -0
  51. package/.opencode/commands/lint.md +11 -0
  52. package/.opencode/commands/mode-switch.md +95 -0
  53. package/.opencode/commands/model-health-check.md +186 -0
  54. package/.opencode/commands/performance-analysis.md +144 -0
  55. package/.opencode/commands/pre-commit-introspection.md +185 -0
  56. package/.opencode/commands/pre-commit-introspection.sh +133 -0
  57. package/.opencode/commands/security-scan.md +157 -0
  58. package/.opencode/commands/sisyphus-validation.md +128 -0
  59. package/.opencode/commands/summary-logger.md +83 -0
  60. package/.opencode/enforcer-config.json +285 -0
  61. package/.opencode/hooks/hook-metrics.json +380 -0
  62. package/.opencode/hooks/post-commit +114 -0
  63. package/.opencode/hooks/post-push +34 -0
  64. package/.opencode/init.sh +151 -0
  65. package/.opencode/skills/api-design/SKILL.md +37 -0
  66. package/.opencode/skills/architect-tools/SKILL.md +37 -0
  67. package/.opencode/skills/architecture-patterns/SKILL.md +37 -0
  68. package/.opencode/skills/auto-format/SKILL.md +37 -0
  69. package/.opencode/skills/backend-engineer/SKILL.md +49 -0
  70. package/.opencode/skills/boot-orchestrator/SKILL.md +37 -0
  71. package/.opencode/skills/bug-triage/SKILL.md +43 -0
  72. package/.opencode/skills/code-analyzer/SKILL.md +45 -0
  73. package/.opencode/skills/code-review/SKILL.md +52 -0
  74. package/.opencode/skills/content-creator/SKILL.md +38 -0
  75. package/.opencode/skills/database-engineer/SKILL.md +46 -0
  76. package/.opencode/skills/devops-engineer/SKILL.md +49 -0
  77. package/.opencode/skills/enforcer/SKILL.md +37 -0
  78. package/.opencode/skills/framework-compliance-audit/SKILL.md +37 -0
  79. package/.opencode/skills/frontend-engineer/SKILL.md +49 -0
  80. package/.opencode/skills/frontend-ui-ux-engineer/SKILL.md +41 -0
  81. package/.opencode/skills/git-workflow/SKILL.md +37 -0
  82. package/.opencode/skills/growth-strategist/SKILL.md +48 -0
  83. package/.opencode/skills/hermes-agent/SKILL.md +212 -0
  84. package/.opencode/skills/inference-improve/SKILL.md +97 -0
  85. package/.opencode/skills/lint/SKILL.md +37 -0
  86. package/.opencode/skills/log-monitor/SKILL.md +44 -0
  87. package/.opencode/skills/mobile-developer/SKILL.md +42 -0
  88. package/.opencode/skills/model-health-check/SKILL.md +37 -0
  89. package/.opencode/skills/multimodal-looker/SKILL.md +45 -0
  90. package/.opencode/skills/orchestrator/SKILL.md +37 -0
  91. package/.opencode/skills/performance-analysis/SKILL.md +37 -0
  92. package/.opencode/skills/performance-engineer/SKILL.md +41 -0
  93. package/.opencode/skills/performance-optimization/SKILL.md +37 -0
  94. package/.opencode/skills/processor-pipeline/SKILL.md +37 -0
  95. package/.opencode/skills/project-analysis/SKILL.md +42 -0
  96. package/.opencode/skills/refactoring-strategies/SKILL.md +37 -0
  97. package/.opencode/skills/registry.json +66 -0
  98. package/.opencode/skills/researcher/SKILL.md +37 -0
  99. package/.opencode/skills/security-audit/SKILL.md +47 -0
  100. package/.opencode/skills/security-scan/SKILL.md +37 -0
  101. package/.opencode/skills/seo-consultant/SKILL.md +43 -0
  102. package/.opencode/skills/session-management/SKILL.md +36 -0
  103. package/.opencode/skills/state-manager/SKILL.md +37 -0
  104. package/.opencode/skills/storyteller/SKILL.md +130 -0
  105. package/.opencode/skills/strategist/SKILL.md +32 -0
  106. package/.opencode/skills/tech-writer/SKILL.md +37 -0
  107. package/.opencode/skills/testing-best-practices/SKILL.md +37 -0
  108. package/.opencode/skills/testing-strategy/SKILL.md +43 -0
  109. package/.opencode/skills/ui-ux-design/SKILL.md +603 -0
  110. package/.opencode/workflows/post-deployment-audit.yml +123 -0
  111. package/AGENTS.md +110 -0
  112. package/LICENSE +21 -0
  113. package/README.md +131 -0
  114. package/dist/AGENTS.md +110 -0
  115. package/dist/CHANGELOG.md +2182 -0
  116. package/dist/LICENSE +21 -0
  117. package/dist/README.md +131 -0
  118. package/dist/agents/architect.js +56 -0
  119. package/dist/agents/backend-engineer.js +81 -0
  120. package/dist/agents/bug-triage-specialist.js +74 -0
  121. package/dist/agents/code-analyzer.js +150 -0
  122. package/dist/agents/code-reviewer.js +69 -0
  123. package/dist/agents/content-creator.js +72 -0
  124. package/dist/agents/database-engineer.js +76 -0
  125. package/dist/agents/devops-engineer.js +84 -0
  126. package/dist/agents/frontend-engineer.js +78 -0
  127. package/dist/agents/frontend-ui-ux-engineer.js +64 -0
  128. package/dist/agents/growth-strategist.js +111 -0
  129. package/dist/agents/index.js +45 -0
  130. package/dist/agents/librarian-agents-updater.js +333 -0
  131. package/dist/agents/log-monitor.js +109 -0
  132. package/dist/agents/mobile-developer.js +102 -0
  133. package/dist/agents/multimodal-looker.js +93 -0
  134. package/dist/agents/performance-engineer.js +86 -0
  135. package/dist/agents/refactorer.js +80 -0
  136. package/dist/agents/registry.js +340 -0
  137. package/dist/agents/researcher.js +83 -0
  138. package/dist/agents/security-auditor.js +158 -0
  139. package/dist/agents/seo-consultant.js +53 -0
  140. package/dist/agents/strategist.js +47 -0
  141. package/dist/agents/tech-writer.js +84 -0
  142. package/dist/agents/testing-lead.js +95 -0
  143. package/dist/agents/types.js +1 -0
  144. package/dist/analytics/consent-manager.js +258 -0
  145. package/dist/analytics/emerging-pattern-detector.js +260 -0
  146. package/dist/analytics/pattern-learning-engine.js +278 -0
  147. package/dist/analytics/pattern-performance-tracker.js +336 -0
  148. package/dist/analytics/predictive-analytics.js +248 -0
  149. package/dist/analytics/prompt-pattern-analyzer.js +371 -0
  150. package/dist/analytics/routing-performance-analyzer.js +356 -0
  151. package/dist/analytics/routing-refiner.js +380 -0
  152. package/dist/analytics/simple-pattern-analyzer.js +297 -0
  153. package/dist/architect/architect-tools.js +437 -0
  154. package/dist/architect/architectural-integrity.js +78 -0
  155. package/dist/benchmark/performance-benchmark.js +372 -0
  156. package/dist/cli/commands/analytics-disable.js +72 -0
  157. package/dist/cli/commands/analytics-enable-action.js +82 -0
  158. package/dist/cli/commands/analytics-preview.js +106 -0
  159. package/dist/cli/commands/analytics-status.js +68 -0
  160. package/dist/cli/commands/antigravity-status.js +106 -0
  161. package/dist/cli/commands/archive-logs.js +161 -0
  162. package/dist/cli/commands/credible-init.js +82 -0
  163. package/dist/cli/commands/grok-install.js +16 -0
  164. package/dist/cli/commands/hermes-install.js +66 -0
  165. package/dist/cli/commands/mcp-install.js +253 -0
  166. package/dist/cli/commands/openclaw-install.js +44 -0
  167. package/dist/cli/commands/opencode-install.js +99 -0
  168. package/dist/cli/commands/plugin-commands.js +246 -0
  169. package/dist/cli/commands/publish-agent.js +184 -0
  170. package/dist/cli/commands/security-audit.js +219 -0
  171. package/dist/cli/commands/skill-install.js +481 -0
  172. package/dist/cli/commands/status.js +196 -0
  173. package/dist/cli/commands/storyteller.js +230 -0
  174. package/dist/cli/index.js +986 -0
  175. package/dist/cli/server.js +147 -0
  176. package/dist/config/default-agents.js +16 -0
  177. package/dist/core/activity-logger.js +260 -0
  178. package/dist/core/adaptive-kernel.js +192 -0
  179. package/dist/core/agent-spawn-gate.js +120 -0
  180. package/dist/core/boot-orchestrator.js +812 -0
  181. package/dist/core/bridge.mjs +945 -0
  182. package/dist/core/codex-formatter.js +220 -0
  183. package/dist/core/codex-injector.js +424 -0
  184. package/dist/core/config-loader.js +148 -0
  185. package/dist/core/config-paths.js +162 -0
  186. package/dist/core/context-loader.js +269 -0
  187. package/dist/core/context-validator.js +212 -0
  188. package/dist/core/features-config.js +457 -0
  189. package/dist/core/framework-logger.js +275 -0
  190. package/dist/core/index.js +6 -0
  191. package/dist/core/kernel-patterns.js +302 -0
  192. package/dist/core/logging-config.js +43 -0
  193. package/dist/core/model-router.js +175 -0
  194. package/dist/core/orchestrator.js +408 -0
  195. package/dist/core/system-prompt-generator.js +265 -0
  196. package/dist/core/trace-context.js +33 -0
  197. package/dist/core/xray-activation.js +134 -0
  198. package/dist/delegation/agent-delegator.js +769 -0
  199. package/dist/delegation/agent-expertise.js +156 -0
  200. package/dist/delegation/analytics/index.js +12 -0
  201. package/dist/delegation/analytics/learning-engine.js +277 -0
  202. package/dist/delegation/analytics/outcome-tracker.js +279 -0
  203. package/dist/delegation/analytics/routing-analytics.js +193 -0
  204. package/dist/delegation/ast-code-parser.js +878 -0
  205. package/dist/delegation/codebase-context-analyzer.js +1040 -0
  206. package/dist/delegation/complexity-analyzer.js +282 -0
  207. package/dist/delegation/complexity-core.js +219 -0
  208. package/dist/delegation/config/types.js +6 -0
  209. package/dist/delegation/dependency-graph-builder.js +409 -0
  210. package/dist/delegation/index.js +20 -0
  211. package/dist/delegation/metrics-aggregator.js +335 -0
  212. package/dist/delegation/session-coordinator.js +352 -0
  213. package/dist/delegation/strategy-selector.js +108 -0
  214. package/dist/delegation/voting-coordinator.js +375 -0
  215. package/dist/delegation/voting-types.js +10 -0
  216. package/dist/delegation/weighted-voting-aggregator.js +194 -0
  217. package/dist/enforcement/core/index.js +19 -0
  218. package/dist/enforcement/core/rule-executor.js +365 -0
  219. package/dist/enforcement/core/rule-hierarchy.js +259 -0
  220. package/dist/enforcement/core/rule-registry.js +240 -0
  221. package/dist/enforcement/core/violation-fixer.js +651 -0
  222. package/dist/enforcement/enforcer-tools.js +909 -0
  223. package/dist/enforcement/index.js +41 -0
  224. package/dist/enforcement/loaders/agent-triage-loader.js +222 -0
  225. package/dist/enforcement/loaders/agents-md-validation-loader.js +252 -0
  226. package/dist/enforcement/loaders/base-loader.js +86 -0
  227. package/dist/enforcement/loaders/codex-loader.js +396 -0
  228. package/dist/enforcement/loaders/index.js +40 -0
  229. package/dist/enforcement/loaders/loader-orchestrator.js +168 -0
  230. package/dist/enforcement/loaders/processor-loader.js +113 -0
  231. package/dist/enforcement/rule-enforcer.js +298 -0
  232. package/dist/enforcement/test-auto-healing.js +325 -0
  233. package/dist/enforcement/types.js +30 -0
  234. package/dist/enforcement/validators/architecture-validators.js +600 -0
  235. package/dist/enforcement/validators/base-validator.js +108 -0
  236. package/dist/enforcement/validators/code-quality-validators.js +336 -0
  237. package/dist/enforcement/validators/index.js +21 -0
  238. package/dist/enforcement/validators/security-validators.js +220 -0
  239. package/dist/enforcement/validators/testing-validators.js +253 -0
  240. package/dist/enforcement/validators/validator-registry.js +150 -0
  241. package/dist/execution/opencode-cli-invoker.js +173 -0
  242. package/dist/execution/proposal-applier.js +254 -0
  243. package/dist/governance/codex-policy.service.js +167 -0
  244. package/dist/governance/governance-core.js +152 -0
  245. package/dist/governance/governance-service.js +274 -0
  246. package/dist/governance/governance-types.js +6 -0
  247. package/dist/index.js +24 -0
  248. package/dist/inference/deploy-verifier.js +161 -0
  249. package/dist/inference/index.js +5 -0
  250. package/dist/inference/inference-accumulator.js +126 -0
  251. package/dist/inference/inference-cycle.js +1168 -0
  252. package/dist/inference/semantic-patterns.js +310 -0
  253. package/dist/inference/session-capture.js +308 -0
  254. package/dist/integrations/base/ExampleIntegration.js +181 -0
  255. package/dist/integrations/base/Integration.js +395 -0
  256. package/dist/integrations/base/README.md +446 -0
  257. package/dist/integrations/base/index.js +16 -0
  258. package/dist/integrations/base/registry.js +606 -0
  259. package/dist/integrations/base/types.js +118 -0
  260. package/dist/integrations/governance/governance-client.js +316 -0
  261. package/dist/integrations/governance/index.js +373 -0
  262. package/dist/integrations/governance/types.js +97 -0
  263. package/dist/integrations/grok/grok-cli.js +83 -0
  264. package/dist/integrations/grok/hooks/pre-tool-use.js +134 -0
  265. package/dist/integrations/grok/plugin/0xray/.mcp.json +15 -0
  266. package/dist/integrations/grok/plugin/0xray/hooks/hooks.json +30 -0
  267. package/dist/integrations/hermes-agent/__init__.py +718 -0
  268. package/dist/integrations/hermes-agent/after-install.md +71 -0
  269. package/dist/integrations/hermes-agent/bridge.mjs +861 -0
  270. package/dist/integrations/hermes-agent/conftest.py +14 -0
  271. package/dist/integrations/hermes-agent/plugin.yaml +12 -0
  272. package/dist/integrations/hermes-agent/schemas.py +100 -0
  273. package/dist/integrations/hermes-agent/test_plugin.py +1100 -0
  274. package/dist/integrations/hermes-agent/tools.py +253 -0
  275. package/dist/integrations/openclaw/README.md +134 -0
  276. package/dist/integrations/openclaw/api-server.js +389 -0
  277. package/dist/integrations/openclaw/client.js +492 -0
  278. package/dist/integrations/openclaw/config.js +374 -0
  279. package/dist/integrations/openclaw/hooks/strray-hooks.js +280 -0
  280. package/dist/integrations/openclaw/index.js +351 -0
  281. package/dist/integrations/openclaw/types.js +153 -0
  282. package/dist/integrations/plugins/index.js +10 -0
  283. package/dist/integrations/plugins/plugin-integration.js +606 -0
  284. package/dist/integrations/plugins/plugin-registry.js +580 -0
  285. package/dist/mcps/agent-resolver.js +106 -0
  286. package/dist/mcps/architect-tools.server.js +277 -0
  287. package/dist/mcps/auto-format.server.js +413 -0
  288. package/dist/mcps/boot-orchestrator.server.js +853 -0
  289. package/dist/mcps/config/config-loader.js +103 -0
  290. package/dist/mcps/config/config-validator.js +101 -0
  291. package/dist/mcps/config/index.js +30 -0
  292. package/dist/mcps/config/plugin-server-registry.js +223 -0
  293. package/dist/mcps/config/server-config-registry.js +348 -0
  294. package/dist/mcps/connection/connection-manager.js +79 -0
  295. package/dist/mcps/connection/connection-pool.js +164 -0
  296. package/dist/mcps/connection/mcp-connection.js +233 -0
  297. package/dist/mcps/connection/process-spawner.js +34 -0
  298. package/dist/mcps/enforcer-tools.server.js +778 -0
  299. package/dist/mcps/estimation.server.js +192 -0
  300. package/dist/mcps/framework-compliance-audit.server.js +500 -0
  301. package/dist/mcps/framework-help.server.js +400 -0
  302. package/dist/mcps/governance.server.js +464 -0
  303. package/dist/mcps/in-process-skill-registry.js +48 -0
  304. package/dist/mcps/knowledge-skills/api-design.server.js +123 -0
  305. package/dist/mcps/knowledge-skills/architecture-patterns.server.js +113 -0
  306. package/dist/mcps/knowledge-skills/bug-triage-specialist.server.js +474 -0
  307. package/dist/mcps/knowledge-skills/code-analyzer.server.js +605 -0
  308. package/dist/mcps/knowledge-skills/code-review.server.js +847 -0
  309. package/dist/mcps/knowledge-skills/content-creator.server.js +256 -0
  310. package/dist/mcps/knowledge-skills/database-design.server.js +822 -0
  311. package/dist/mcps/knowledge-skills/devops-deployment.server.js +1180 -0
  312. package/dist/mcps/knowledge-skills/git-workflow.server.js +115 -0
  313. package/dist/mcps/knowledge-skills/growth-strategist.server.js +361 -0
  314. package/dist/mcps/knowledge-skills/log-monitor.server.js +451 -0
  315. package/dist/mcps/knowledge-skills/mobile-development.server.js +525 -0
  316. package/dist/mcps/knowledge-skills/multimodal-looker.server.js +1063 -0
  317. package/dist/mcps/knowledge-skills/performance-optimization.server.js +1587 -0
  318. package/dist/mcps/knowledge-skills/project-analysis.server.js +811 -0
  319. package/dist/mcps/knowledge-skills/refactoring-strategies.server.js +796 -0
  320. package/dist/mcps/knowledge-skills/security-audit.server.js +846 -0
  321. package/dist/mcps/knowledge-skills/seo-consultant.server.js +937 -0
  322. package/dist/mcps/knowledge-skills/session-management.server.js +470 -0
  323. package/dist/mcps/knowledge-skills/skill-invocation.server.js +729 -0
  324. package/dist/mcps/knowledge-skills/strategist.server.js +217 -0
  325. package/dist/mcps/knowledge-skills/tech-writer.server.js +1191 -0
  326. package/dist/mcps/knowledge-skills/testing-best-practices.server.js +866 -0
  327. package/dist/mcps/knowledge-skills/testing-strategy.server.js +827 -0
  328. package/dist/mcps/knowledge-skills/ui-ux-design.server.js +1538 -0
  329. package/dist/mcps/lint.server.js +381 -0
  330. package/dist/mcps/mcp-client.js +574 -0
  331. package/dist/mcps/model-health-check.server.js +228 -0
  332. package/dist/mcps/orchestrator/config/agent-capabilities.js +87 -0
  333. package/dist/mcps/orchestrator/execution/execution-planner.js +279 -0
  334. package/dist/mcps/orchestrator/handlers/complexity-handler.js +94 -0
  335. package/dist/mcps/orchestrator/handlers/status-handler.js +232 -0
  336. package/dist/mcps/orchestrator/handlers/task-handler.js +197 -0
  337. package/dist/mcps/orchestrator/server.js +305 -0
  338. package/dist/mcps/orchestrator/types.js +6 -0
  339. package/dist/mcps/orchestrator.server.js +19 -0
  340. package/dist/mcps/performance-analysis.server.js +519 -0
  341. package/dist/mcps/processor-pipeline.server.js +561 -0
  342. package/dist/mcps/protocol/protocol-constants.js +46 -0
  343. package/dist/mcps/registry.json +110 -0
  344. package/dist/mcps/researcher.server.js +504 -0
  345. package/dist/mcps/security-scan.server.js +531 -0
  346. package/dist/mcps/simulation/index.js +12 -0
  347. package/dist/mcps/simulation/server-simulations.js +219 -0
  348. package/dist/mcps/simulation/simulation-engine.js +96 -0
  349. package/dist/mcps/state-manager.server.js +637 -0
  350. package/dist/mcps/tools/index.js +14 -0
  351. package/dist/mcps/tools/tool-cache.js +112 -0
  352. package/dist/mcps/tools/tool-discovery.js +65 -0
  353. package/dist/mcps/tools/tool-executor.js +75 -0
  354. package/dist/mcps/tools/tool-registry.js +67 -0
  355. package/dist/mcps/types/index.js +15 -0
  356. package/dist/mcps/types/json-rpc.types.js +7 -0
  357. package/dist/mcps/types/mcp.types.js +7 -0
  358. package/dist/metrics/agent-metrics.js +574 -0
  359. package/dist/metrics/index.js +6 -0
  360. package/dist/monitoring/advanced-profiler.js +232 -0
  361. package/dist/monitoring/memory-monitor.js +315 -0
  362. package/dist/monitoring/nudge-watchdog.js +356 -0
  363. package/dist/monitoring/test-auto-generation-monitor.js +157 -0
  364. package/dist/orchestrator/agent-spawn-governor.js +559 -0
  365. package/dist/orchestrator/enhanced-multi-agent-orchestrator.js +399 -0
  366. package/dist/orchestrator/intelligent-commit-batcher.js +353 -0
  367. package/dist/orchestrator/multi-agent-orchestration-coordinator.js +456 -0
  368. package/dist/orchestrator/orchestrator.js +657 -0
  369. package/dist/orchestrator/self-direction-activation.js +245 -0
  370. package/dist/orchestrator/universal-librarian-consultation.js +216 -0
  371. package/dist/orchestrator/universal-registry-bridge.js +247 -0
  372. package/dist/performance/performance-budget-enforcer.js +434 -0
  373. package/dist/performance/performance-regression-tester.js +342 -0
  374. package/dist/plugin/xray-codex-injection.js +857 -0
  375. package/dist/postprocessor/PostProcessor.js +1048 -0
  376. package/dist/postprocessor/analysis/FailureAnalysisEngine.js +245 -0
  377. package/dist/postprocessor/autofix/AutoFixEngine.js +254 -0
  378. package/dist/postprocessor/autofix/FixValidator.js +56 -0
  379. package/dist/postprocessor/config.js +65 -0
  380. package/dist/postprocessor/escalation/EscalationEngine.js +492 -0
  381. package/dist/postprocessor/monitoring/MonitoringEngine.js +125 -0
  382. package/dist/postprocessor/redeploy/RedeployCoordinator.js +342 -0
  383. package/dist/postprocessor/services/RegressionAnalysisService.js +131 -0
  384. package/dist/postprocessor/success/SuccessHandler.js +134 -0
  385. package/dist/postprocessor/triggers/APITrigger.js +115 -0
  386. package/dist/postprocessor/triggers/GitHookTrigger.js +551 -0
  387. package/dist/postprocessor/triggers/WebhookTrigger.js +211 -0
  388. package/dist/postprocessor/types.js +4 -0
  389. package/dist/processors/doc-write-guard.js +46 -0
  390. package/dist/processors/implementations/agents-md-validation-processor.js +286 -0
  391. package/dist/processors/implementations/async-pattern-processor.js +158 -0
  392. package/dist/processors/implementations/codex-compliance-processor.js +57 -0
  393. package/dist/processors/implementations/commit-batcher-processor.js +71 -0
  394. package/dist/processors/implementations/console-log-guard-processor.js +163 -0
  395. package/dist/processors/implementations/coverage-analysis-processor.js +138 -0
  396. package/dist/processors/implementations/error-boundary-processor.js +44 -0
  397. package/dist/processors/implementations/inference-improvement-processor.js +270 -0
  398. package/dist/processors/implementations/log-protection-processor.js +118 -0
  399. package/dist/processors/implementations/nudge-processor.js +130 -0
  400. package/dist/processors/implementations/performance-budget-processor.js +217 -0
  401. package/dist/processors/implementations/postprocessor-chain-validator.js +149 -0
  402. package/dist/processors/implementations/pre-validate-processor.js +18 -0
  403. package/dist/processors/implementations/publish-preflight-processor.js +249 -0
  404. package/dist/processors/implementations/refactoring-logging-processor-wrapper.js +33 -0
  405. package/dist/processors/implementations/refactoring-logging-processor.js +96 -0
  406. package/dist/processors/implementations/regression-testing-processor.js +59 -0
  407. package/dist/processors/implementations/session-capture-processor.js +37 -0
  408. package/dist/processors/implementations/session-summary-processor.js +130 -0
  409. package/dist/processors/implementations/spawn-governance-processor.js +219 -0
  410. package/dist/processors/implementations/state-validation-processor.js +15 -0
  411. package/dist/processors/implementations/storytelling-trigger-processor.js +589 -0
  412. package/dist/processors/implementations/test-auto-creation-processor.js +484 -0
  413. package/dist/processors/implementations/test-execution-processor.js +132 -0
  414. package/dist/processors/implementations/typescript-compilation-processor.js +87 -0
  415. package/dist/processors/implementations/version-compliance-processor.js +350 -0
  416. package/dist/processors/processor-interfaces.js +126 -0
  417. package/dist/processors/processor-manager.js +826 -0
  418. package/dist/processors/processor-types.js +12 -0
  419. package/dist/public/about.html +228 -0
  420. package/dist/public/enterprise.html +27 -0
  421. package/dist/public/features.html +102 -0
  422. package/dist/public/index.html +145 -0
  423. package/dist/reporting/framework-reporting-system.js +187 -0
  424. package/dist/reporting/log-parser.js +281 -0
  425. package/dist/reporting/metrics.js +202 -0
  426. package/dist/reporting/report-formatter.js +146 -0
  427. package/dist/reporting/types.js +1 -0
  428. package/dist/scripts/activate-kernel-pipeline.js +101 -0
  429. package/dist/scripts/integration.js +234 -0
  430. package/dist/scripts/pre-command +26 -0
  431. package/dist/scripts/pre-command.mjs +358 -0
  432. package/dist/security/comprehensive-security-audit.js +1005 -0
  433. package/dist/security/index.js +13 -0
  434. package/dist/security/prompt-security-validator.js +148 -0
  435. package/dist/security/security-agent-coordinator.js +204 -0
  436. package/dist/security/security-auditor.js +584 -0
  437. package/dist/security/security-hardener.js +170 -0
  438. package/dist/security/security-hardening-system.js +727 -0
  439. package/dist/security/security-headers.js +118 -0
  440. package/dist/security/security-orchestration-layer.js +496 -0
  441. package/dist/security/security-scanner.js +429 -0
  442. package/dist/services/inference-tuner.js +301 -0
  443. package/dist/session/index.js +3 -0
  444. package/dist/session/session-cleanup-manager.js +366 -0
  445. package/dist/session/session-monitor.js +503 -0
  446. package/dist/session/session-state-manager.js +522 -0
  447. package/dist/skills/api-design/SKILL.md +37 -0
  448. package/dist/skills/architect-tools/SKILL.md +37 -0
  449. package/dist/skills/architecture-patterns/SKILL.md +37 -0
  450. package/dist/skills/auto-format/SKILL.md +37 -0
  451. package/dist/skills/backend-engineer/SKILL.md +49 -0
  452. package/dist/skills/boot-orchestrator/SKILL.md +37 -0
  453. package/dist/skills/bug-triage/SKILL.md +43 -0
  454. package/dist/skills/code-analyzer/SKILL.md +45 -0
  455. package/dist/skills/code-review/SKILL.md +52 -0
  456. package/dist/skills/content-creator/SKILL.md +38 -0
  457. package/dist/skills/database-engineer/SKILL.md +46 -0
  458. package/dist/skills/devops-engineer/SKILL.md +49 -0
  459. package/dist/skills/enforcer/SKILL.md +37 -0
  460. package/dist/skills/framework-compliance-audit/SKILL.md +37 -0
  461. package/dist/skills/frontend-engineer/SKILL.md +49 -0
  462. package/dist/skills/frontend-ui-ux-engineer/SKILL.md +41 -0
  463. package/dist/skills/git-workflow/SKILL.md +37 -0
  464. package/dist/skills/growth-strategist/SKILL.md +48 -0
  465. package/dist/skills/hermes-agent/SKILL.md +212 -0
  466. package/dist/skills/inference-improve/SKILL.md +97 -0
  467. package/dist/skills/lint/SKILL.md +37 -0
  468. package/dist/skills/log-monitor/SKILL.md +44 -0
  469. package/dist/skills/mobile-developer/SKILL.md +42 -0
  470. package/dist/skills/model-health-check/SKILL.md +37 -0
  471. package/dist/skills/multimodal-looker/SKILL.md +45 -0
  472. package/dist/skills/orchestrator/SKILL.md +37 -0
  473. package/dist/skills/performance-analysis/SKILL.md +37 -0
  474. package/dist/skills/performance-engineer/SKILL.md +41 -0
  475. package/dist/skills/performance-optimization/SKILL.md +37 -0
  476. package/dist/skills/processor-pipeline/SKILL.md +37 -0
  477. package/dist/skills/project-analysis/SKILL.md +42 -0
  478. package/dist/skills/refactoring-strategies/SKILL.md +37 -0
  479. package/dist/skills/registry.json +66 -0
  480. package/dist/skills/researcher/SKILL.md +37 -0
  481. package/dist/skills/security-audit/SKILL.md +48 -0
  482. package/dist/skills/security-scan/SKILL.md +37 -0
  483. package/dist/skills/seo-consultant/SKILL.md +43 -0
  484. package/dist/skills/session-management/SKILL.md +36 -0
  485. package/dist/skills/state-manager/SKILL.md +37 -0
  486. package/dist/skills/storyteller/SKILL.md +130 -0
  487. package/dist/skills/strategist/SKILL.md +32 -0
  488. package/dist/skills/tech-writer/SKILL.md +37 -0
  489. package/dist/skills/testing-best-practices/SKILL.md +37 -0
  490. package/dist/skills/testing-strategy/SKILL.md +43 -0
  491. package/dist/skills/ui-ux-design/SKILL.md +603 -0
  492. package/dist/state/context-providers.js +1 -0
  493. package/dist/state/index.js +7 -0
  494. package/dist/state/state-manager.js +208 -0
  495. package/dist/state/state-types.js +1 -0
  496. package/dist/testing/memory-regression-suite.js +258 -0
  497. package/dist/utils/batch-operations.js +292 -0
  498. package/dist/utils/codex-parser.js +445 -0
  499. package/dist/utils/command-runner.js +96 -0
  500. package/dist/utils/import-resolver.js +189 -0
  501. package/dist/utils/language-detector.js +383 -0
  502. package/dist/utils/path-resolver.js +112 -0
  503. package/dist/utils/shutdown-handler.js +75 -0
  504. package/dist/utils/test-template-generator.js +178 -0
  505. package/dist/utils/token-manager.js +163 -0
  506. package/dist/validation/estimation-validator.js +241 -0
  507. package/dist/validation/report-content-validator.js +218 -0
  508. package/opencode.json +153 -0
  509. package/package.json +170 -0
  510. package/scripts/helpers/resolve-config-path.cjs +57 -0
  511. package/scripts/helpers/resolve-config-path.mjs +73 -0
  512. package/scripts/hooks/pre-command +26 -0
  513. package/scripts/hooks/pre-command.mjs +358 -0
  514. package/scripts/hooks/run-hook.js +570 -0
  515. package/scripts/mjs/test-consumer-readiness.mjs +273 -0
  516. package/scripts/mjs/test-mcp-functionality.mjs +507 -0
  517. package/scripts/mjs/validate-mcp-connectivity.cjs +75 -0
  518. package/scripts/mjs/validate-postinstall-config.mjs +308 -0
  519. package/scripts/node/auto-reflection-generator.mjs +496 -0
  520. package/scripts/node/basic-security-audit.cjs +338 -0
  521. package/scripts/node/ci-cd-auto-fix.cjs +263 -0
  522. package/scripts/node/ci-report-generator.mjs +227 -0
  523. package/scripts/node/enforce-agents-md.mjs +420 -0
  524. package/scripts/node/enforce-version-compliance.sh +22 -0
  525. package/scripts/node/enforce-version-compliance.ts +126 -0
  526. package/scripts/node/github-actions-monitor.cjs +23 -0
  527. package/scripts/node/govern-reflection.mjs +160 -0
  528. package/scripts/node/postinstall.cjs +78 -0
  529. package/scripts/node/pre-publish-guard.js +267 -0
  530. package/scripts/node/prepare-consumer.cjs +143 -0
  531. package/scripts/node/reflection-processor.cjs +213 -0
  532. package/scripts/node/reflection-validate.sh +194 -0
  533. package/scripts/node/release-tweet.mjs +39 -0
  534. package/scripts/node/release.js +159 -0
  535. package/scripts/node/release.mjs +213 -0
  536. package/scripts/node/setup-dev.cjs +83 -0
  537. package/scripts/node/setup.cjs +214 -0
  538. package/scripts/node/sync-versions.mjs +140 -0
  539. package/scripts/node/universal-version-manager.js +1025 -0
  540. package/scripts/node/validate-external-processes.js +265 -0
  541. package/scripts/node/validate-mcp-connectivity.js +258 -0
  542. package/scripts/node/version-manager.mjs +524 -0
  543. package/scripts/validate-stringray-comprehensive.js +636 -0
  544. package/src/integrations/grok/plugin/0xray/.mcp.json +15 -0
  545. package/src/integrations/grok/plugin/0xray/hooks/hooks.json +30 -0
  546. package/src/mcps/agent-resolver.ts +168 -0
  547. package/src/mcps/architect-tools.server.ts +343 -0
  548. package/src/mcps/auto-format.server.ts +529 -0
  549. package/src/mcps/boot-orchestrator.server.ts +1082 -0
  550. package/src/mcps/config/__tests__/config-loader.test.ts +338 -0
  551. package/src/mcps/config/__tests__/config-validator.test.ts +646 -0
  552. package/src/mcps/config/__tests__/server-config-registry.test.ts +257 -0
  553. package/src/mcps/config/config-loader.ts +127 -0
  554. package/src/mcps/config/config-validator.ts +127 -0
  555. package/src/mcps/config/index.ts +32 -0
  556. package/src/mcps/config/plugin-server-registry.ts +335 -0
  557. package/src/mcps/config/server-config-registry.ts +395 -0
  558. package/src/mcps/connection/connection-manager.ts +91 -0
  559. package/src/mcps/connection/connection-pool.ts +216 -0
  560. package/src/mcps/connection/mcp-connection.ts +327 -0
  561. package/src/mcps/connection/process-spawner.ts +47 -0
  562. package/src/mcps/enforcer-tools.server.ts +1106 -0
  563. package/src/mcps/estimation.server.ts +229 -0
  564. package/src/mcps/framework-compliance-audit.server.ts +635 -0
  565. package/src/mcps/framework-help.server.ts +467 -0
  566. package/src/mcps/governance.server.ts +551 -0
  567. package/src/mcps/in-process-skill-registry.ts +79 -0
  568. package/src/mcps/knowledge-skills/api-design.server.test.ts +41 -0
  569. package/src/mcps/knowledge-skills/api-design.server.ts +160 -0
  570. package/src/mcps/knowledge-skills/architecture-patterns.server.ts +152 -0
  571. package/src/mcps/knowledge-skills/bug-triage-specialist.server.ts +624 -0
  572. package/src/mcps/knowledge-skills/code-analyzer.server.test.ts +129 -0
  573. package/src/mcps/knowledge-skills/code-analyzer.server.ts +591 -0
  574. package/src/mcps/knowledge-skills/code-review.server.ts +1132 -0
  575. package/src/mcps/knowledge-skills/content-creator.server.ts +300 -0
  576. package/src/mcps/knowledge-skills/database-design.server.ts +1200 -0
  577. package/src/mcps/knowledge-skills/devops-deployment.server.ts +1622 -0
  578. package/src/mcps/knowledge-skills/git-workflow.server.ts +152 -0
  579. package/src/mcps/knowledge-skills/growth-strategist.server.ts +413 -0
  580. package/src/mcps/knowledge-skills/log-monitor.server.ts +619 -0
  581. package/src/mcps/knowledge-skills/mobile-development.server.ts +672 -0
  582. package/src/mcps/knowledge-skills/multimodal-looker.server.ts +1500 -0
  583. package/src/mcps/knowledge-skills/performance-optimization.server.ts +2065 -0
  584. package/src/mcps/knowledge-skills/project-analysis.server.ts +1111 -0
  585. package/src/mcps/knowledge-skills/refactoring-strategies.server.ts +1092 -0
  586. package/src/mcps/knowledge-skills/security-audit.server.test.ts +112 -0
  587. package/src/mcps/knowledge-skills/security-audit.server.ts +1193 -0
  588. package/src/mcps/knowledge-skills/seo-consultant.server.ts +1160 -0
  589. package/src/mcps/knowledge-skills/session-management.server.ts +576 -0
  590. package/src/mcps/knowledge-skills/skill-invocation.server.ts +941 -0
  591. package/src/mcps/knowledge-skills/strategist.server.ts +267 -0
  592. package/src/mcps/knowledge-skills/tech-writer.server.ts +1638 -0
  593. package/src/mcps/knowledge-skills/testing-best-practices.server.test.ts +136 -0
  594. package/src/mcps/knowledge-skills/testing-best-practices.server.ts +1232 -0
  595. package/src/mcps/knowledge-skills/testing-strategy.server.test.ts +100 -0
  596. package/src/mcps/knowledge-skills/testing-strategy.server.ts +1172 -0
  597. package/src/mcps/knowledge-skills/ui-ux-design.server.ts +2076 -0
  598. package/src/mcps/lint.server.ts +483 -0
  599. package/src/mcps/mcp-client.ts +706 -0
  600. package/src/mcps/model-health-check.server.ts +292 -0
  601. package/src/mcps/orchestrator/config/agent-capabilities.ts +108 -0
  602. package/src/mcps/orchestrator/execution/execution-planner.ts +353 -0
  603. package/src/mcps/orchestrator/handlers/complexity-handler.ts +125 -0
  604. package/src/mcps/orchestrator/handlers/status-handler.ts +295 -0
  605. package/src/mcps/orchestrator/handlers/task-handler.ts +268 -0
  606. package/src/mcps/orchestrator/server.ts +388 -0
  607. package/src/mcps/orchestrator/types.ts +81 -0
  608. package/src/mcps/orchestrator.server.ts +34 -0
  609. package/src/mcps/performance-analysis.server.ts +715 -0
  610. package/src/mcps/processor-pipeline.server.ts +778 -0
  611. package/src/mcps/protocol/protocol-constants.ts +51 -0
  612. package/src/mcps/registry.json +110 -0
  613. package/src/mcps/researcher.server.ts +595 -0
  614. package/src/mcps/security-scan.server.ts +651 -0
  615. package/src/mcps/simulation/__tests__/simulation-engine.test.ts +275 -0
  616. package/src/mcps/simulation/index.ts +23 -0
  617. package/src/mcps/simulation/server-simulations.ts +241 -0
  618. package/src/mcps/simulation/simulation-engine.ts +126 -0
  619. package/src/mcps/state-manager.server.ts +777 -0
  620. package/src/mcps/tools/__tests__/tool-cache.test.ts +205 -0
  621. package/src/mcps/tools/__tests__/tool-discovery.test.ts +189 -0
  622. package/src/mcps/tools/__tests__/tool-executor.test.ts +215 -0
  623. package/src/mcps/tools/__tests__/tool-registry.test.ts +230 -0
  624. package/src/mcps/tools/index.ts +15 -0
  625. package/src/mcps/tools/tool-cache.ts +145 -0
  626. package/src/mcps/tools/tool-discovery.ts +83 -0
  627. package/src/mcps/tools/tool-executor.ts +106 -0
  628. package/src/mcps/tools/tool-registry.ts +78 -0
  629. package/src/mcps/types/__tests__/types.test.ts +341 -0
  630. package/src/mcps/types/index.ts +17 -0
  631. package/src/mcps/types/json-rpc.types.ts +38 -0
  632. package/src/mcps/types/mcp.types.ts +115 -0
  633. package/src/opencode/agents/api-design.yml +31 -0
  634. package/src/opencode/agents/architect.yml +15 -0
  635. package/src/opencode/agents/architecture-patterns.yml +14 -0
  636. package/src/opencode/agents/auto-format.yml +14 -0
  637. package/src/opencode/agents/backend-engineer.yml +12 -0
  638. package/src/opencode/agents/boot-orchestrator.yml +14 -0
  639. package/src/opencode/agents/bug-triage-specialist.yml +15 -0
  640. package/src/opencode/agents/code-analyzer.yml +12 -0
  641. package/src/opencode/agents/code-reviewer.yml +14 -0
  642. package/src/opencode/agents/content-creator.yml +12 -0
  643. package/src/opencode/agents/database-engineer.yml +12 -0
  644. package/src/opencode/agents/devops-engineer.yml +12 -0
  645. package/src/opencode/agents/framework-compliance-audit.yml +14 -0
  646. package/src/opencode/agents/frontend-engineer.yml +12 -0
  647. package/src/opencode/agents/frontend-ui-ux-engineer.yml +12 -0
  648. package/src/opencode/agents/git-workflow.yml +15 -0
  649. package/src/opencode/agents/growth-strategist.yml +12 -0
  650. package/src/opencode/agents/hermes-agent.yml +12 -0
  651. package/src/opencode/agents/inference-improve.yml +12 -0
  652. package/src/opencode/agents/lint.yml +14 -0
  653. package/src/opencode/agents/log-monitor.yml +12 -0
  654. package/src/opencode/agents/mobile-developer.yml +12 -0
  655. package/src/opencode/agents/model-health-check.yml +12 -0
  656. package/src/opencode/agents/multimodal-looker.yml +12 -0
  657. package/src/opencode/agents/performance-analysis.yml +12 -0
  658. package/src/opencode/agents/performance-engineer.yml +12 -0
  659. package/src/opencode/agents/performance-optimization.yml +12 -0
  660. package/src/opencode/agents/processor-pipeline.yml +14 -0
  661. package/src/opencode/agents/project-analysis.yml +12 -0
  662. package/src/opencode/agents/refactorer.yml +12 -0
  663. package/src/opencode/agents/researcher.yml +12 -0
  664. package/src/opencode/agents/security-auditor.yml +12 -0
  665. package/src/opencode/agents/security-scan.yml +12 -0
  666. package/src/opencode/agents/seo-consultant.yml +12 -0
  667. package/src/opencode/agents/session-management.yml +12 -0
  668. package/src/opencode/agents/state-manager.yml +12 -0
  669. package/src/opencode/agents/storyteller.yml +12 -0
  670. package/src/opencode/agents/strategist.yml +12 -0
  671. package/src/opencode/agents/tech-writer.yml +12 -0
  672. package/src/opencode/agents/testing-best-practices.yml +12 -0
  673. package/src/opencode/agents/testing-lead.yml +12 -0
  674. package/src/opencode/agents/ui-ux-design.yml +12 -0
  675. package/src/opencode/codex.codex +8 -0
  676. package/src/opencode/commands/auto-format.md +99 -0
  677. package/src/opencode/commands/auto-summary-capture.md +90 -0
  678. package/src/opencode/commands/dependency-audit.md +184 -0
  679. package/src/opencode/commands/enforcer-daily-scan.md +137 -0
  680. package/src/opencode/commands/framework-compliance-audit.md +205 -0
  681. package/src/opencode/commands/interactive-validator.md +75 -0
  682. package/src/opencode/commands/job-summary-logger.md +68 -0
  683. package/src/opencode/commands/lint.md +11 -0
  684. package/src/opencode/commands/mode-switch.md +95 -0
  685. package/src/opencode/commands/model-health-check.md +186 -0
  686. package/src/opencode/commands/performance-analysis.md +144 -0
  687. package/src/opencode/commands/pre-commit-introspection.md +185 -0
  688. package/src/opencode/commands/pre-commit-introspection.sh +133 -0
  689. package/src/opencode/commands/security-scan.md +157 -0
  690. package/src/opencode/commands/sisyphus-validation.md +128 -0
  691. package/src/opencode/commands/summary-logger.md +83 -0
  692. package/src/opencode/enforcer-config.json +285 -0
  693. package/src/opencode/openclaw/config.json +25 -0
  694. package/src/opencode/workflows/post-deployment-audit.yml +123 -0
  695. package/src/skills/api-design/SKILL.md +37 -0
  696. package/src/skills/architect-tools/SKILL.md +37 -0
  697. package/src/skills/architecture-patterns/SKILL.md +37 -0
  698. package/src/skills/auto-format/SKILL.md +37 -0
  699. package/src/skills/backend-engineer/SKILL.md +49 -0
  700. package/src/skills/boot-orchestrator/SKILL.md +37 -0
  701. package/src/skills/bug-triage/SKILL.md +43 -0
  702. package/src/skills/code-analyzer/SKILL.md +45 -0
  703. package/src/skills/code-review/SKILL.md +52 -0
  704. package/src/skills/content-creator/SKILL.md +38 -0
  705. package/src/skills/database-engineer/SKILL.md +46 -0
  706. package/src/skills/devops-engineer/SKILL.md +49 -0
  707. package/src/skills/enforcer/SKILL.md +37 -0
  708. package/src/skills/framework-compliance-audit/SKILL.md +37 -0
  709. package/src/skills/frontend-engineer/SKILL.md +49 -0
  710. package/src/skills/frontend-ui-ux-engineer/SKILL.md +41 -0
  711. package/src/skills/git-workflow/SKILL.md +37 -0
  712. package/src/skills/growth-strategist/SKILL.md +48 -0
  713. package/src/skills/hermes-agent/SKILL.md +212 -0
  714. package/src/skills/inference-improve/SKILL.md +97 -0
  715. package/src/skills/lint/SKILL.md +37 -0
  716. package/src/skills/log-monitor/SKILL.md +44 -0
  717. package/src/skills/mobile-developer/SKILL.md +42 -0
  718. package/src/skills/model-health-check/SKILL.md +37 -0
  719. package/src/skills/multimodal-looker/SKILL.md +45 -0
  720. package/src/skills/orchestrator/SKILL.md +37 -0
  721. package/src/skills/performance-analysis/SKILL.md +37 -0
  722. package/src/skills/performance-engineer/SKILL.md +41 -0
  723. package/src/skills/performance-optimization/SKILL.md +37 -0
  724. package/src/skills/processor-pipeline/SKILL.md +37 -0
  725. package/src/skills/project-analysis/SKILL.md +42 -0
  726. package/src/skills/refactoring-strategies/SKILL.md +37 -0
  727. package/src/skills/registry.json +66 -0
  728. package/src/skills/researcher/SKILL.md +37 -0
  729. package/src/skills/security-audit/SKILL.md +48 -0
  730. package/src/skills/security-scan/SKILL.md +37 -0
  731. package/src/skills/seo-consultant/SKILL.md +43 -0
  732. package/src/skills/session-management/SKILL.md +36 -0
  733. package/src/skills/state-manager/SKILL.md +37 -0
  734. package/src/skills/storyteller/SKILL.md +130 -0
  735. package/src/skills/strategist/SKILL.md +32 -0
  736. package/src/skills/tech-writer/SKILL.md +37 -0
  737. package/src/skills/testing-best-practices/SKILL.md +37 -0
  738. package/src/skills/testing-strategy/SKILL.md +43 -0
  739. package/src/skills/ui-ux-design/SKILL.md +603 -0
@@ -0,0 +1,1100 @@
1
+ """Comprehensive tests for the StringRay Hermes Plugin v2.
2
+
3
+ Tests all 3 tools, both hooks, the slash command, bridge integration,
4
+ logging to disk, and the full register() wiring.
5
+
6
+ v2 changes from v1:
7
+ - Hooks now pipe through Node.js bridge for real framework integration
8
+ - Tool events logged to disk (activity.log, plugin-tool-events.log)
9
+ - Tools use bridge first, fall back to CLI
10
+ - Session stats track quality gate runs, processor runs, bridge calls
11
+ - No more in-memory _TOOL_LOG — everything persists to disk
12
+ """
13
+
14
+ import json
15
+ import subprocess
16
+ import os
17
+ import sys
18
+ import tempfile
19
+ import unittest
20
+ from pathlib import Path
21
+ from unittest.mock import patch, MagicMock, call
22
+ import logging
23
+ import importlib
24
+ import types
25
+
26
+ # ── Path setup ────────────────────────────────────────────────
27
+
28
+ PLUGIN_DIR = str(Path(__file__).resolve().parent)
29
+ sys.path.insert(0, PLUGIN_DIR)
30
+
31
+ # Force reimport
32
+ for mod in list(sys.modules):
33
+ if "strray" in mod and ("schemas" in mod or "tools" in mod or "plugin" in mod):
34
+ del sys.modules[mod]
35
+
36
+ schemas = importlib.import_module("schemas")
37
+ tools_mod = importlib.import_module("tools")
38
+
39
+ # Create a fake package for __init__.py execution
40
+ pkg = types.ModuleType("strray_hermes_pkg")
41
+ pkg.__path__ = [PLUGIN_DIR]
42
+ pkg.__dict__["schemas"] = schemas
43
+ pkg.__dict__["tools"] = tools_mod
44
+ sys.modules["strray_hermes_pkg"] = pkg
45
+
46
+ init_path = os.path.join(PLUGIN_DIR, "__init__.py")
47
+ with open(init_path) as f:
48
+ init_code = f.read()
49
+ init_code = init_code.replace("from . import schemas, tools", "import schemas, tools")
50
+ # Provide __file__ since exec() loses it
51
+ pkg.__dict__["__file__"] = init_path
52
+ exec(compile(init_code, init_path, "exec"), pkg.__dict__)
53
+
54
+ pi = pkg # plugin init module
55
+
56
+
57
+ class TestSchemas(unittest.TestCase):
58
+ def test_validate_schema_has_required_fields(self):
59
+ s = schemas.STRRAY_VALIDATE
60
+ self.assertEqual(s["name"], "strray_validate")
61
+ params = s["parameters"]
62
+ self.assertEqual(params["type"], "object")
63
+ self.assertIn("files", params["properties"])
64
+ self.assertIn("operation", params["properties"])
65
+ self.assertIn("files", params["required"])
66
+ self.assertEqual(params["properties"]["files"]["type"], "array")
67
+
68
+ def test_codex_check_schema(self):
69
+ s = schemas.STRRAY_CODEX_CHECK
70
+ self.assertEqual(s["name"], "strray_codex_check")
71
+ fa = s["parameters"]["properties"]["focus_areas"]
72
+ self.assertIn("enum", fa["items"])
73
+ self.assertIn("error-handling", fa["items"]["enum"])
74
+
75
+ def test_health_schema_no_required(self):
76
+ s = schemas.STRRAY_HEALTH
77
+ self.assertEqual(len(s["parameters"].get("required", [])), 0)
78
+
79
+ def test_descriptions_non_empty(self):
80
+ for name, schema in [("v", schemas.STRRAY_VALIDATE), ("c", schemas.STRRAY_CODEX_CHECK), ("h", schemas.STRRAY_HEALTH)]:
81
+ self.assertTrue(len(schema["description"]) > 20, f"{name}")
82
+
83
+
84
+ class TestRunStrrayHelper(unittest.TestCase):
85
+ """Test the CLI fallback helper (still exists for bridge-less environments)."""
86
+
87
+ def test_successful_command(self):
88
+ with patch("subprocess.run") as m:
89
+ m.return_value = MagicMock(returncode=0, stdout="all good", stderr="")
90
+ r = json.loads(tools_mod._run_strray(["health"]))
91
+ self.assertEqual(r["status"], "ok")
92
+ self.assertEqual(m.call_args[0][0], ["npx", "0xray", "health"])
93
+
94
+ def test_command_failure(self):
95
+ with patch("subprocess.run") as m:
96
+ m.return_value = MagicMock(returncode=1, stdout="", stderr="broke")
97
+ r = json.loads(tools_mod._run_strray(["validate"]))
98
+ self.assertEqual(r["status"], "error")
99
+
100
+ def test_file_not_found(self):
101
+ with patch("subprocess.run", side_effect=FileNotFoundError):
102
+ r = json.loads(tools_mod._run_strray(["health"]))
103
+ self.assertIn("not found", r["error"])
104
+
105
+ def test_timeout(self):
106
+ with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("c", 30)):
107
+ r = json.loads(tools_mod._run_strray(["health"], timeout=15))
108
+ self.assertIn("15s", r["error"])
109
+
110
+ def test_custom_timeout(self):
111
+ with patch("subprocess.run") as m:
112
+ m.return_value = MagicMock(returncode=0, stdout="", stderr="")
113
+ tools_mod._run_strray(["health"], timeout=60)
114
+ self.assertEqual(m.call_args[1]["timeout"], 60)
115
+
116
+
117
+ class TestBridgeHelper(unittest.TestCase):
118
+ """Test the bridge.mjs calling helper."""
119
+
120
+ def test_successful_bridge_call(self):
121
+ with patch("subprocess.run") as m:
122
+ m.return_value = MagicMock(returncode=0, stdout='{"status":"ok"}', stderr="")
123
+ r = tools_mod._bridge_call({"command": "health"})
124
+ self.assertEqual(r["status"], "ok")
125
+ # Should call node with bridge path
126
+ self.assertIn("node", m.call_args[0][0])
127
+ self.assertIn("bridge.mjs", m.call_args[0][0][1])
128
+
129
+ def test_bridge_returns_error(self):
130
+ with patch("subprocess.run") as m:
131
+ m.return_value = MagicMock(returncode=1, stdout="", stderr="module not found")
132
+ r = tools_mod._bridge_call({"command": "health"})
133
+ self.assertIn("error", r)
134
+
135
+ def test_bridge_node_not_found(self):
136
+ with patch("subprocess.run", side_effect=FileNotFoundError):
137
+ r = tools_mod._bridge_call({"command": "health"})
138
+ self.assertEqual(r["error"], "node not found")
139
+
140
+ def test_bridge_timeout(self):
141
+ with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("c", 10)):
142
+ r = tools_mod._bridge_call({"command": "health"}, timeout=10)
143
+ self.assertIn("timed out", r["error"])
144
+
145
+
146
+ class TestStrrayHealth(unittest.TestCase):
147
+ def test_health_via_bridge(self):
148
+ """v2: health uses bridge first."""
149
+ with patch.object(tools_mod, "_bridge_call", return_value={"status": "ok", "framework": "loaded", "version": "1.15.0", "components": {}}) as m:
150
+ r = json.loads(tools_mod.strray_health({}))
151
+ self.assertEqual(r["status"], "ok")
152
+ self.assertEqual(r["via"], "bridge")
153
+ m.assert_called_once_with({"command": "health"}, timeout=10)
154
+
155
+ def test_health_fallback_to_cli(self):
156
+ """v2: falls back to CLI when bridge fails."""
157
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
158
+ with patch.object(tools_mod, "_run_strray", return_value='{"status":"ok","output":"healthy"}') as cli:
159
+ r = json.loads(tools_mod.strray_health({}))
160
+ cli.assert_called_once()
161
+
162
+ def test_health_ignores_extra_args(self):
163
+ with patch.object(tools_mod, "_bridge_call", return_value={"status": "ok", "framework": "loaded", "version": "1.0", "components": {}}):
164
+ r = json.loads(tools_mod.strray_health({"x": 1}))
165
+ self.assertEqual(r["status"], "ok")
166
+
167
+
168
+ class TestStrrayValidate(unittest.TestCase):
169
+ def test_with_files_via_bridge(self):
170
+ """v2: validate uses bridge first."""
171
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "fileResults": []}) as m:
172
+ r = json.loads(tools_mod.strray_validate({"files": ["a.ts", "b.ts"], "operation": "commit"}))
173
+ self.assertEqual(r["status"], "passed")
174
+ self.assertEqual(r["files_checked"], 2)
175
+ self.assertEqual(r["via"], "bridge")
176
+ m.assert_called_once()
177
+
178
+ def test_bridge_violations(self):
179
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": False, "fileResults": [{"file": "a.ts", "passed": False, "violations": ["tests-required"]}]}) as m:
180
+ r = json.loads(tools_mod.strray_validate({"files": ["a.ts"]}))
181
+ self.assertEqual(r["status"], "violations")
182
+ self.assertEqual(r["file_results"][0]["violations"], ["tests-required"])
183
+
184
+ def test_bridge_error_fallback_to_cli(self):
185
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
186
+ with patch.object(tools_mod, "_run_strray", return_value='{"status":"ok","output":"valid"}') as cli:
187
+ r = json.loads(tools_mod.strray_validate({"files": ["a.ts"]}))
188
+ self.assertEqual(r["via"], "cli")
189
+ cli.assert_called_once()
190
+
191
+ def test_no_files_error(self):
192
+ r = json.loads(tools_mod.strray_validate({"files": []}))
193
+ self.assertIn("No files", r["error"])
194
+
195
+ def test_no_files_key_error(self):
196
+ r = json.loads(tools_mod.strray_validate({}))
197
+ self.assertIn("error", r)
198
+
199
+ def test_default_operation(self):
200
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "fileResults": []}):
201
+ r = json.loads(tools_mod.strray_validate({"files": ["a.ts"]}))
202
+ self.assertEqual(r["operation"], "commit")
203
+
204
+ def test_100_files(self):
205
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "fileResults": []}) as m:
206
+ fs = [f"f{i}.ts" for i in range(100)]
207
+ r = json.loads(tools_mod.strray_validate({"files": fs}))
208
+ self.assertEqual(r["files_checked"], 100)
209
+
210
+
211
+ class TestStrrayCodexCheck(unittest.TestCase):
212
+ def test_with_code_via_bridge(self):
213
+ """v2: codex check uses bridge for real quality gate analysis."""
214
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}) as m:
215
+ r = json.loads(tools_mod.strray_codex_check({"code": "const x = null;", "operation": "create"}))
216
+ self.assertEqual(r["status"], "passed")
217
+ self.assertEqual(r["via"], "bridge")
218
+ m.assert_called_once()
219
+
220
+ def test_with_code_bridge_violations(self):
221
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": False, "violations": ["console.log found"], "checks": []}):
222
+ r = json.loads(tools_mod.strray_codex_check({"code": "console.log(x)", "operation": "create"}))
223
+ self.assertEqual(r["status"], "violations")
224
+ self.assertEqual(r["violations"], ["console.log found"])
225
+
226
+ def test_with_focus_areas(self):
227
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}) as m:
228
+ tools_mod.strray_codex_check({"code": "eval()", "operation": "modify", "focus_areas": ["security"]})
229
+ self.assertEqual(m.call_args[0][0]["focusAreas"], ["security"])
230
+
231
+ def test_empty_string_code_treated_as_code(self):
232
+ """BUG FIX: empty string '' should still be treated as code (is not None)."""
233
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}) as m:
234
+ r = json.loads(tools_mod.strray_codex_check({"code": "", "operation": "create"}))
235
+ self.assertEqual(r["status"], "passed")
236
+ self.assertEqual(r["code_length"], 0)
237
+
238
+ def test_no_code_bridge_health(self):
239
+ """No code provided — bridge health check."""
240
+ with patch.object(tools_mod, "_bridge_call", return_value={"framework": "loaded", "version": "1.15.0", "components": {}}) as m:
241
+ r = json.loads(tools_mod.strray_codex_check({"operation": "refactor"}))
242
+ self.assertEqual(r["status"], "ok")
243
+ self.assertIn("Pass", r["note"])
244
+
245
+ def test_no_code_bridge_error_fallback(self):
246
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
247
+ with patch.object(tools_mod, "_run_strray", return_value='{"status":"ok","output":"healthy"}') as cli:
248
+ r = json.loads(tools_mod.strray_codex_check({"operation": "create"}))
249
+ cli.assert_called_once()
250
+
251
+ def test_default_operation(self):
252
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}):
253
+ r = json.loads(tools_mod.strray_codex_check({"code": "x"}))
254
+ self.assertEqual(r["operation"], "create")
255
+
256
+ def test_multiline_code(self):
257
+ code = "function foo() {\n return null;\n}\n"
258
+ with patch.object(tools_mod, "_bridge_call", return_value={"passed": True, "violations": [], "checks": []}):
259
+ r = json.loads(tools_mod.strray_codex_check({"code": code, "operation": "create"}))
260
+ self.assertEqual(r["code_length"], len(code))
261
+
262
+
263
+ class TestPreToolCallHook(unittest.TestCase):
264
+ """v2: pre_tool_call now runs bridge for code tools and logs to disk."""
265
+
266
+ def setUp(self):
267
+ # Reset session stats
268
+ pi._session_stats = dict.fromkeys(pi._session_stats, 0)
269
+ pi._session_stats["started_at"] = None
270
+ pi._session_stats["session_id"] = None
271
+
272
+ @patch.object(pi, "_call_bridge")
273
+ def test_strray_mcp_no_bridge(self, mock_bridge):
274
+ """StringRay MCP tools skip bridge entirely."""
275
+ pi._on_pre_tool_call("mcp_strray_lint_lint", {}, "t1")
276
+ self.assertEqual(pi._session_stats["strray_mcp_calls"], 1)
277
+ self.assertEqual(pi._session_stats["native_tool_calls"], 0)
278
+ mock_bridge.assert_not_called()
279
+
280
+ @patch.object(pi, "_call_bridge")
281
+ def test_native_tool_no_bridge(self, mock_bridge):
282
+ """Non-code native tools don't call bridge."""
283
+ pi._on_pre_tool_call("read_file", {"path": "a.md"}, "t1")
284
+ mock_bridge.assert_not_called()
285
+
286
+ @patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True, "violations": []}, "processors": {"ran": False}})
287
+ def test_code_tool_calls_bridge(self, mock_bridge):
288
+ """Code-producing tools trigger bridge pre-process."""
289
+ pi._on_pre_tool_call("write_file", {"path": "a.ts"}, "t1")
290
+ mock_bridge.assert_called_once()
291
+ call_cmd = mock_bridge.call_args[0][0]
292
+ self.assertEqual(call_cmd["command"], "pre-process")
293
+ self.assertEqual(call_cmd["tool"], "write_file")
294
+
295
+ @patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True, "violations": []}, "processors": {"ran": False}})
296
+ def test_code_tool_increments_stats(self, mock_bridge):
297
+ for t in ["write_file", "patch", "execute_code"]:
298
+ pi._session_stats["code_operations"] = 0
299
+ pi._session_stats["quality_gate_runs"] = 0
300
+ pi._on_pre_tool_call(t, {}, "t1")
301
+ self.assertEqual(pi._session_stats["code_operations"], 1, f"{t}")
302
+ self.assertEqual(pi._session_stats["quality_gate_runs"], 1, f"{t}")
303
+
304
+ @patch.object(pi, "_call_bridge", return_value={"passed": False, "qualityGate": {"passed": False, "violations": ["tests-required: no test"]}, "processors": {"ran": False}})
305
+ def test_quality_gate_block(self, mock_bridge):
306
+ """Quality gate failures increment block counter."""
307
+ pi._on_pre_tool_call("write_file", {"path": "a.ts"}, "t1")
308
+ self.assertEqual(pi._session_stats["quality_gate_blocks"], 1)
309
+
310
+ @patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True}, "processors": {"ran": True, "success": True, "processorCount": 2, "details": [{"name": "preValidate", "success": True}]}})
311
+ def test_pre_processor_stats(self, mock_bridge):
312
+ pi._on_pre_tool_call("write_file", {"path": "a.ts"}, "t1")
313
+ self.assertEqual(pi._session_stats["pre_processor_runs"], 1)
314
+
315
+ def test_nudge_terminal(self):
316
+ """Terminal nudge only fires when command matches a known pattern."""
317
+ # No command arg — no nudge
318
+ with self.assertRaises(AssertionError):
319
+ with self.assertLogs("strray-hermes", level="INFO"):
320
+ pi._on_pre_tool_call("terminal", {}, "t1")
321
+
322
+ # Generic command (git, ls) — no nudge
323
+ with self.assertRaises(AssertionError):
324
+ with self.assertLogs("strray-hermes", level="INFO"):
325
+ pi._on_pre_tool_call("terminal", {"command": "git status"}, "t1")
326
+
327
+ # Grep command — should nudge
328
+ with self.assertLogs("strray-hermes", level="INFO") as cm:
329
+ pi._on_pre_tool_call("terminal", {"command": "grep -r 'pattern' src/"}, "t1")
330
+ self.assertTrue(any("grep" in m for m in cm.output))
331
+
332
+ # npm audit — should nudge
333
+ with self.assertLogs("strray-hermes", level="INFO") as cm:
334
+ pi._on_pre_tool_call("terminal", {"command": "npm audit"}, "t1")
335
+ self.assertTrue(any("audit" in m for m in cm.output))
336
+
337
+ def test_nudge_search_files(self):
338
+ with self.assertLogs("strray-hermes", level="INFO") as cm:
339
+ pi._on_pre_tool_call("search_files", {}, "t1")
340
+ self.assertTrue(any("Tip" in m for m in cm.output))
341
+
342
+ def test_no_nudge_write_file(self):
343
+ """write_file is a code tool — no nudge, gets bridge instead."""
344
+ with patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True}, "processors": {"ran": False}}):
345
+ with self.assertRaises(AssertionError):
346
+ with self.assertLogs("strray-hermes", level="INFO"):
347
+ pi._on_pre_tool_call("write_file", {}, "t1")
348
+
349
+ def test_accumulates(self):
350
+ with patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True}, "processors": {"ran": False}}):
351
+ for _ in range(5):
352
+ pi._on_pre_tool_call("terminal", {}, "t1")
353
+ self.assertEqual(pi._session_stats["total_tool_calls"], 5)
354
+
355
+ def test_is_strray_mcp(self):
356
+ self.assertTrue(pi._is_strray_mcp("mcp_strray_lint_lint"))
357
+ self.assertTrue(pi._is_strray_mcp("mcp_strray_enforcer_codex_enforcement"))
358
+ self.assertFalse(pi._is_strray_mcp("terminal"))
359
+ self.assertFalse(pi._is_strray_mcp("strray_validate"))
360
+ self.assertFalse(pi._is_strray_mcp("mcp_other_tool"))
361
+
362
+
363
+ class TestPostToolCallHook(unittest.TestCase):
364
+ """v2: post_tool_call logs to disk and calls bridge for code tools."""
365
+
366
+ def setUp(self):
367
+ # Reset post_processor_runs since it accumulates across tests
368
+ pi._session_stats["post_processor_runs"] = 0
369
+
370
+ @patch.object(pi, "_call_bridge")
371
+ def test_non_code_no_bridge(self, mock_bridge):
372
+ """Non-code tools don't trigger bridge post-process."""
373
+ pi._on_post_tool_call("terminal", {"command": "ls"}, None, "t1")
374
+ mock_bridge.assert_not_called()
375
+
376
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": True, "success": True, "processorCount": 2, "details": []}})
377
+ def test_code_tool_calls_bridge(self, mock_bridge):
378
+ """Code-producing tools trigger bridge post-process."""
379
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, None, "t1")
380
+ mock_bridge.assert_called_once()
381
+ call_cmd = mock_bridge.call_args[0][0]
382
+ self.assertEqual(call_cmd["command"], "post-process")
383
+ self.assertEqual(call_cmd["tool"], "write_file")
384
+
385
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": True, "success": True, "processorCount": 1, "details": []}})
386
+ def test_post_processor_stats(self, mock_bridge):
387
+ pi._on_post_tool_call("patch", {"path": "a.ts"}, None, "t1")
388
+ self.assertEqual(pi._session_stats["post_processor_runs"], 1)
389
+
390
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": True, "success": True, "processorCount": 2, "details": []}})
391
+ def test_post_captures_result(self, mock_bridge):
392
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, {"error": "disk full"}, "t1")
393
+ call_cmd = mock_bridge.call_args[0][0]
394
+ self.assertEqual(call_cmd["error"], "disk full")
395
+
396
+ def test_args_not_dict_no_crash(self):
397
+ pi._on_post_tool_call("write_file", "not a dict", None, "t1")
398
+
399
+ def test_args_none_no_crash(self):
400
+ pi._on_post_tool_call("write_file", None, None, "t1")
401
+
402
+ def test_missing_path_key_no_file_tracking(self):
403
+ """BUG FIX: missing path key should not crash."""
404
+ pi._on_post_tool_call("write_file", {"content": "hello"}, None, "t1")
405
+
406
+ def test_empty_path_no_file_tracking(self):
407
+ """Empty string path should not crash."""
408
+ pi._on_post_tool_call("write_file", {"path": ""}, None, "t1")
409
+
410
+
411
+ class TestSlashCommand(unittest.TestCase):
412
+ def setUp(self):
413
+ pi._session_stats = {
414
+ "started_at": "2026-03-27T15:00:00Z",
415
+ "session_id": "test-session",
416
+ "code_operations": 5,
417
+ "total_tool_calls": 20,
418
+ "strray_mcp_calls": 12,
419
+ "native_tool_calls": 8,
420
+ "quality_gate_runs": 10,
421
+ "quality_gate_blocks": 2,
422
+ "pre_processor_runs": 8,
423
+ "post_processor_runs": 6,
424
+ "bridge_calls": 15,
425
+ "bridge_errors": 0,
426
+ "subagent_dispatches": 3,
427
+ "subagent_validations": 1,
428
+ "subagent_blocks": 0,
429
+ }
430
+
431
+ def test_stats(self):
432
+ o = pi._strray_command("stats")
433
+ self.assertIn("20", o)
434
+ self.assertIn("12", o)
435
+ self.assertIn("8", o)
436
+ self.assertIn("5", o)
437
+ # v2 new stats
438
+ self.assertIn("10", o) # quality_gate_runs
439
+ self.assertIn("2", o) # quality_gate_blocks
440
+ self.assertIn("15", o) # bridge_calls
441
+
442
+ def test_help(self):
443
+ o = pi._strray_command("help")
444
+ self.assertIn("status", o)
445
+ self.assertIn("help", o)
446
+
447
+ def test_status_via_bridge(self):
448
+ """v2: status uses bridge, not tools.strray_health."""
449
+ with patch.object(pi, "_call_bridge", return_value={"framework": "loaded", "version": "1.15.0", "components": {"qualityGate": True, "processorManager": True}}) as m:
450
+ o = pi._strray_command("status")
451
+ self.assertIn("loaded", o)
452
+ self.assertIn("1.15.0", o)
453
+ m.assert_called_once_with({"command": "health"}, timeout=10)
454
+
455
+ def test_status_bridge_error(self):
456
+ with patch.object(pi, "_call_bridge", return_value={"error": "node not found"}):
457
+ o = pi._strray_command("status")
458
+ self.assertIn("node not found", o)
459
+
460
+ def test_default_status(self):
461
+ with patch.object(pi, "_call_bridge", return_value={"framework": "loaded", "version": "1.0", "components": {}}):
462
+ o = pi._strray_command("")
463
+ self.assertIn("loaded", o)
464
+
465
+ def test_stats_null_started_at(self):
466
+ pi._session_stats["started_at"] = None
467
+ pi._session_stats["session_id"] = None
468
+ o = pi._strray_command("stats")
469
+ self.assertIn("N/A", o)
470
+
471
+
472
+ class TestSessionStartHook(unittest.TestCase):
473
+ def test_resets_stats(self):
474
+ pi._session_stats["total_tool_calls"] = 99
475
+ pi._session_stats["code_operations"] = 50
476
+ pi._session_stats["bridge_calls"] = 20
477
+ pi._session_stats["quality_gate_blocks"] = 5
478
+ pi._on_session_start("s1", "cli")
479
+ self.assertEqual(pi._session_stats["total_tool_calls"], 0)
480
+ self.assertEqual(pi._session_stats["code_operations"], 0)
481
+ self.assertEqual(pi._session_stats["bridge_calls"], 0)
482
+ self.assertEqual(pi._session_stats["quality_gate_blocks"], 0)
483
+ self.assertIsNotNone(pi._session_stats["started_at"])
484
+ self.assertEqual(pi._session_stats["session_id"], "s1")
485
+
486
+ def test_logs(self):
487
+ with self.assertLogs("strray-hermes", level="INFO") as cm:
488
+ pi._on_session_start("s1", "telegram")
489
+ self.assertTrue(any("s1" in m for m in cm.output))
490
+ self.assertTrue(any("telegram" in m for m in cm.output))
491
+
492
+
493
+ class TestRegisterIntegration(unittest.TestCase):
494
+ def test_wires_three_tools(self):
495
+ ctx = MagicMock()
496
+ pi.register(ctx)
497
+ names = [c[1]["name"] for c in ctx.register_tool.call_args_list]
498
+ self.assertEqual(set(names), {"strray_validate", "strray_codex_check", "strray_health", "strray_hooks"})
499
+
500
+ def test_toolset_name(self):
501
+ ctx = MagicMock()
502
+ pi.register(ctx)
503
+ for c in ctx.register_tool.call_args_list:
504
+ self.assertEqual(c[1]["toolset"], "strray-hermes")
505
+
506
+ def test_schemas_wired(self):
507
+ ctx = MagicMock()
508
+ pi.register(ctx)
509
+ sm = {c[1]["name"]: c[1]["schema"] for c in ctx.register_tool.call_args_list}
510
+ self.assertIs(sm["strray_validate"], schemas.STRRAY_VALIDATE)
511
+ self.assertIs(sm["strray_codex_check"], schemas.STRRAY_CODEX_CHECK)
512
+ self.assertIs(sm["strray_health"], schemas.STRRAY_HEALTH)
513
+
514
+ def test_handlers_wired(self):
515
+ ctx = MagicMock()
516
+ pi.register(ctx)
517
+ hm = {c[1]["name"]: c[1]["handler"] for c in ctx.register_tool.call_args_list}
518
+ self.assertIs(hm["strray_validate"], tools_mod.strray_validate)
519
+ self.assertIs(hm["strray_codex_check"], tools_mod.strray_codex_check)
520
+ self.assertIs(hm["strray_health"], tools_mod.strray_health)
521
+
522
+ def test_hooks_registered(self):
523
+ ctx = MagicMock()
524
+ pi.register(ctx)
525
+ names = [c[0][0] for c in ctx.register_hook.call_args_list]
526
+ self.assertIn("pre_tool_call", names)
527
+ self.assertIn("post_tool_call", names)
528
+
529
+ def test_hooks_are_callable(self):
530
+ ctx = MagicMock()
531
+ pi.register(ctx)
532
+ for c in ctx.register_hook.call_args_list:
533
+ self.assertTrue(callable(c[0][1]))
534
+
535
+ def test_slash_command_attempted(self):
536
+ ctx = MagicMock()
537
+ pi.register(ctx)
538
+ cmds = [c for c in ctx.method_calls if "register_command" in str(c)]
539
+ self.assertTrue(len(cmds) >= 1)
540
+
541
+ def test_survives_missing_session_hook(self):
542
+ ctx = MagicMock()
543
+ # pre_tool_call, post_tool_call succeed; on_session_start fails; 3 lifecycle hooks fail
544
+ ctx.register_hook.side_effect = [None, None, AttributeError("nope"), AttributeError("nope"), AttributeError("nope"), AttributeError("nope")]
545
+ pi.register(ctx) # should not raise
546
+
547
+ def test_survives_missing_command_reg(self):
548
+ ctx = MagicMock()
549
+ ctx.register_command.side_effect = TypeError("nope")
550
+ pi.register(ctx)
551
+
552
+ def test_logs_on_load(self):
553
+ ctx = MagicMock()
554
+ with self.assertLogs("strray-hermes", level="INFO") as cm:
555
+ pi.register(ctx)
556
+ self.assertTrue(any("Plugin" in m and "loaded" in m for m in cm.output))
557
+
558
+
559
+ class TestFileLogging(unittest.TestCase):
560
+ """Test that tool events are written to log files."""
561
+
562
+ def test_log_tool_event_creates_file(self):
563
+ with tempfile.TemporaryDirectory() as td:
564
+ log_dir = Path(td)
565
+ # Temporarily override LOG_DIR
566
+ original = pi.LOG_DIR
567
+ pi.LOG_DIR = log_dir
568
+
569
+ pi._log_tool_event("start", "terminal", {"command": "ls"})
570
+ pi._log_tool_event("complete", "terminal", {"command": "ls"}, duration=100)
571
+
572
+ activity_file = log_dir / "plugin-tool-events.log"
573
+ self.assertTrue(activity_file.exists())
574
+
575
+ content = activity_file.read_text()
576
+ self.assertIn("tool-started", content)
577
+ self.assertIn("tool-complete", content)
578
+ self.assertIn("SUCCESS", content)
579
+
580
+ pi.LOG_DIR = original
581
+
582
+ def test_log_to_file_creates_file(self):
583
+ with tempfile.TemporaryDirectory() as td:
584
+ log_dir = Path(td)
585
+ original = pi.LOG_DIR
586
+ pi.LOG_DIR = log_dir
587
+
588
+ pi._log_to_file("activity.log", "[test] hello world")
589
+
590
+ activity_file = log_dir / "activity.log"
591
+ self.assertTrue(activity_file.exists())
592
+ content = activity_file.read_text()
593
+ self.assertIn("[test] hello world", content)
594
+
595
+ pi.LOG_DIR = original
596
+
597
+ def test_log_to_file_survives_permission_error(self):
598
+ """Should never crash the agent over logging."""
599
+ original = pi.LOG_DIR
600
+ pi.LOG_DIR = Path("/nonexistent/path/that/does/not/exist/and/cannot/be/created")
601
+ pi._log_to_file("activity.log", "should not crash") # noqa: B023
602
+ pi.LOG_DIR = original
603
+
604
+
605
+ class TestLiveBridge(unittest.TestCase):
606
+ """Live test: actually call bridge.mjs if it exists."""
607
+
608
+ def test_bridge_health(self):
609
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
610
+ if not bridge_path.exists():
611
+ self.skipTest("bridge.mjs not built yet")
612
+
613
+ r = tools_mod._bridge_call({"command": "health"}, timeout=10)
614
+ self.assertNotIn("error", r)
615
+ self.assertIn("status", r)
616
+
617
+ def test_bridge_stats(self):
618
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
619
+ if not bridge_path.exists():
620
+ self.skipTest("bridge.mjs not built yet")
621
+
622
+ r = tools_mod._bridge_call({"command": "stats"}, timeout=5)
623
+ self.assertIn("frameworkReady", r)
624
+
625
+ def test_bridge_quality_gate(self):
626
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
627
+ if not bridge_path.exists():
628
+ self.skipTest("bridge.mjs not built yet")
629
+
630
+ # Clean code should pass
631
+ r = tools_mod._bridge_call({
632
+ "command": "codex-check",
633
+ "code": "const x: number = 42;",
634
+ }, timeout=10)
635
+ self.assertNotIn("error", r)
636
+ self.assertIn("passed", r)
637
+
638
+ def test_bridge_quality_gate_violation(self):
639
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
640
+ if not bridge_path.exists():
641
+ self.skipTest("bridge.mjs not built yet")
642
+
643
+ # Code with console.log should fail
644
+ r = tools_mod._bridge_call({
645
+ "command": "codex-check",
646
+ "code": "console.log('hello');",
647
+ }, timeout=10)
648
+ self.assertNotIn("error", r)
649
+ # console.log is a violation
650
+ if r.get("passed") is False:
651
+ self.assertTrue(any("console.log" in v for v in r.get("violations", [])))
652
+
653
+ def test_bridge_positional_health(self):
654
+ """Positional arg mode: node bridge.mjs health --cwd /path (no stdin pipe)."""
655
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
656
+ if not bridge_path.exists():
657
+ self.skipTest("bridge.mjs not built yet")
658
+
659
+ # Use stringray dev repo as project root (has package.json)
660
+ strray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "stringray")
661
+ cwd = strray_root if Path(strray_root).exists() else PLUGIN_DIR
662
+
663
+ r = subprocess.run(
664
+ ["node", str(bridge_path), "health", "--cwd", cwd],
665
+ capture_output=True, text=True, timeout=10,
666
+ )
667
+ self.assertEqual(r.returncode, 0, f"stderr: {r.stderr}")
668
+ data = json.loads(r.stdout)
669
+ self.assertEqual(data["status"], "ok")
670
+ self.assertIn("framework", data)
671
+
672
+ def test_bridge_positional_stats(self):
673
+ """Positional stats: no stdin needed."""
674
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
675
+ if not bridge_path.exists():
676
+ self.skipTest("bridge.mjs not built yet")
677
+
678
+ strray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "stringray")
679
+ cwd = strray_root if Path(strray_root).exists() else PLUGIN_DIR
680
+
681
+ r = subprocess.run(
682
+ ["node", str(bridge_path), "stats", "--cwd", cwd],
683
+ capture_output=True, text=True, timeout=10,
684
+ )
685
+ self.assertEqual(r.returncode, 0, f"stderr: {r.stderr}")
686
+ data = json.loads(r.stdout)
687
+ self.assertIn("frameworkReady", data)
688
+
689
+ def test_bridge_positional_with_json_payload(self):
690
+ """Positional mode with --json payload for commands needing args."""
691
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
692
+ if not bridge_path.exists():
693
+ self.skipTest("bridge.mjs not built yet")
694
+
695
+ strray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "stringray")
696
+ cwd = strray_root if Path(strray_root).exists() else PLUGIN_DIR
697
+
698
+ r = subprocess.run(
699
+ ["node", str(bridge_path), "validate", "--cwd", cwd,
700
+ "--json", json.dumps({"files": ["src/index.ts"], "operation": "commit"})],
701
+ capture_output=True, text=True, timeout=15,
702
+ )
703
+ self.assertEqual(r.returncode, 0, f"stderr: {r.stderr}")
704
+ data = json.loads(r.stdout)
705
+ self.assertIn("passed", data)
706
+
707
+ def test_bridge_positional_invalid_json(self):
708
+ """Positional mode with invalid --json returns error."""
709
+ bridge_path = Path(PLUGIN_DIR) / "bridge.mjs"
710
+ if not bridge_path.exists():
711
+ self.skipTest("bridge.mjs not built yet")
712
+
713
+ strray_root = str(Path(PLUGIN_DIR).parent.parent.parent / "dev" / "stringray")
714
+ cwd = strray_root if Path(strray_root).exists() else PLUGIN_DIR
715
+
716
+ r = subprocess.run(
717
+ ["node", str(bridge_path), "validate", "--cwd", cwd,
718
+ "--json", "not-json"],
719
+ capture_output=True, text=True, timeout=10,
720
+ )
721
+ # Should fail with error about invalid payload
722
+ data = json.loads(r.stdout)
723
+ self.assertIn("error", data)
724
+
725
+
726
+ class TestBridgeErrorPaths(unittest.TestCase):
727
+ """Cover remaining bridge error branches in tools.py."""
728
+
729
+ def test_bridge_json_decode_error(self):
730
+ """_bridge_call with non-JSON stdout returns error."""
731
+ with patch("subprocess.run") as m:
732
+ m.return_value = MagicMock(returncode=0, stdout="not json", stderr="")
733
+ r = tools_mod._bridge_call({"command": "health"})
734
+ self.assertIn("error", r)
735
+
736
+ def test_bridge_os_error(self):
737
+ """_bridge_call with OSError during subprocess returns error."""
738
+ with patch("subprocess.run", side_effect=OSError("broken pipe")):
739
+ r = tools_mod._bridge_call({"command": "health"})
740
+ self.assertIn("error", r)
741
+
742
+ def test_bridge_generic_exception_in_run_strray(self):
743
+ """_run_strray catches non-standard exceptions."""
744
+ with patch("subprocess.run", side_effect=RuntimeError("unexpected")):
745
+ r = json.loads(tools_mod._run_strray(["health"]))
746
+ self.assertIn("unexpected", r["error"])
747
+
748
+ def test_validate_cli_fallback_error(self):
749
+ """strray_validate CLI fallback when CLI returns an error JSON."""
750
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "bridge down"}):
751
+ with patch.object(tools_mod, "_run_strray", return_value='{"error": "validation failed"}'):
752
+ r = json.loads(tools_mod.strray_validate({"files": ["a.ts"]}))
753
+ # CLI error path returns raw result without "via" key
754
+ self.assertIn("error", r)
755
+
756
+ def test_codex_check_static_fallback(self):
757
+ """strray_codex_check with code but bridge down falls back to static analysis."""
758
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "bridge down"}):
759
+ r = json.loads(tools_mod.strray_codex_check({"code": "console.log(1)", "operation": "create"}))
760
+ self.assertEqual(r["via"], "static")
761
+ self.assertIn("basic analysis", r["note"])
762
+
763
+ def test_codex_check_cli_health_error(self):
764
+ """strray_codex_check no-code path: bridge error + CLI also errors."""
765
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "no node"}):
766
+ with patch.object(tools_mod, "_run_strray", return_value='{"error": "0xray not found"}'):
767
+ r = json.loads(tools_mod.strray_codex_check({"operation": "create"}))
768
+ self.assertIn("error", r)
769
+
770
+
771
+ class TestGetProjectRoot(unittest.TestCase):
772
+ """Test the _get_project_root helper in tools.py."""
773
+
774
+ def test_returns_cwd_when_no_package_json(self):
775
+ """With no package.json in any ancestor, falls back to cwd."""
776
+ # _get_project_root delegates to __init__.py's PROJECT_ROOT.
777
+ # We verify the function exists and is callable.
778
+ self.assertTrue(callable(tools_mod._get_project_root))
779
+ # Verify it returns a Path-like value
780
+ result = tools_mod._get_project_root()
781
+ self.assertIsNotNone(result)
782
+
783
+
784
+ class TestPreToolCallBridgeErrors(unittest.TestCase):
785
+ """Test pre_tool_call hook when bridge returns errors."""
786
+
787
+ def setUp(self):
788
+ pi._session_stats = dict.fromkeys(pi._session_stats, 0)
789
+ pi._session_stats["started_at"] = None
790
+ pi._session_stats["session_id"] = None
791
+
792
+ @patch.object(pi, "_call_bridge", return_value={"error": "bridge crashed"})
793
+ def test_code_tool_bridge_error_does_not_crash(self, mock_bridge):
794
+ """Bridge error during pre-process should not crash the hook."""
795
+ pi._on_pre_tool_call("write_file", {"path": "a.ts"}, "t1")
796
+ self.assertEqual(pi._session_stats["code_operations"], 1)
797
+ # Note: bridge_calls stat is inside the real _bridge_call,
798
+ # so mocking it doesn't increment the counter. Verify hook doesn't crash.
799
+ mock_bridge.assert_called_once()
800
+
801
+ @patch.object(pi, "_call_bridge", return_value={"error": "timeout"})
802
+ def test_multiple_code_tools_with_bridge_errors(self, mock_bridge):
803
+ """Multiple bridge errors accumulate properly."""
804
+ for i in range(3):
805
+ pi._on_pre_tool_call("write_file", {"path": f"f{i}.ts"}, "t1")
806
+ self.assertEqual(pi._session_stats["code_operations"], 3)
807
+ self.assertEqual(pi._session_stats["total_tool_calls"], 3)
808
+
809
+
810
+ class TestPostToolCallBridgeErrors(unittest.TestCase):
811
+ """Test post_tool_call hook when bridge returns errors."""
812
+
813
+ def setUp(self):
814
+ pi._session_stats["post_processor_runs"] = 0
815
+
816
+ @patch.object(pi, "_call_bridge", return_value={"error": "bridge down"})
817
+ def test_code_tool_post_bridge_error(self, mock_bridge):
818
+ """Bridge error during post-process should not crash."""
819
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, None, "t1")
820
+ mock_bridge.assert_called_once()
821
+
822
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": False}})
823
+ def test_code_tool_processors_not_ran(self, mock_bridge):
824
+ """Processors not running is handled gracefully."""
825
+ pi._on_post_tool_call("execute_code", {"command": "echo hi"}, {"duration": 42}, "t1")
826
+ self.assertEqual(pi._session_stats["post_processor_runs"], 0)
827
+
828
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": True, "success": False, "processorCount": 1, "details": [{"name": "testAutoCreation", "success": False, "error": "no test file"}]}})
829
+ def test_post_processor_failure_logging(self, mock_bridge):
830
+ """Failed post-processors are tracked but don't crash."""
831
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, None, "t1")
832
+ self.assertEqual(pi._session_stats["post_processor_runs"], 1)
833
+
834
+
835
+ class TestPreToolCallEdgeCases(unittest.TestCase):
836
+ """Edge cases for pre_tool_call."""
837
+
838
+ def setUp(self):
839
+ pi._session_stats = dict.fromkeys(pi._session_stats, 0)
840
+ pi._session_stats["started_at"] = None
841
+ pi._session_stats["session_id"] = None
842
+
843
+ @patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True}, "processors": {"ran": False}})
844
+ def test_all_code_tools_trigger_bridge(self, mock_bridge):
845
+ """Every tool in _CODE_TOOLS calls the bridge."""
846
+ code_tools = ["write_file", "patch", "execute_code", "write", "edit"]
847
+ for tool in code_tools:
848
+ pi._session_stats["code_operations"] = 0
849
+ pi._on_pre_tool_call(tool, {}, "t1")
850
+ self.assertEqual(pi._session_stats["code_operations"], 1, f"{tool} should be a code tool")
851
+
852
+ @patch.object(pi, "_call_bridge")
853
+ def test_unknown_tool_not_strray_mcp(self, mock_bridge):
854
+ """Unknown tools should be treated as native tools."""
855
+ pi._on_pre_tool_call("some_random_tool", {}, "t1")
856
+ self.assertEqual(pi._session_stats["native_tool_calls"], 1)
857
+ mock_bridge.assert_not_called()
858
+
859
+ def test_strray_validate_tool_not_treated_as_mcp(self):
860
+ """strray_validate is a native tool, not an MCP tool."""
861
+ pi._on_pre_tool_call("strray_validate", {}, "t1")
862
+ self.assertEqual(pi._session_stats["native_tool_calls"], 1)
863
+ self.assertEqual(pi._session_stats["strray_mcp_calls"], 0)
864
+
865
+ @patch.object(pi, "_call_bridge", return_value={"passed": True, "qualityGate": {"passed": True, "violations": []}, "processors": {"ran": True, "success": True, "processorCount": 3, "details": [{"name": "p1", "success": True}, {"name": "p2", "success": True}, {"name": "p3", "success": False, "error": "failed"}]}})
866
+ def test_pre_processor_partial_failure(self, mock_bridge):
867
+ """Pre-processors with partial failure still count as ran."""
868
+ pi._on_pre_tool_call("write_file", {"path": "a.ts"}, "t1")
869
+ self.assertEqual(pi._session_stats["pre_processor_runs"], 1)
870
+
871
+
872
+ class TestSlashCommandEdgeCases(unittest.TestCase):
873
+ """Edge cases for the slash command handler."""
874
+
875
+ def test_unknown_command_defaults_to_status(self):
876
+ """Unknown args default to status."""
877
+ with patch.object(pi, "_call_bridge", return_value={"framework": "loaded", "version": "1.0", "components": {}}) as m:
878
+ pi._strray_command("something-random")
879
+ m.assert_called_once_with({"command": "health"}, timeout=10)
880
+
881
+ def test_case_insensitive(self):
882
+ """Command arg is lowercased."""
883
+ o = pi._strray_command(" STATS ")
884
+ self.assertIn("Session", o)
885
+
886
+
887
+ class TestLogToFileTimestamps(unittest.TestCase):
888
+ """Verify log file formatting."""
889
+
890
+ def test_log_entry_has_iso_timestamp(self):
891
+ """Every log entry should start with an ISO timestamp."""
892
+ import re
893
+ with tempfile.TemporaryDirectory() as td:
894
+ log_dir = Path(td)
895
+ original = pi.LOG_DIR
896
+ pi.LOG_DIR = log_dir
897
+
898
+ pi._log_to_file("test.log", "[test] message")
899
+ content = (log_dir / "test.log").read_text()
900
+ # ISO timestamp: 2026-03-27T17:00:00Z or similar
901
+ self.assertTrue(re.match(r"\d{4}-\d{2}-\d{2}T", content.split(" ")[0]))
902
+
903
+ pi.LOG_DIR = original
904
+
905
+
906
+ class TestPostToolCallDuration(unittest.TestCase):
907
+ """Test that duration is correctly extracted from results."""
908
+
909
+ def setUp(self):
910
+ pi._session_stats["post_processor_runs"] = 0
911
+
912
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": False}})
913
+ def test_duration_extracted_from_result_dict(self, mock_bridge):
914
+ """Duration from result dict is logged correctly."""
915
+ # We verify the post hook doesn't crash with duration in result
916
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, {"duration": 1234, "success": True}, "t1")
917
+ # No crash = pass
918
+
919
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": False}})
920
+ def test_non_dict_result_no_crash(self, mock_bridge):
921
+ """Non-dict result doesn't crash the hook."""
922
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, "string result", "t1")
923
+
924
+ @patch.object(pi, "_call_bridge", return_value={"processors": {"ran": False}})
925
+ def test_none_result_no_crash(self, mock_bridge):
926
+ """None result doesn't crash the hook."""
927
+ pi._on_post_tool_call("write_file", {"path": "a.ts"}, None, "t1")
928
+
929
+
930
+ class TestBridgeHelperTimeoutDefault(unittest.TestCase):
931
+ """Verify bridge timeout defaults."""
932
+
933
+ def test_default_timeout(self):
934
+ """_bridge_call defaults to 30s timeout."""
935
+ with patch("subprocess.run") as m:
936
+ m.return_value = MagicMock(returncode=0, stdout='{"ok":true}', stderr="")
937
+ tools_mod._bridge_call({"command": "health"})
938
+ self.assertEqual(m.call_args[1]["timeout"], 30)
939
+
940
+ def test_custom_timeout(self):
941
+ """_bridge_call respects custom timeout."""
942
+ with patch("subprocess.run") as m:
943
+ m.return_value = MagicMock(returncode=0, stdout='{\"ok\":true}', stderr="")
944
+ tools_mod._bridge_call({"command": "health"}, timeout=5)
945
+ self.assertEqual(m.call_args[1]["timeout"], 5)
946
+
947
+
948
+ class TestStrrayHooksTool(unittest.TestCase):
949
+ """Tests for the strray_hooks tool."""
950
+
951
+ def test_list_via_bridge(self):
952
+ """list action uses bridge when available."""
953
+ with patch.object(tools_mod, "_bridge_call", return_value={
954
+ "status": "ok", "action": "list",
955
+ "hooks": {"managed": ["pre-commit"], "missing": [], "external": [], "stale": []},
956
+ }) as m:
957
+ r = json.loads(tools_mod.strray_hooks({"action": "list"}))
958
+ self.assertEqual(r["status"], "ok")
959
+ self.assertEqual(r["via"], "bridge")
960
+ m.assert_called_once()
961
+ call_cmd = m.call_args[0][0]
962
+ self.assertEqual(call_cmd["command"], "hooks")
963
+ self.assertEqual(call_cmd["action"], "list")
964
+
965
+ def test_install_via_bridge(self):
966
+ """install action uses bridge."""
967
+ with patch.object(tools_mod, "_bridge_call", return_value={
968
+ "status": "ok", "action": "install", "installed": ["pre-commit", "post-commit"],
969
+ "skipped": [], "errors": [],
970
+ }) as m:
971
+ r = json.loads(tools_mod.strray_hooks({"action": "install"}))
972
+ self.assertEqual(r["via"], "bridge")
973
+ self.assertEqual(len(r["result"]["installed"]), 2)
974
+
975
+ def test_uninstall_via_bridge(self):
976
+ """uninstall action uses bridge."""
977
+ with patch.object(tools_mod, "_bridge_call", return_value={
978
+ "status": "ok", "action": "uninstall", "removed": ["pre-commit"], "restored": [],
979
+ }) as m:
980
+ r = json.loads(tools_mod.strray_hooks({"action": "uninstall"}))
981
+ self.assertEqual(r["via"], "bridge")
982
+
983
+ def test_bridge_error_fallback(self):
984
+ """Falls back to file-based when bridge errors."""
985
+ with patch.object(tools_mod, "_bridge_call", return_value={"error": "node not found"}):
986
+ # Without a real git repo, should return error
987
+ r = json.loads(tools_mod.strray_hooks({"action": "list"}))
988
+ self.assertIn("via", r)
989
+
990
+ def test_specific_hooks(self):
991
+ """Can request specific hooks."""
992
+ with patch.object(tools_mod, "_bridge_call", return_value={
993
+ "status": "ok", "action": "list",
994
+ "hooks": {"managed": [], "missing": ["pre-commit"], "external": [], "stale": []},
995
+ }) as m:
996
+ tools_mod.strray_hooks({"action": "list", "hooks": ["pre-commit"]})
997
+ call_cmd = m.call_args[0][0]
998
+ self.assertEqual(call_cmd["hooks"], ["pre-commit"])
999
+
1000
+ def test_status_defaults_to_list(self):
1001
+ """status action works like list."""
1002
+ with patch.object(tools_mod, "_bridge_call", return_value={
1003
+ "status": "ok", "action": "status",
1004
+ "hooks": {"managed": [], "missing": [], "external": [], "stale": []},
1005
+ }) as m:
1006
+ r = json.loads(tools_mod.strray_hooks({"action": "status"}))
1007
+ self.assertEqual(r["status"], "ok")
1008
+ m.assert_called_once()
1009
+
1010
+ def test_default_action_is_list(self):
1011
+ """Missing action defaults to list."""
1012
+ with patch.object(tools_mod, "_bridge_call", return_value={
1013
+ "status": "ok", "action": "list",
1014
+ "hooks": {"managed": [], "missing": [], "external": [], "stale": []},
1015
+ }) as m:
1016
+ tools_mod.strray_hooks({})
1017
+ call_cmd = m.call_args[0][0]
1018
+ self.assertEqual(call_cmd["action"], "list")
1019
+
1020
+
1021
+ class TestStrrayHooksSchema(unittest.TestCase):
1022
+ """Tests for the STRRAY_HOOKS schema."""
1023
+
1024
+ def test_schema_has_required_fields(self):
1025
+ s = schemas.STRRAY_HOOKS
1026
+ self.assertEqual(s["name"], "strray_hooks")
1027
+ self.assertIn("action", s["parameters"]["properties"])
1028
+ self.assertIn("hooks", s["parameters"]["properties"])
1029
+ self.assertIn("action", s["parameters"]["required"])
1030
+
1031
+ def test_action_enum(self):
1032
+ s = schemas.STRRAY_HOOKS
1033
+ action = s["parameters"]["properties"]["action"]
1034
+ self.assertIn("install", action["enum"])
1035
+ self.assertIn("uninstall", action["enum"])
1036
+ self.assertIn("list", action["enum"])
1037
+ self.assertIn("status", action["enum"])
1038
+
1039
+ def test_hooks_enum(self):
1040
+ s = schemas.STRRAY_HOOKS
1041
+ hooks = s["parameters"]["properties"]["hooks"]
1042
+ self.assertIn("pre-commit", hooks["items"]["enum"])
1043
+ self.assertIn("post-commit", hooks["items"]["enum"])
1044
+ self.assertIn("pre-push", hooks["items"]["enum"])
1045
+ self.assertIn("post-push", hooks["items"]["enum"])
1046
+
1047
+ def test_description_mentions_hooks(self):
1048
+ s = schemas.STRRAY_HOOKS
1049
+ self.assertIn("git hooks", s["description"])
1050
+
1051
+
1052
+ class TestRegisterIntegrationV2_1(unittest.TestCase):
1053
+ """Test that register() wires all 4 tools and 2 hooks in v2.2."""
1054
+
1055
+ def test_wires_four_tools(self):
1056
+ ctx = MagicMock()
1057
+ pi.register(ctx)
1058
+ names = [c[1]["name"] for c in ctx.register_tool.call_args_list]
1059
+ self.assertEqual(set(names), {
1060
+ "strray_validate", "strray_codex_check",
1061
+ "strray_health", "strray_hooks",
1062
+ })
1063
+
1064
+ def test_strray_hooks_schema_wired(self):
1065
+ ctx = MagicMock()
1066
+ pi.register(ctx)
1067
+ sm = {c[1]["name"]: c[1]["schema"] for c in ctx.register_tool.call_args_list}
1068
+ self.assertIs(sm["strray_hooks"], schemas.STRRAY_HOOKS)
1069
+
1070
+ def test_strray_hooks_handler_wired(self):
1071
+ ctx = MagicMock()
1072
+ pi.register(ctx)
1073
+ hm = {c[1]["name"]: c[1]["handler"] for c in ctx.register_tool.call_args_list}
1074
+ self.assertIs(hm["strray_hooks"], tools_mod.strray_hooks)
1075
+
1076
+ def test_registers_two_hooks(self):
1077
+ ctx = MagicMock()
1078
+ pi.register(ctx)
1079
+ hook_names = [c[0][0] for c in ctx.register_hook.call_args_list]
1080
+ self.assertIn("pre_tool_call", hook_names)
1081
+ self.assertIn("post_tool_call", hook_names)
1082
+
1083
+ def test_survives_missing_session_hook(self):
1084
+ """Session hook should fail gracefully if not supported."""
1085
+ ctx = MagicMock()
1086
+ def side_effect(*args):
1087
+ raise AttributeError("not available")
1088
+ ctx.register_hook.side_effect = [None, None, side_effect]
1089
+ pi.register(ctx) # should not raise
1090
+
1091
+ def test_v2_2_log_message(self):
1092
+ ctx = MagicMock()
1093
+ with self.assertLogs("strray-hermes", level="INFO") as cm:
1094
+ pi.register(ctx)
1095
+ self.assertTrue(any("v2.2" in m for m in cm.output))
1096
+ self.assertTrue(any("4 tools" in m for m in cm.output))
1097
+
1098
+
1099
+ if __name__ == "__main__":
1100
+ unittest.main(verbosity=2)