@trac3r/oh-my-god 2.2.11

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 (638) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/INSTALL-VERIFICATION-INDEX.md +51 -0
  3. package/LICENSE +21 -0
  4. package/OMG-setup.sh +2549 -0
  5. package/QUICK-REFERENCE.md +58 -0
  6. package/README.md +207 -0
  7. package/agents/__init__.py +1 -0
  8. package/agents/__pycache__/model_roles.cpython-313.pyc +0 -0
  9. package/agents/_model_roles.yaml +26 -0
  10. package/agents/designer.md +67 -0
  11. package/agents/explore.md +60 -0
  12. package/agents/model_roles.py +196 -0
  13. package/agents/omg-api-builder.md +23 -0
  14. package/agents/omg-architect-mode.md +41 -0
  15. package/agents/omg-architect.md +13 -0
  16. package/agents/omg-backend-engineer.md +41 -0
  17. package/agents/omg-critic.md +16 -0
  18. package/agents/omg-database-engineer.md +41 -0
  19. package/agents/omg-escalation-router.md +17 -0
  20. package/agents/omg-executor.md +12 -0
  21. package/agents/omg-frontend-designer.md +41 -0
  22. package/agents/omg-implement-mode.md +49 -0
  23. package/agents/omg-infra-engineer.md +41 -0
  24. package/agents/omg-qa-tester.md +16 -0
  25. package/agents/omg-research-mode.md +41 -0
  26. package/agents/omg-security-auditor.md +41 -0
  27. package/agents/omg-testing-engineer.md +41 -0
  28. package/agents/plan.md +80 -0
  29. package/agents/quick_task.md +64 -0
  30. package/agents/reviewer.md +83 -0
  31. package/agents/task.md +71 -0
  32. package/bin/omg +41 -0
  33. package/commands/OMG:ai-commit.md +113 -0
  34. package/commands/OMG:api-twin.md +22 -0
  35. package/commands/OMG:arch.md +313 -0
  36. package/commands/OMG:browser.md +29 -0
  37. package/commands/OMG:ccg.md +22 -0
  38. package/commands/OMG:compat.md +57 -0
  39. package/commands/OMG:cost.md +181 -0
  40. package/commands/OMG:crazy.md +125 -0
  41. package/commands/OMG:create-agent.md +183 -0
  42. package/commands/OMG:deep-plan.md +18 -0
  43. package/commands/OMG:deps.md +248 -0
  44. package/commands/OMG:diagnose-plugins.md +33 -0
  45. package/commands/OMG:doctor.md +37 -0
  46. package/commands/OMG:domain-init.md +11 -0
  47. package/commands/OMG:escalate.md +52 -0
  48. package/commands/OMG:forge.md +103 -0
  49. package/commands/OMG:health-check.md +48 -0
  50. package/commands/OMG:init.md +134 -0
  51. package/commands/OMG:issue.md +56 -0
  52. package/commands/OMG:mode.md +44 -0
  53. package/commands/OMG:playwright.md +17 -0
  54. package/commands/OMG:preflight.md +26 -0
  55. package/commands/OMG:preset.md +49 -0
  56. package/commands/OMG:profile-review.md +58 -0
  57. package/commands/OMG:project-init.md +11 -0
  58. package/commands/OMG:ralph-start.md +43 -0
  59. package/commands/OMG:ralph-stop.md +23 -0
  60. package/commands/OMG:security-check.md +28 -0
  61. package/commands/OMG:session-branch.md +101 -0
  62. package/commands/OMG:session-fork.md +57 -0
  63. package/commands/OMG:session-merge.md +138 -0
  64. package/commands/OMG:setup.md +82 -0
  65. package/commands/OMG:ship.md +18 -0
  66. package/commands/OMG:stats.md +225 -0
  67. package/commands/OMG:teams.md +54 -0
  68. package/commands/OMG:theme.md +44 -0
  69. package/commands/OMG:validate.md +59 -0
  70. package/commands/__init__.py +1 -0
  71. package/docs/command-surface.md +55 -0
  72. package/docs/install/claude-code.md +53 -0
  73. package/docs/install/codex.md +45 -0
  74. package/docs/install/gemini.md +43 -0
  75. package/docs/install/github-action.md +81 -0
  76. package/docs/install/github-app-required-checks.md +107 -0
  77. package/docs/install/github-app.md +161 -0
  78. package/docs/install/kimi.md +43 -0
  79. package/docs/install/opencode.md +38 -0
  80. package/docs/proof.md +182 -0
  81. package/hooks/__init__.py +0 -0
  82. package/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  83. package/hooks/__pycache__/_agent_registry.cpython-313.pyc +0 -0
  84. package/hooks/__pycache__/_analytics.cpython-313.pyc +0 -0
  85. package/hooks/__pycache__/_budget.cpython-313.pyc +0 -0
  86. package/hooks/__pycache__/_common.cpython-313.pyc +0 -0
  87. package/hooks/__pycache__/_compression_optimizer.cpython-313.pyc +0 -0
  88. package/hooks/__pycache__/_cost_ledger.cpython-313.pyc +0 -0
  89. package/hooks/__pycache__/_learnings.cpython-313.pyc +0 -0
  90. package/hooks/__pycache__/_memory.cpython-313.pyc +0 -0
  91. package/hooks/__pycache__/_post_write.cpython-313.pyc +0 -0
  92. package/hooks/__pycache__/_protected_context.cpython-313.pyc +0 -0
  93. package/hooks/__pycache__/_token_counter.cpython-313.pyc +0 -0
  94. package/hooks/__pycache__/branch_manager.cpython-313.pyc +0 -0
  95. package/hooks/__pycache__/budget_governor.cpython-313.pyc +0 -0
  96. package/hooks/__pycache__/circuit-breaker.cpython-313.pyc +0 -0
  97. package/hooks/__pycache__/compression_feedback.cpython-313.pyc +0 -0
  98. package/hooks/__pycache__/config-guard.cpython-313.pyc +0 -0
  99. package/hooks/__pycache__/context_pressure.cpython-313.pyc +0 -0
  100. package/hooks/__pycache__/credential_store.cpython-313.pyc +0 -0
  101. package/hooks/__pycache__/fetch-rate-limits.cpython-313.pyc +0 -0
  102. package/hooks/__pycache__/firewall.cpython-313.pyc +0 -0
  103. package/hooks/__pycache__/hashline-formatter-bridge.cpython-313.pyc +0 -0
  104. package/hooks/__pycache__/hashline-injector.cpython-313.pyc +0 -0
  105. package/hooks/__pycache__/hashline-validator.cpython-313.pyc +0 -0
  106. package/hooks/__pycache__/idle-detector.cpython-313.pyc +0 -0
  107. package/hooks/__pycache__/instructions-loaded.cpython-313.pyc +0 -0
  108. package/hooks/__pycache__/intentgate-keyword-detector.cpython-313.pyc +0 -0
  109. package/hooks/__pycache__/magic-keyword-router.cpython-313.pyc +0 -0
  110. package/hooks/__pycache__/policy_engine.cpython-313.pyc +0 -0
  111. package/hooks/__pycache__/post-tool-failure.cpython-313.pyc +0 -0
  112. package/hooks/__pycache__/post-write.cpython-313.pyc +0 -0
  113. package/hooks/__pycache__/post_write.cpython-313.pyc +0 -0
  114. package/hooks/__pycache__/pre-compact.cpython-313.pyc +0 -0
  115. package/hooks/__pycache__/pre-tool-inject.cpython-313.pyc +0 -0
  116. package/hooks/__pycache__/prompt-enhancer.cpython-313.pyc +0 -0
  117. package/hooks/__pycache__/quality-runner.cpython-313.pyc +0 -0
  118. package/hooks/__pycache__/query.cpython-313.pyc +0 -0
  119. package/hooks/__pycache__/secret-guard.cpython-313.pyc +0 -0
  120. package/hooks/__pycache__/secret_audit.cpython-313.pyc +0 -0
  121. package/hooks/__pycache__/security_validators.cpython-313.pyc +0 -0
  122. package/hooks/__pycache__/session-end-capture.cpython-313.pyc +0 -0
  123. package/hooks/__pycache__/session-start.cpython-313.pyc +0 -0
  124. package/hooks/__pycache__/setup_wizard.cpython-313.pyc +0 -0
  125. package/hooks/__pycache__/shadow_manager.cpython-313.pyc +0 -0
  126. package/hooks/__pycache__/state_migration.cpython-313.pyc +0 -0
  127. package/hooks/__pycache__/stop-gate.cpython-313.pyc +0 -0
  128. package/hooks/__pycache__/stop_dispatcher.cpython-313.pyc +0 -0
  129. package/hooks/__pycache__/tdd-gate.cpython-313.pyc +0 -0
  130. package/hooks/__pycache__/terms-guard.cpython-313.pyc +0 -0
  131. package/hooks/__pycache__/test-validator.cpython-313.pyc +0 -0
  132. package/hooks/__pycache__/test_generator_hook.cpython-313.pyc +0 -0
  133. package/hooks/__pycache__/todo-state-tracker.cpython-313.pyc +0 -0
  134. package/hooks/__pycache__/tool-ledger.cpython-313.pyc +0 -0
  135. package/hooks/__pycache__/trust_review.cpython-313.pyc +0 -0
  136. package/hooks/__pycache__/user-prompt-submit.cpython-313.pyc +0 -0
  137. package/hooks/_agent_registry.py +481 -0
  138. package/hooks/_analytics.py +291 -0
  139. package/hooks/_budget.py +31 -0
  140. package/hooks/_common.py +761 -0
  141. package/hooks/_compression_optimizer.py +119 -0
  142. package/hooks/_cost_ledger.py +176 -0
  143. package/hooks/_learnings.py +126 -0
  144. package/hooks/_memory.py +103 -0
  145. package/hooks/_post_write.py +46 -0
  146. package/hooks/_protected_context.py +150 -0
  147. package/hooks/_token_counter.py +221 -0
  148. package/hooks/branch_manager.py +255 -0
  149. package/hooks/budget_governor.py +326 -0
  150. package/hooks/circuit-breaker.py +270 -0
  151. package/hooks/compression_feedback.py +254 -0
  152. package/hooks/config-guard.py +193 -0
  153. package/hooks/context_pressure.py +119 -0
  154. package/hooks/credential_store.py +970 -0
  155. package/hooks/fetch-rate-limits.py +212 -0
  156. package/hooks/firewall.py +323 -0
  157. package/hooks/hashline-formatter-bridge.py +224 -0
  158. package/hooks/hashline-injector.py +273 -0
  159. package/hooks/hashline-validator.py +216 -0
  160. package/hooks/idle-detector.py +97 -0
  161. package/hooks/instructions-loaded.py +26 -0
  162. package/hooks/intentgate-keyword-detector.py +200 -0
  163. package/hooks/magic-keyword-router.py +195 -0
  164. package/hooks/policy_engine.py +767 -0
  165. package/hooks/post-tool-failure.py +19 -0
  166. package/hooks/post-write.py +233 -0
  167. package/hooks/pre-compact.py +470 -0
  168. package/hooks/pre-tool-inject.py +98 -0
  169. package/hooks/prompt-enhancer.py +879 -0
  170. package/hooks/quality-runner.py +191 -0
  171. package/hooks/query.py +512 -0
  172. package/hooks/secret-guard.py +120 -0
  173. package/hooks/secret_audit.py +144 -0
  174. package/hooks/security_validators.py +93 -0
  175. package/hooks/session-end-capture.py +505 -0
  176. package/hooks/session-start.py +261 -0
  177. package/hooks/setup_wizard.py +1101 -0
  178. package/hooks/shadow_manager.py +476 -0
  179. package/hooks/state_migration.py +228 -0
  180. package/hooks/stop-gate.py +7 -0
  181. package/hooks/stop_dispatcher.py +1259 -0
  182. package/hooks/tdd-gate.py +10 -0
  183. package/hooks/terms-guard.py +98 -0
  184. package/hooks/test-validator.py +462 -0
  185. package/hooks/test_generator_hook.py +123 -0
  186. package/hooks/todo-state-tracker.py +114 -0
  187. package/hooks/tool-ledger.py +165 -0
  188. package/hooks/trust_review.py +662 -0
  189. package/hooks/user-prompt-submit.py +12 -0
  190. package/hud/omg-hud.mjs +1571 -0
  191. package/lab/__init__.py +1 -0
  192. package/lab/__pycache__/__init__.cpython-313.pyc +0 -0
  193. package/lab/__pycache__/axolotl_adapter.cpython-313.pyc +0 -0
  194. package/lab/__pycache__/forge_runner.cpython-313.pyc +0 -0
  195. package/lab/__pycache__/gazebo_adapter.cpython-313.pyc +0 -0
  196. package/lab/__pycache__/isaac_gym_adapter.cpython-313.pyc +0 -0
  197. package/lab/__pycache__/mock_isaac_env.cpython-313.pyc +0 -0
  198. package/lab/__pycache__/pipeline.cpython-313.pyc +0 -0
  199. package/lab/__pycache__/policies.cpython-313.pyc +0 -0
  200. package/lab/__pycache__/pybullet_adapter.cpython-313.pyc +0 -0
  201. package/lab/axolotl_adapter.py +531 -0
  202. package/lab/forge_runner.py +103 -0
  203. package/lab/gazebo_adapter.py +168 -0
  204. package/lab/isaac_gym_adapter.py +190 -0
  205. package/lab/mock_isaac_env.py +47 -0
  206. package/lab/pipeline.py +712 -0
  207. package/lab/policies.py +52 -0
  208. package/lab/pybullet_adapter.py +192 -0
  209. package/package.json +61 -0
  210. package/plugins/README.md +78 -0
  211. package/plugins/__init__.py +1 -0
  212. package/plugins/__pycache__/__init__.cpython-313.pyc +0 -0
  213. package/plugins/advanced/commands/OMG-code-review.md +114 -0
  214. package/plugins/advanced/commands/OMG-deep-plan.md +266 -0
  215. package/plugins/advanced/commands/OMG-handoff.md +115 -0
  216. package/plugins/advanced/commands/OMG-learn.md +110 -0
  217. package/plugins/advanced/commands/OMG-maintainer.md +31 -0
  218. package/plugins/advanced/commands/OMG-ralph-start.md +43 -0
  219. package/plugins/advanced/commands/OMG-ralph-stop.md +23 -0
  220. package/plugins/advanced/commands/OMG-security-review.md +16 -0
  221. package/plugins/advanced/commands/OMG-sequential-thinking.md +20 -0
  222. package/plugins/advanced/commands/OMG-ship.md +46 -0
  223. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  224. package/plugins/advanced/commands/OMG:deep-plan.md +266 -0
  225. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  226. package/plugins/advanced/commands/OMG:learn.md +110 -0
  227. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  228. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  229. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  230. package/plugins/advanced/commands/OMG:security-review.md +16 -0
  231. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  232. package/plugins/advanced/commands/OMG:ship.md +46 -0
  233. package/plugins/advanced/plugin.json +104 -0
  234. package/plugins/core/plugin.json +204 -0
  235. package/plugins/dephealth/__init__.py +0 -0
  236. package/plugins/dephealth/__pycache__/__init__.cpython-313.pyc +0 -0
  237. package/plugins/dephealth/__pycache__/cve_scanner.cpython-313.pyc +0 -0
  238. package/plugins/dephealth/__pycache__/license_checker.cpython-313.pyc +0 -0
  239. package/plugins/dephealth/__pycache__/manifest_detector.cpython-313.pyc +0 -0
  240. package/plugins/dephealth/__pycache__/vuln_analyzer.cpython-313.pyc +0 -0
  241. package/plugins/dephealth/cve_scanner.py +279 -0
  242. package/plugins/dephealth/license_checker.py +135 -0
  243. package/plugins/dephealth/manifest_detector.py +423 -0
  244. package/plugins/dephealth/vuln_analyzer.py +176 -0
  245. package/plugins/testgen/__init__.py +0 -0
  246. package/plugins/testgen/__pycache__/__init__.cpython-313.pyc +0 -0
  247. package/plugins/testgen/__pycache__/codamosa_engine.cpython-313.pyc +0 -0
  248. package/plugins/testgen/__pycache__/edge_case_synthesizer.cpython-313.pyc +0 -0
  249. package/plugins/testgen/__pycache__/framework_detector.cpython-313.pyc +0 -0
  250. package/plugins/testgen/__pycache__/skeleton_generator.cpython-313.pyc +0 -0
  251. package/plugins/testgen/codamosa_engine.py +402 -0
  252. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  253. package/plugins/testgen/framework_detector.py +271 -0
  254. package/plugins/testgen/skeleton_generator.py +219 -0
  255. package/plugins/viz/__init__.py +0 -0
  256. package/plugins/viz/__pycache__/__init__.cpython-313.pyc +0 -0
  257. package/plugins/viz/__pycache__/ast_parser.cpython-313.pyc +0 -0
  258. package/plugins/viz/__pycache__/diagram_generator.cpython-313.pyc +0 -0
  259. package/plugins/viz/__pycache__/graph_builder.cpython-313.pyc +0 -0
  260. package/plugins/viz/__pycache__/native_parsers.cpython-313.pyc +0 -0
  261. package/plugins/viz/__pycache__/regex_parser.cpython-313.pyc +0 -0
  262. package/plugins/viz/ast_parser.py +139 -0
  263. package/plugins/viz/diagram_generator.py +192 -0
  264. package/plugins/viz/graph_builder.py +444 -0
  265. package/plugins/viz/native_parsers.py +259 -0
  266. package/plugins/viz/regex_parser.py +112 -0
  267. package/pyproject.toml +143 -0
  268. package/registry/__init__.py +1 -0
  269. package/registry/__pycache__/__init__.cpython-313.pyc +0 -0
  270. package/registry/__pycache__/approval_artifact.cpython-313.pyc +0 -0
  271. package/registry/__pycache__/verify_artifact.cpython-313.pyc +0 -0
  272. package/registry/approval_artifact.py +236 -0
  273. package/registry/bundles/algorithms.yaml +45 -0
  274. package/registry/bundles/api-twin.yaml +48 -0
  275. package/registry/bundles/ast-pack.yaml +80 -0
  276. package/registry/bundles/claim-judge.yaml +49 -0
  277. package/registry/bundles/control-plane.yaml +192 -0
  278. package/registry/bundles/data-lineage.yaml +47 -0
  279. package/registry/bundles/delta-classifier.yaml +47 -0
  280. package/registry/bundles/eval-gate.yaml +47 -0
  281. package/registry/bundles/hash-edit.yaml +73 -0
  282. package/registry/bundles/health.yaml +45 -0
  283. package/registry/bundles/hook-governor.yaml +101 -0
  284. package/registry/bundles/incident-replay.yaml +47 -0
  285. package/registry/bundles/lsp-pack.yaml +80 -0
  286. package/registry/bundles/mcp-fabric.yaml +53 -0
  287. package/registry/bundles/plan-council.yaml +56 -0
  288. package/registry/bundles/preflight.yaml +48 -0
  289. package/registry/bundles/proof-gate.yaml +49 -0
  290. package/registry/bundles/remote-supervisor.yaml +49 -0
  291. package/registry/bundles/robotics.yaml +45 -0
  292. package/registry/bundles/secure-worktree-pipeline.yaml +69 -0
  293. package/registry/bundles/security-check.yaml +50 -0
  294. package/registry/bundles/terminal-lane.yaml +61 -0
  295. package/registry/bundles/test-intent-lock.yaml +49 -0
  296. package/registry/bundles/tracebank.yaml +47 -0
  297. package/registry/bundles/vision.yaml +45 -0
  298. package/registry/omg-capability.schema.json +378 -0
  299. package/registry/policy-packs/airgapped.lock.json +11 -0
  300. package/registry/policy-packs/airgapped.signature.json +10 -0
  301. package/registry/policy-packs/airgapped.yaml +16 -0
  302. package/registry/policy-packs/fintech.lock.json +11 -0
  303. package/registry/policy-packs/fintech.signature.json +10 -0
  304. package/registry/policy-packs/fintech.yaml +15 -0
  305. package/registry/policy-packs/locked-prod.lock.json +11 -0
  306. package/registry/policy-packs/locked-prod.signature.json +10 -0
  307. package/registry/policy-packs/locked-prod.yaml +18 -0
  308. package/registry/trusted_signers.json +44 -0
  309. package/registry/verify_artifact.py +493 -0
  310. package/runtime/__init__.py +36 -0
  311. package/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  312. package/runtime/__pycache__/adoption.cpython-313.pyc +0 -0
  313. package/runtime/__pycache__/agent_selector.cpython-313.pyc +0 -0
  314. package/runtime/__pycache__/api_twin.cpython-313.pyc +0 -0
  315. package/runtime/__pycache__/architecture_signal.cpython-313.pyc +0 -0
  316. package/runtime/__pycache__/artifact_parsers.cpython-313.pyc +0 -0
  317. package/runtime/__pycache__/asset_loader.cpython-313.pyc +0 -0
  318. package/runtime/__pycache__/background_verification.cpython-313.pyc +0 -0
  319. package/runtime/__pycache__/budget_envelopes.cpython-313.pyc +0 -0
  320. package/runtime/__pycache__/business_workflow.cpython-313.pyc +0 -0
  321. package/runtime/__pycache__/canonical_surface.cpython-313.pyc +0 -0
  322. package/runtime/__pycache__/canonical_taxonomy.cpython-313.pyc +0 -0
  323. package/runtime/__pycache__/claim_judge.cpython-313.pyc +0 -0
  324. package/runtime/__pycache__/cli_provider.cpython-313.pyc +0 -0
  325. package/runtime/__pycache__/compat.cpython-313.pyc +0 -0
  326. package/runtime/__pycache__/complexity_scorer.cpython-313.pyc +0 -0
  327. package/runtime/__pycache__/compliance_governor.cpython-313.pyc +0 -0
  328. package/runtime/__pycache__/config_transaction.cpython-313.pyc +0 -0
  329. package/runtime/__pycache__/context_compiler.cpython-313.pyc +0 -0
  330. package/runtime/__pycache__/context_engine.cpython-313.pyc +0 -0
  331. package/runtime/__pycache__/context_limits.cpython-313.pyc +0 -0
  332. package/runtime/__pycache__/contract_compiler.cpython-313.pyc +0 -0
  333. package/runtime/__pycache__/custom_agent_loader.cpython-313.pyc +0 -0
  334. package/runtime/__pycache__/data_lineage.cpython-313.pyc +0 -0
  335. package/runtime/__pycache__/defense_state.cpython-313.pyc +0 -0
  336. package/runtime/__pycache__/delta_classifier.cpython-313.pyc +0 -0
  337. package/runtime/__pycache__/dispatcher.cpython-313.pyc +0 -0
  338. package/runtime/__pycache__/doc_generator.cpython-313.pyc +0 -0
  339. package/runtime/__pycache__/domain_packs.cpython-313.pyc +0 -0
  340. package/runtime/__pycache__/ecosystem.cpython-313.pyc +0 -0
  341. package/runtime/__pycache__/equalizer.cpython-313.pyc +0 -0
  342. package/runtime/__pycache__/eval_gate.cpython-313.pyc +0 -0
  343. package/runtime/__pycache__/evidence_narrator.cpython-313.pyc +0 -0
  344. package/runtime/__pycache__/evidence_query.cpython-313.pyc +0 -0
  345. package/runtime/__pycache__/evidence_registry.cpython-313.pyc +0 -0
  346. package/runtime/__pycache__/evidence_requirements.cpython-313.pyc +0 -0
  347. package/runtime/__pycache__/exec_kernel.cpython-313.pyc +0 -0
  348. package/runtime/__pycache__/explainer_formatter.cpython-313.pyc +0 -0
  349. package/runtime/__pycache__/feature_registry.cpython-313.pyc +0 -0
  350. package/runtime/__pycache__/forge_agents.cpython-313.pyc +0 -0
  351. package/runtime/__pycache__/forge_contracts.cpython-313.pyc +0 -0
  352. package/runtime/__pycache__/forge_domains.cpython-313.pyc +0 -0
  353. package/runtime/__pycache__/forge_run_id.cpython-313.pyc +0 -0
  354. package/runtime/__pycache__/github_integration.cpython-313.pyc +0 -0
  355. package/runtime/__pycache__/github_review_bot.cpython-313.pyc +0 -0
  356. package/runtime/__pycache__/github_review_contract.cpython-313.pyc +0 -0
  357. package/runtime/__pycache__/github_review_formatter.cpython-313.pyc +0 -0
  358. package/runtime/__pycache__/guide_assert.cpython-313.pyc +0 -0
  359. package/runtime/__pycache__/hook_governor.cpython-313.pyc +0 -0
  360. package/runtime/__pycache__/host_parity.cpython-313.pyc +0 -0
  361. package/runtime/__pycache__/incident_replay.cpython-313.pyc +0 -0
  362. package/runtime/__pycache__/install_planner.cpython-313.pyc +0 -0
  363. package/runtime/__pycache__/interaction_journal.cpython-313.pyc +0 -0
  364. package/runtime/__pycache__/issue_surface.cpython-313.pyc +0 -0
  365. package/runtime/__pycache__/legacy_compat.cpython-313.pyc +0 -0
  366. package/runtime/__pycache__/mcp_config_writers.cpython-313.pyc +0 -0
  367. package/runtime/__pycache__/mcp_lifecycle.cpython-313.pyc +0 -0
  368. package/runtime/__pycache__/mcp_memory_server.cpython-313.pyc +0 -0
  369. package/runtime/__pycache__/memory_store.cpython-313.pyc +0 -0
  370. package/runtime/__pycache__/merge_writer.cpython-313.pyc +0 -0
  371. package/runtime/__pycache__/music_omr_testbed.cpython-313.pyc +0 -0
  372. package/runtime/__pycache__/mutation_gate.cpython-313.pyc +0 -0
  373. package/runtime/__pycache__/omc_compat.cpython-313.pyc +0 -0
  374. package/runtime/__pycache__/omg_browser_cli.cpython-313.pyc +0 -0
  375. package/runtime/__pycache__/omg_mcp_server.cpython-313.pyc +0 -0
  376. package/runtime/__pycache__/opus_plan.cpython-313.pyc +0 -0
  377. package/runtime/__pycache__/playwright_adapter.cpython-313.pyc +0 -0
  378. package/runtime/__pycache__/playwright_pack.cpython-313.pyc +0 -0
  379. package/runtime/__pycache__/plugin_diagnostics.cpython-313.pyc +0 -0
  380. package/runtime/__pycache__/plugin_interop.cpython-313.pyc +0 -0
  381. package/runtime/__pycache__/policy_pack_loader.cpython-313.pyc +0 -0
  382. package/runtime/__pycache__/preflight.cpython-313.pyc +0 -0
  383. package/runtime/__pycache__/profile_io.cpython-313.pyc +0 -0
  384. package/runtime/__pycache__/prompt_compiler.cpython-313.pyc +0 -0
  385. package/runtime/__pycache__/proof_chain.cpython-313.pyc +0 -0
  386. package/runtime/__pycache__/proof_gate.cpython-313.pyc +0 -0
  387. package/runtime/__pycache__/provider_parity_eval.cpython-313.pyc +0 -0
  388. package/runtime/__pycache__/release_artifact_audit.cpython-313.pyc +0 -0
  389. package/runtime/__pycache__/release_run_coordinator.cpython-313.pyc +0 -0
  390. package/runtime/__pycache__/release_surface_compiler.cpython-313.pyc +0 -0
  391. package/runtime/__pycache__/release_surface_registry.cpython-313.pyc +0 -0
  392. package/runtime/__pycache__/release_surfaces.cpython-313.pyc +0 -0
  393. package/runtime/__pycache__/remote_supervisor.cpython-313.pyc +0 -0
  394. package/runtime/__pycache__/repro_pack.cpython-313.pyc +0 -0
  395. package/runtime/__pycache__/rollback_manifest.cpython-313.pyc +0 -0
  396. package/runtime/__pycache__/router_critics.cpython-313.pyc +0 -0
  397. package/runtime/__pycache__/router_executor.cpython-313.pyc +0 -0
  398. package/runtime/__pycache__/router_selector.cpython-313.pyc +0 -0
  399. package/runtime/__pycache__/runtime_contracts.cpython-313.pyc +0 -0
  400. package/runtime/__pycache__/runtime_profile.cpython-313.pyc +0 -0
  401. package/runtime/__pycache__/security_check.cpython-313.pyc +0 -0
  402. package/runtime/__pycache__/session_health.cpython-313.pyc +0 -0
  403. package/runtime/__pycache__/skill_evolution.cpython-313.pyc +0 -0
  404. package/runtime/__pycache__/skill_registry.cpython-313.pyc +0 -0
  405. package/runtime/__pycache__/subagent_dispatcher.cpython-313.pyc +0 -0
  406. package/runtime/__pycache__/subscription_tiers.cpython-313.pyc +0 -0
  407. package/runtime/__pycache__/team_router.cpython-313.pyc +0 -0
  408. package/runtime/__pycache__/test_intent_lock.cpython-313-pytest-9.0.2.pyc +0 -0
  409. package/runtime/__pycache__/test_intent_lock.cpython-313.pyc +0 -0
  410. package/runtime/__pycache__/tmux_session_manager.cpython-313.pyc +0 -0
  411. package/runtime/__pycache__/tool_fabric.cpython-313.pyc +0 -0
  412. package/runtime/__pycache__/tool_plan_gate.cpython-313.pyc +0 -0
  413. package/runtime/__pycache__/tool_relevance.cpython-313.pyc +0 -0
  414. package/runtime/__pycache__/tracebank.cpython-313.pyc +0 -0
  415. package/runtime/__pycache__/untrusted_content.cpython-313.pyc +0 -0
  416. package/runtime/__pycache__/validate.cpython-313.pyc +0 -0
  417. package/runtime/__pycache__/verdict_schema.cpython-313.pyc +0 -0
  418. package/runtime/__pycache__/verification_controller.cpython-313.pyc +0 -0
  419. package/runtime/__pycache__/verification_loop.cpython-313.pyc +0 -0
  420. package/runtime/__pycache__/vision_artifacts.cpython-313.pyc +0 -0
  421. package/runtime/__pycache__/vision_cache.cpython-313.pyc +0 -0
  422. package/runtime/__pycache__/vision_jobs.cpython-313.pyc +0 -0
  423. package/runtime/__pycache__/worker_watchdog.cpython-313.pyc +0 -0
  424. package/runtime/adapters/__init__.py +13 -0
  425. package/runtime/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  426. package/runtime/adapters/__pycache__/claude.cpython-313.pyc +0 -0
  427. package/runtime/adapters/__pycache__/gpt.cpython-313.pyc +0 -0
  428. package/runtime/adapters/__pycache__/local.cpython-313.pyc +0 -0
  429. package/runtime/adapters/claude.py +63 -0
  430. package/runtime/adapters/gpt.py +56 -0
  431. package/runtime/adapters/local.py +56 -0
  432. package/runtime/adoption.py +280 -0
  433. package/runtime/api_twin.py +450 -0
  434. package/runtime/architecture_signal.py +226 -0
  435. package/runtime/artifact_parsers.py +161 -0
  436. package/runtime/asset_loader.py +62 -0
  437. package/runtime/background_verification.py +178 -0
  438. package/runtime/budget_envelopes.py +398 -0
  439. package/runtime/business_workflow.py +234 -0
  440. package/runtime/canonical_surface.py +53 -0
  441. package/runtime/canonical_taxonomy.py +27 -0
  442. package/runtime/claim_judge.py +648 -0
  443. package/runtime/cli_provider.py +105 -0
  444. package/runtime/compat.py +2222 -0
  445. package/runtime/complexity_scorer.py +148 -0
  446. package/runtime/compliance_governor.py +505 -0
  447. package/runtime/config_transaction.py +304 -0
  448. package/runtime/context_compiler.py +131 -0
  449. package/runtime/context_engine.py +708 -0
  450. package/runtime/context_limits.py +363 -0
  451. package/runtime/contract_compiler.py +3664 -0
  452. package/runtime/custom_agent_loader.py +366 -0
  453. package/runtime/data_lineage.py +244 -0
  454. package/runtime/defense_state.py +261 -0
  455. package/runtime/delta_classifier.py +231 -0
  456. package/runtime/dispatcher.py +47 -0
  457. package/runtime/doc_generator.py +319 -0
  458. package/runtime/domain_packs.py +75 -0
  459. package/runtime/ecosystem.py +371 -0
  460. package/runtime/equalizer.py +268 -0
  461. package/runtime/eval_gate.py +96 -0
  462. package/runtime/evidence_narrator.py +147 -0
  463. package/runtime/evidence_query.py +303 -0
  464. package/runtime/evidence_registry.py +16 -0
  465. package/runtime/evidence_requirements.py +157 -0
  466. package/runtime/exec_kernel.py +267 -0
  467. package/runtime/explainer_formatter.py +82 -0
  468. package/runtime/feature_registry.py +109 -0
  469. package/runtime/forge_agents.py +915 -0
  470. package/runtime/forge_contracts.py +519 -0
  471. package/runtime/forge_domains.py +68 -0
  472. package/runtime/forge_run_id.py +86 -0
  473. package/runtime/guide_assert.py +135 -0
  474. package/runtime/hook_governor.py +156 -0
  475. package/runtime/host_parity.py +373 -0
  476. package/runtime/incident_replay.py +310 -0
  477. package/runtime/install_planner.py +617 -0
  478. package/runtime/interaction_journal.py +566 -0
  479. package/runtime/issue_surface.py +472 -0
  480. package/runtime/legacy_compat.py +7 -0
  481. package/runtime/mcp_config_writers.py +360 -0
  482. package/runtime/mcp_lifecycle.py +175 -0
  483. package/runtime/mcp_memory_server.py +220 -0
  484. package/runtime/memory_parsers/__init__.py +0 -0
  485. package/runtime/memory_parsers/__pycache__/__init__.cpython-313.pyc +0 -0
  486. package/runtime/memory_parsers/__pycache__/chatgpt_parser.cpython-313.pyc +0 -0
  487. package/runtime/memory_parsers/__pycache__/claude_import.cpython-313.pyc +0 -0
  488. package/runtime/memory_parsers/__pycache__/export.cpython-313.pyc +0 -0
  489. package/runtime/memory_parsers/__pycache__/gemini_import.cpython-313.pyc +0 -0
  490. package/runtime/memory_parsers/__pycache__/kimi_import.cpython-313.pyc +0 -0
  491. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  492. package/runtime/memory_parsers/claude_import.py +107 -0
  493. package/runtime/memory_parsers/export.py +97 -0
  494. package/runtime/memory_parsers/gemini_import.py +91 -0
  495. package/runtime/memory_parsers/kimi_import.py +91 -0
  496. package/runtime/memory_store.py +1182 -0
  497. package/runtime/merge_writer.py +445 -0
  498. package/runtime/music_omr_testbed.py +336 -0
  499. package/runtime/mutation_gate.py +320 -0
  500. package/runtime/omc_compat.py +7 -0
  501. package/runtime/omg_browser_cli.py +95 -0
  502. package/runtime/omg_compat_contract_snapshot.json +936 -0
  503. package/runtime/omg_contract_snapshot.json +936 -0
  504. package/runtime/omg_mcp_server.py +306 -0
  505. package/runtime/playwright_adapter.py +39 -0
  506. package/runtime/playwright_pack.py +253 -0
  507. package/runtime/plugin_diagnostics.py +308 -0
  508. package/runtime/plugin_interop.py +1060 -0
  509. package/runtime/policy_pack_loader.py +147 -0
  510. package/runtime/preflight.py +135 -0
  511. package/runtime/profile_io.py +328 -0
  512. package/runtime/proof_chain.py +472 -0
  513. package/runtime/proof_gate.py +442 -0
  514. package/runtime/provider_parity_eval.py +109 -0
  515. package/runtime/providers/__init__.py +0 -0
  516. package/runtime/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  517. package/runtime/providers/__pycache__/codex_provider.cpython-313.pyc +0 -0
  518. package/runtime/providers/__pycache__/gemini_provider.cpython-313.pyc +0 -0
  519. package/runtime/providers/__pycache__/kimi_provider.cpython-313.pyc +0 -0
  520. package/runtime/providers/__pycache__/opencode_provider.cpython-313.pyc +0 -0
  521. package/runtime/providers/codex_provider.py +129 -0
  522. package/runtime/providers/gemini_provider.py +143 -0
  523. package/runtime/providers/kimi_provider.py +167 -0
  524. package/runtime/providers/opencode_provider.py +99 -0
  525. package/runtime/release_artifact_audit.py +556 -0
  526. package/runtime/release_run_coordinator.py +574 -0
  527. package/runtime/release_surface_compiler.py +643 -0
  528. package/runtime/release_surface_registry.py +283 -0
  529. package/runtime/release_surfaces.py +320 -0
  530. package/runtime/remote_supervisor.py +79 -0
  531. package/runtime/repro_pack.py +398 -0
  532. package/runtime/rollback_manifest.py +143 -0
  533. package/runtime/router_critics.py +229 -0
  534. package/runtime/router_executor.py +142 -0
  535. package/runtime/router_selector.py +99 -0
  536. package/runtime/runtime_contracts.py +292 -0
  537. package/runtime/runtime_profile.py +133 -0
  538. package/runtime/security_check.py +1094 -0
  539. package/runtime/session_health.py +546 -0
  540. package/runtime/skill_evolution.py +221 -0
  541. package/runtime/skill_registry.py +53 -0
  542. package/runtime/subagent_dispatcher.py +604 -0
  543. package/runtime/subscription_tiers.py +258 -0
  544. package/runtime/team_router.py +1399 -0
  545. package/runtime/test_intent_lock.py +543 -0
  546. package/runtime/tmux_session_manager.py +172 -0
  547. package/runtime/tool_fabric.py +570 -0
  548. package/runtime/tool_plan_gate.py +460 -0
  549. package/runtime/tracebank.py +125 -0
  550. package/runtime/untrusted_content.py +360 -0
  551. package/runtime/validate.py +293 -0
  552. package/runtime/verdict_schema.py +198 -0
  553. package/runtime/verification_controller.py +235 -0
  554. package/runtime/verification_loop.py +73 -0
  555. package/runtime/vision_artifacts.py +31 -0
  556. package/runtime/vision_cache.py +38 -0
  557. package/runtime/vision_jobs.py +92 -0
  558. package/runtime/worker_watchdog.py +526 -0
  559. package/scripts/__pycache__/audit-published-artifact.cpython-313.pyc +0 -0
  560. package/scripts/__pycache__/check-doc-parity.cpython-313.pyc +0 -0
  561. package/scripts/__pycache__/check-omg-standalone-clean.cpython-313.pyc +0 -0
  562. package/scripts/__pycache__/github_review_helpers.cpython-313.pyc +0 -0
  563. package/scripts/__pycache__/omg.cpython-313.pyc +0 -0
  564. package/scripts/__pycache__/prepare-release-proof-fixtures.cpython-313.pyc +0 -0
  565. package/scripts/__pycache__/sync-release-identity.cpython-313.pyc +0 -0
  566. package/scripts/__pycache__/validate-release-identity.cpython-313.pyc +0 -0
  567. package/scripts/audit-published-artifact.py +59 -0
  568. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  569. package/scripts/check-omg-contract-snapshot.py +12 -0
  570. package/scripts/check-omg-public-ready.py +273 -0
  571. package/scripts/check-omg-standalone-clean.py +133 -0
  572. package/scripts/emit_host_parity.py +72 -0
  573. package/scripts/legacy_to_omg_migrate.py +29 -0
  574. package/scripts/migrate-legacy.py +464 -0
  575. package/scripts/omc_to_omg_migrate.py +12 -0
  576. package/scripts/omg.py +2962 -0
  577. package/scripts/pre-release-check.sh +38 -0
  578. package/scripts/prepare-release-proof-fixtures.py +602 -0
  579. package/scripts/print-canonical-version.py +80 -0
  580. package/scripts/settings-merge.py +289 -0
  581. package/scripts/sync-release-identity.py +481 -0
  582. package/scripts/validate-release-identity.py +632 -0
  583. package/scripts/verify-no-omc.sh +5 -0
  584. package/scripts/verify-standalone.sh +35 -0
  585. package/settings.json +751 -0
  586. package/tools/__init__.py +2 -0
  587. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  588. package/tools/__pycache__/browser_consent.cpython-313.pyc +0 -0
  589. package/tools/__pycache__/browser_stealth.cpython-313.pyc +0 -0
  590. package/tools/__pycache__/browser_tool.cpython-313.pyc +0 -0
  591. package/tools/__pycache__/changelog_generator.cpython-313.pyc +0 -0
  592. package/tools/__pycache__/commit_splitter.cpython-313.pyc +0 -0
  593. package/tools/__pycache__/config_discovery.cpython-313.pyc +0 -0
  594. package/tools/__pycache__/config_merger.cpython-313.pyc +0 -0
  595. package/tools/__pycache__/dashboard_generator.cpython-313.pyc +0 -0
  596. package/tools/__pycache__/git_inspector.cpython-313.pyc +0 -0
  597. package/tools/__pycache__/lsp_client.cpython-313.pyc +0 -0
  598. package/tools/__pycache__/lsp_operations.cpython-313.pyc +0 -0
  599. package/tools/__pycache__/pr_generator.cpython-313.pyc +0 -0
  600. package/tools/__pycache__/python_repl.cpython-313.pyc +0 -0
  601. package/tools/__pycache__/python_sandbox.cpython-313.pyc +0 -0
  602. package/tools/__pycache__/session_snapshot.cpython-313.pyc +0 -0
  603. package/tools/__pycache__/ssh_manager.cpython-313.pyc +0 -0
  604. package/tools/__pycache__/theme_engine.cpython-313.pyc +0 -0
  605. package/tools/__pycache__/theme_selector.cpython-313.pyc +0 -0
  606. package/tools/__pycache__/web_search.cpython-313.pyc +0 -0
  607. package/tools/browser_consent.py +289 -0
  608. package/tools/browser_stealth.py +481 -0
  609. package/tools/browser_tool.py +448 -0
  610. package/tools/changelog_generator.py +347 -0
  611. package/tools/commit_splitter.py +749 -0
  612. package/tools/config_discovery.py +151 -0
  613. package/tools/config_merger.py +449 -0
  614. package/tools/dashboard_generator.py +300 -0
  615. package/tools/git_inspector.py +298 -0
  616. package/tools/lsp_client.py +275 -0
  617. package/tools/lsp_discovery.py +231 -0
  618. package/tools/lsp_operations.py +392 -0
  619. package/tools/pr_generator.py +404 -0
  620. package/tools/python_repl.py +712 -0
  621. package/tools/python_sandbox.py +768 -0
  622. package/tools/search_providers/__init__.py +77 -0
  623. package/tools/search_providers/__pycache__/__init__.cpython-313.pyc +0 -0
  624. package/tools/search_providers/__pycache__/brave.cpython-313.pyc +0 -0
  625. package/tools/search_providers/__pycache__/exa.cpython-313.pyc +0 -0
  626. package/tools/search_providers/__pycache__/jina.cpython-313.pyc +0 -0
  627. package/tools/search_providers/__pycache__/perplexity.cpython-313.pyc +0 -0
  628. package/tools/search_providers/__pycache__/synthetic.cpython-313.pyc +0 -0
  629. package/tools/search_providers/brave.py +115 -0
  630. package/tools/search_providers/exa.py +116 -0
  631. package/tools/search_providers/jina.py +104 -0
  632. package/tools/search_providers/perplexity.py +139 -0
  633. package/tools/search_providers/synthetic.py +74 -0
  634. package/tools/session_snapshot.py +851 -0
  635. package/tools/ssh_manager.py +912 -0
  636. package/tools/theme_engine.py +296 -0
  637. package/tools/theme_selector.py +137 -0
  638. package/tools/web_search.py +675 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Backward-compatible entrypoint for the test validator stop hook."""
3
+ from __future__ import annotations
4
+
5
+ import runpy
6
+ from pathlib import Path
7
+
8
+
9
+ if __name__ == "__main__":
10
+ runpy.run_path(str(Path(__file__).with_name("test-validator.py")), run_name="__main__")
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ HOOKS_DIR = str(Path(__file__).resolve().parent)
9
+ PROJECT_ROOT = str(Path(HOOKS_DIR).parent)
10
+ PORTABLE_RUNTIME_ROOT = str(Path(PROJECT_ROOT) / "omg-runtime")
11
+ for path in (HOOKS_DIR, PROJECT_ROOT, PORTABLE_RUNTIME_ROOT):
12
+ if path not in sys.path:
13
+ sys.path.insert(0, path)
14
+
15
+ from hooks._common import bootstrap_runtime_paths, setup_crash_handler, json_input, deny_decision, get_feature_flag
16
+
17
+ bootstrap_runtime_paths(__file__)
18
+ setup_crash_handler("terms-guard", fail_closed=True)
19
+
20
+ MODEL_TOKENS = ("claude", "codex", "gemini", "kimi", "gpt", "openai", "anthropic")
21
+
22
+
23
+ def _collect_mutation_text(tool_name: str, tool_input: object) -> str:
24
+ if tool_name not in {"Write", "Edit", "MultiEdit"}:
25
+ return ""
26
+ if not isinstance(tool_input, dict):
27
+ return ""
28
+
29
+ parts: list[str] = []
30
+ for key in ("content", "new_string", "old_string", "insert_text", "text"):
31
+ value = tool_input.get(key)
32
+ if isinstance(value, str) and value:
33
+ parts.append(value)
34
+
35
+ edits = tool_input.get("edits")
36
+ if isinstance(edits, list):
37
+ for item in edits:
38
+ if not isinstance(item, dict):
39
+ continue
40
+ for key in ("newText", "oldText", "new_string", "old_string", "content"):
41
+ value = item.get(key)
42
+ if isinstance(value, str) and value:
43
+ parts.append(value)
44
+
45
+ return "\n".join(parts)
46
+
47
+
48
+ def _model_mentions(text: str) -> int:
49
+ seen = {token for token in MODEL_TOKENS if re.search(rf"\b{re.escape(token)}\b", text, flags=re.IGNORECASE)}
50
+ return len(seen)
51
+
52
+
53
+ def _detect_violation_reason(text: str) -> str | None:
54
+ lowered = text.lower()
55
+ has_star = re.search(r"\b(star|starring|upvote)\b", lowered) is not None
56
+ has_share = re.search(r"\b(share|forward|broadcast|cross-model|cross model|copy this prompt|paste this prompt)\b", lowered) is not None
57
+ if has_star and has_share and _model_mentions(lowered) >= 2:
58
+ return "promotion_star_cross_model"
59
+
60
+ has_switching = re.search(r"\b(route|switch|proxy|forward|delegate)\b", lowered) is not None
61
+ has_identity_claim = re.search(
62
+ r"\b(tell\s+the\s+user|claim|pretend|masquerade|say\s+this\s+came\s+from|present\s+as)\b",
63
+ lowered,
64
+ ) is not None
65
+ has_hidden = re.search(
66
+ r"\b(hidden|hide\s+this|secretly|without\s+disclos(?:ing|ure)|do\s+not\s+disclose|don['’]?t\s+disclose|undisclosed)\b",
67
+ lowered,
68
+ ) is not None
69
+ if has_switching and has_identity_claim and has_hidden and _model_mentions(lowered) >= 2:
70
+ return "hidden_model_identity_switch"
71
+
72
+ has_third_party = re.search(r"\b(third[- ]party|external\s+(?:api|service|vendor)|analytics\s+(?:api|service))\b", lowered) is not None
73
+ has_data = re.search(r"\b(logs?|conversation|prompt|chat\s*history|transcript|user\s*data)\b", lowered) is not None
74
+ has_no_disclosure = re.search(
75
+ r"\b(without\s+(?:user\s+)?disclos(?:ing|ure)|without\s+consent|undisclosed|do\s+not\s+disclose|don['’]?t\s+disclose)\b",
76
+ lowered,
77
+ ) is not None
78
+ if has_third_party and has_data and has_no_disclosure:
79
+ return "undisclosed_third_party_sharing"
80
+
81
+ return None
82
+
83
+
84
+ data = json_input()
85
+ if not get_feature_flag("TERMS_ENFORCEMENT", default=False):
86
+ sys.exit(0)
87
+
88
+ tool_name = data.get("tool_name", "") if isinstance(data, dict) else ""
89
+ tool_input = data.get("tool_input", {}) if isinstance(data, dict) else {}
90
+ mutation_text = _collect_mutation_text(tool_name, tool_input)
91
+ if not mutation_text:
92
+ sys.exit(0)
93
+
94
+ reason = _detect_violation_reason(mutation_text)
95
+ if reason is not None:
96
+ deny_decision(f"terms_guard:{reason}")
97
+
98
+ sys.exit(0)
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stop Hook: Test Validator (v5) — Enhanced Anti-Pattern Detection
4
+
5
+ v5 additions (T32):
6
+ - Skip/ignore test detection (pytest.mark.skip, xit, xdescribe, etc.)
7
+ - Mock-heavy test detection (ratio-based: mocks vs assertions)
8
+ - Parameterized test gap detection (same function 3+ literal args)
9
+ - Assertion-free Python test detection (def test_* with no assert)
10
+ - Empty test body detection (pass, ..., comment-only)
11
+ - Coverage metrics persistence to .omg/state/test-metrics.json
12
+
13
+ Callable API:
14
+ check_test_quality(data, project_dir) -> list[str]
15
+ Returns list of block reasons (empty = pass).
16
+ analyze_test_content(content, filename) -> list[str]
17
+ Returns list of issue strings for a single file's content.
18
+ persist_metrics(project_dir, analysis) -> None
19
+ Writes test quality metrics to .omg/state/test-metrics.json.
20
+ """
21
+ import json, sys, os, re
22
+ from collections import Counter
23
+ from datetime import datetime, timezone
24
+
25
+ HOOKS_DIR = os.path.dirname(__file__)
26
+ PROJECT_ROOT = os.path.dirname(HOOKS_DIR)
27
+ if HOOKS_DIR not in sys.path:
28
+ sys.path.insert(0, HOOKS_DIR)
29
+ if PROJECT_ROOT not in sys.path:
30
+ sys.path.insert(0, PROJECT_ROOT)
31
+
32
+ from hooks._common import _resolve_project_dir, should_skip_stop_hooks
33
+
34
+ # --- Builtins excluded from parameterized-gap detection ---
35
+ _BUILTIN_FUNCS = frozenset({
36
+ "test", "it", "describe", "print", "len", "range", "str", "int",
37
+ "float", "list", "dict", "set", "tuple", "type", "isinstance",
38
+ "assert_equal", "assertEqual", "patch", "mock", "Mock", "MagicMock",
39
+ "expect", "require", "import", "open", "super", "getattr", "setattr",
40
+ "hasattr", "sorted", "enumerate", "zip", "map", "filter", "min", "max",
41
+ "get", "append", "extend", "insert", "pop", "remove", "update", "add",
42
+ "setdefault", "keys", "values", "items", "join", "split", "strip",
43
+ "replace", "format", "encode", "decode", "startswith", "endswith",
44
+ "write", "read", "write_text", "read_text", "mkdir", "exists",
45
+ "param", "parametrize", "mark", "fixture", "yield_fixture",
46
+ "setenv", "delenv", "monkeypatch", "chdir",
47
+ "dumps", "loads", "dump", "load",
48
+ })
49
+ _MUTATION_TOOLS = frozenset({"write", "edit", "multiedit", "bash"})
50
+ _SKIP_DIR_SEGMENTS = frozenset({"build", "dist", "node_modules", ".git"})
51
+ _TEST_DIR_NAMES = frozenset({"tests", "test", "__tests__"})
52
+
53
+
54
+ def analyze_test_content(content, filename="test.py"):
55
+ """
56
+ Analyze test file content for quality anti-patterns.
57
+
58
+ Returns list of issue strings, each prefixed with a category label:
59
+ FAKE:, BOILERPLATE:, HAPPY PATH ONLY:, EMPTY:, OVER-MOCKED:,
60
+ SKIP:, ASSERTION-FREE:, MOCK-HEAVY:, PARAMETERIZED:
61
+ """
62
+ issues = []
63
+
64
+ # === FAKE TEST PATTERNS (v3, kept) ===
65
+ fake_patterns = [
66
+ (r"expect\s*\(\s*true\s*\)\s*\.to(Be|Equal)\s*\(\s*true\s*\)", "assert true === true"),
67
+ (r"expect\s*\(\s*1\s*\)\s*\.toBe\s*\(\s*1\s*\)", "assert 1 === 1"),
68
+ (r"assert\s+True\b", "assert True (Python)"),
69
+ (r"assert\s+1\s*==\s*1", "assert 1 == 1"),
70
+ ]
71
+ for pat, label in fake_patterns:
72
+ if re.search(pat, content):
73
+ issues.append(f"FAKE: {label}")
74
+
75
+ # === BOILERPLATE-ONLY (v4, kept) ===
76
+ type_checks = len(re.findall(
77
+ r"(typeof\s+\w+|instanceof\s+\w+|toBeDefined|toBeInstanceOf|\.type\b)", content))
78
+ behavior_checks = len(re.findall(
79
+ r"(toEqual|toContain|toMatch|toThrow|rejects|resolves|toHaveBeenCalledWith|"
80
+ r"toHaveProperty|toHaveLength|toBeGreaterThan|toBeLessThan|assert.*==|"
81
+ r"assertEqual|assertIn|assertRaises|assert_called_with)", content))
82
+
83
+ if type_checks > 3 and behavior_checks == 0:
84
+ issues.append("BOILERPLATE: Only checks types/existence, never tests actual behavior")
85
+
86
+ # === HAPPY PATH ONLY (v4, kept) ===
87
+ has_error_tests = bool(re.search(
88
+ r"(toThrow|rejects|assertRaises|error|invalid|empty|null|undefined|"
89
+ r"edge.case|boundary|overflow|timeout|unauthorized|forbidden|not.found|"
90
+ r"bad.request|missing|malformed)", content, re.IGNORECASE))
91
+ test_count = len(re.findall(r"(test|it|describe)\s*\(", content))
92
+
93
+ if test_count >= 3 and not has_error_tests:
94
+ issues.append("HAPPY PATH ONLY: No error/edge case tests. "
95
+ "What happens with bad input? Unauthorized? Empty data?")
96
+
97
+ # === NO ASSERTIONS — JS style (v3, kept) ===
98
+ test_bodies = re.findall(
99
+ r"(?:test|it)\s*\([^)]+,\s*(?:async\s*)?\(\)\s*=>\s*\{([^}]*)\}",
100
+ content, re.DOTALL)
101
+ for body in test_bodies:
102
+ if body.strip() and not re.search(
103
+ r"(expect|assert|should|verify|check|toBe|toEqual|toThrow|toHave)",
104
+ body, re.IGNORECASE):
105
+ issues.append("EMPTY: Test body has no assertions")
106
+ break
107
+
108
+ # === OVER-MOCKED (v3, kept) ===
109
+ mock_count = len(re.findall(
110
+ r"(jest\.mock|mock\(|patch\(|MagicMock|stub\(|sinon\.stub)", content))
111
+ if mock_count > 5 and behavior_checks <= 1:
112
+ issues.append("OVER-MOCKED: Heavy mocking but barely tests real behavior")
113
+
114
+ # ============================================================
115
+ # v5 NEW PATTERNS
116
+ # ============================================================
117
+
118
+ # === SKIP / IGNORE TESTS (v5) ===
119
+ skip_patterns = [
120
+ (r"@pytest\.mark\.skip", "@pytest.mark.skip"),
121
+ (r"@pytest\.mark\.skipIf", "@pytest.mark.skipIf"),
122
+ (r"@unittest\.skip", "@unittest.skip"),
123
+ (r"\bit\.skip\s*\(", "it.skip()"),
124
+ (r"\bdescribe\.skip\s*\(", "describe.skip()"),
125
+ (r"\bxit\s*\(", "xit()"),
126
+ (r"\bxdescribe\s*\(", "xdescribe()"),
127
+ ]
128
+ for pat, label in skip_patterns:
129
+ if re.search(pat, content):
130
+ issues.append(f"SKIP: {label} — skipped tests hide failures")
131
+
132
+ # === ASSERTION-FREE Python tests (v5) ===
133
+ _detect_assertion_free_python(content, issues)
134
+
135
+ # === EMPTY TEST BODY — Python (v5) ===
136
+ _detect_empty_python_test_body(content, issues)
137
+
138
+ # === MOCK-HEAVY (v5, ratio-based refinement) ===
139
+ # Different from OVER-MOCKED: catches moderate mock counts with poor assertion ratio
140
+ assertion_count = len(re.findall(
141
+ r"(\bassert\b|\bexpect\s*\(|\.should\b|\bverify\s*\()", content))
142
+ if mock_count >= 3 and mock_count <= 5 and assertion_count < mock_count / 2:
143
+ issues.append(
144
+ f"MOCK-HEAVY: {mock_count} mocks but only {assertion_count} assertions "
145
+ f"— tests should verify behavior, not just mock dependencies")
146
+
147
+ # === PARAMETERIZED TEST GAP (v5) ===
148
+ _detect_parameterized_gap(content, issues)
149
+
150
+ return issues
151
+
152
+
153
+ def _extract_python_test_bodies(content):
154
+ """
155
+ Extract Python test function names and their body text.
156
+ Returns list of (test_name, body_text) tuples.
157
+ """
158
+ results = []
159
+ test_defs = list(re.finditer(r'def\s+(test_\w+)\s*\([^)]*\)\s*:', content))
160
+
161
+ for idx, m in enumerate(test_defs):
162
+ body_start = m.end()
163
+ # Body extends to next def at same indent level, or EOF
164
+ if idx + 1 < len(test_defs):
165
+ body_end = test_defs[idx + 1].start()
166
+ else:
167
+ body_end = len(content)
168
+
169
+ raw_body = content[body_start:body_end]
170
+ # Keep only indented lines (the actual function body)
171
+ body_lines = []
172
+ for line in raw_body.split('\n'):
173
+ stripped = line.strip()
174
+ if not stripped:
175
+ continue
176
+ if line and (line[0] == ' ' or line[0] == '\t'):
177
+ body_lines.append(stripped)
178
+ elif body_lines:
179
+ # Non-indented non-empty line after body started = end of function
180
+ break
181
+
182
+ results.append((m.group(1), body_lines))
183
+
184
+ return results
185
+
186
+
187
+ def _detect_assertion_free_python(content, issues):
188
+ """Detect Python test functions with no assertion keywords."""
189
+ for test_name, body_lines in _extract_python_test_bodies(content):
190
+ if not body_lines:
191
+ continue # Empty bodies caught by _detect_empty_python_test_body
192
+ body_text = ' '.join(body_lines)
193
+ if not re.search(
194
+ r'(\bassert\b|\bexpect\s*\(|\.should\b|\bverify\s*\()',
195
+ body_text, re.IGNORECASE
196
+ ):
197
+ # Skip bodies that are just pass/ellipsis (caught by empty body detector)
198
+ non_trivial = [l for l in body_lines
199
+ if l not in ('pass', '...') and not l.startswith('#')]
200
+ if non_trivial:
201
+ issues.append(
202
+ f"ASSERTION-FREE: {test_name} has no assertions")
203
+
204
+
205
+ def _detect_empty_python_test_body(content, issues):
206
+ """Detect Python test functions with empty bodies (pass, ..., comment-only)."""
207
+ for test_name, body_lines in _extract_python_test_bodies(content):
208
+ non_trivial = [l for l in body_lines
209
+ if l not in ('pass', '...') and not l.startswith('#')]
210
+ if not non_trivial:
211
+ issues.append(f"EMPTY: {test_name} has empty body (only pass/ellipsis/comments)")
212
+
213
+
214
+ def _detect_parameterized_gap(content, issues):
215
+ """
216
+ Detect functions called 3+ times with different literal arguments,
217
+ suggesting @pytest.mark.parametrize would be more appropriate.
218
+ """
219
+ # Find function calls with literal arguments (numbers or strings)
220
+ calls = re.findall(
221
+ r'\b(\w+)\s*\(\s*(\d+(?:\.\d+)?|"[^"]*"|\'[^\']*\')\s*(?:\)|,)',
222
+ content)
223
+
224
+ # Group by function name, collect unique literal args
225
+ call_groups = {}
226
+ for func, arg in calls:
227
+ if func.lower() not in _BUILTIN_FUNCS:
228
+ call_groups.setdefault(func, set()).add(arg)
229
+
230
+ for func, args in call_groups.items():
231
+ if len(args) >= 6:
232
+ issues.append(
233
+ f"PARAMETERIZED: '{func}' called with {len(args)} different "
234
+ f"literal values — consider @pytest.mark.parametrize")
235
+
236
+
237
+ def persist_metrics(project_dir, analysis):
238
+ """
239
+ Write test quality metrics to .omg/state/test-metrics.json.
240
+
241
+ Args:
242
+ project_dir: Project root directory.
243
+ analysis: Dict with keys: total_tests, fake_count, boilerplate_count,
244
+ edge_case_count, skip_count, assertion_free_count.
245
+ """
246
+ try:
247
+ state_dir = os.path.join(project_dir, ".omg", "state")
248
+ os.makedirs(state_dir, exist_ok=True)
249
+
250
+ total = analysis.get("total_tests", 0)
251
+ issue_sum = (
252
+ analysis.get("fake_count", 0)
253
+ + analysis.get("boilerplate_count", 0)
254
+ + analysis.get("skip_count", 0)
255
+ + analysis.get("assertion_free_count", 0)
256
+ )
257
+
258
+ # Quality score: 1.0 = perfect, 0.0 = all tests problematic
259
+ if total > 0:
260
+ quality_score = round(max(0.0, 1.0 - (issue_sum / total)), 3)
261
+ else:
262
+ quality_score = 1.0
263
+
264
+ metrics = {
265
+ "ts": datetime.now(timezone.utc).isoformat(),
266
+ "total_tests": analysis.get("total_tests", 0),
267
+ "fake_count": analysis.get("fake_count", 0),
268
+ "boilerplate_count": analysis.get("boilerplate_count", 0),
269
+ "edge_case_count": analysis.get("edge_case_count", 0),
270
+ "skip_count": analysis.get("skip_count", 0),
271
+ "assertion_free_count": analysis.get("assertion_free_count", 0),
272
+ "quality_score": quality_score,
273
+ }
274
+
275
+ metrics_path = os.path.join(state_dir, "test-metrics.json")
276
+ with open(metrics_path, "w", encoding="utf-8") as f:
277
+ json.dump(metrics, f, separators=(",", ":"))
278
+ except Exception:
279
+ pass # Crash isolation: never fail the hook
280
+
281
+
282
+ def _is_test_file(rel_path):
283
+ """
284
+ Heuristic: is this path likely a test file (vs source module)?
285
+
286
+ Excludes build artifacts and source modules that happen to start with
287
+ ``test_`` but live outside test directories (e.g. ``runtime/test_intent_lock.py``).
288
+ """
289
+ parts = rel_path.replace("\\", "/").split("/")
290
+ # Skip build artifacts and vendored code
291
+ if any(seg in _SKIP_DIR_SEGMENTS for seg in parts[:-1]):
292
+ return False
293
+ basename = parts[-1].lower() if parts else ""
294
+ # Standard test file patterns (checked against basename)
295
+ if any(p in basename for p in (".test.", ".spec.", "_test.", ".tests.")):
296
+ return True
297
+ # __tests__ directory anywhere in path
298
+ if "__tests__" in rel_path:
299
+ return True
300
+ # test_ prefix: only match if file is in a test directory or at repo root
301
+ if basename.startswith("test_"):
302
+ parent_dirs = {p.lower() for p in parts[:-1]}
303
+ return not parent_dirs or bool(parent_dirs & _TEST_DIR_NAMES)
304
+ return False
305
+
306
+
307
+ def check_test_quality(data, project_dir):
308
+ """Core test-quality validation. Returns list of block-reason strings."""
309
+ import subprocess
310
+
311
+ # Find recently modified test files
312
+ test_files = []
313
+ try:
314
+ result = subprocess.run(
315
+ ["git", "diff", "--name-only", "--diff-filter=AM"],
316
+ capture_output=True, text=True, timeout=10, cwd=project_dir
317
+ )
318
+ for f in result.stdout.strip().split("\n"):
319
+ if f and _is_test_file(f):
320
+ full = os.path.join(project_dir, f)
321
+ if os.path.exists(full):
322
+ test_files.append(full)
323
+ except Exception:
324
+ pass
325
+
326
+ if not test_files:
327
+ return []
328
+
329
+ warnings = []
330
+ # Aggregate metrics across all files
331
+ agg = {
332
+ "total_tests": 0, "fake_count": 0, "boilerplate_count": 0,
333
+ "edge_case_count": 0, "skip_count": 0, "assertion_free_count": 0,
334
+ }
335
+
336
+ for tf in test_files:
337
+ try:
338
+ with open(tf, "r", encoding="utf-8", errors="ignore") as f:
339
+ content = f.read()
340
+ except Exception:
341
+ continue
342
+
343
+ filename = os.path.basename(tf)
344
+ issues = analyze_test_content(content, filename)
345
+
346
+ # Count tests in this file
347
+ py_tests = len(re.findall(r'def\s+test_\w+\s*\(', content))
348
+ js_tests = len(re.findall(r'(test|it)\s*\(', content))
349
+ agg["total_tests"] += py_tests + js_tests
350
+
351
+ # Count issue categories
352
+ for issue in issues:
353
+ if issue.startswith("FAKE:"):
354
+ agg["fake_count"] += 1
355
+ elif issue.startswith("BOILERPLATE:"):
356
+ agg["boilerplate_count"] += 1
357
+ elif "HAPPY PATH" in issue:
358
+ agg["edge_case_count"] += 1
359
+ elif issue.startswith("SKIP:"):
360
+ agg["skip_count"] += 1
361
+ elif issue.startswith("ASSERTION-FREE:"):
362
+ agg["assertion_free_count"] += 1
363
+
364
+ if issues:
365
+ warnings.append(f"{filename}: " + "; ".join(issues))
366
+
367
+ # Persist metrics
368
+ try:
369
+ persist_metrics(project_dir, agg)
370
+ except Exception:
371
+ pass
372
+
373
+ if warnings:
374
+ msg = "TEST QUALITY ISSUES:\n" + "\n".join(f" {w}" for w in warnings)
375
+ msg += ("\n\nTests should verify what USERS need, not just that code exists.\n"
376
+ "Ask: 'What does the user expect to happen? What could go wrong?'\n"
377
+ "Write tests for those scenarios.")
378
+ return [msg]
379
+
380
+ return []
381
+
382
+
383
+ def _is_mutation_capable_bash_for_methodology(command):
384
+ """Check if a bash command is truly mutation-capable for methodology enforcement.
385
+ File-write redirections of read-only commands are excluded."""
386
+ import re as _re
387
+ lowered = command.strip().lower()
388
+ _m_patterns = (
389
+ r"\b(git\s+(add|commit|push|rebase|cherry-pick|merge|tag(?!\s+(-l|--list)\b)))\b",
390
+ r"\b(rm|mv|cp|tee|touch|mkdir|rmdir|ln)\b",
391
+ r"\b(sed\s+-i|perl\s+-pi)\b",
392
+ r"\b(chmod|chown)\b",
393
+ )
394
+ for pattern in _m_patterns:
395
+ if _re.search(pattern, lowered):
396
+ return True
397
+ return False
398
+
399
+
400
+ def check_methodology_contract(data):
401
+ if not isinstance(data, dict):
402
+ return []
403
+
404
+ tool_name = str(data.get("tool_name", "")).strip().lower()
405
+ if tool_name not in _MUTATION_TOOLS:
406
+ return []
407
+
408
+ # For Bash tools, only enforce methodology on actually mutation-capable commands.
409
+ # Read-only commands (git status, ls, python3 script.py) and commands that only
410
+ # redirect output (python3 script.py > file.json) should not require done_when.
411
+ if tool_name == "bash":
412
+ tool_input_raw = data.get("tool_input")
413
+ cmd = str((tool_input_raw or {}).get("command", "")).strip() if isinstance(tool_input_raw, dict) else ""
414
+ if not cmd or not _is_mutation_capable_bash_for_methodology(cmd):
415
+ return []
416
+
417
+ tool_input = data.get("tool_input")
418
+ if not isinstance(tool_input, dict):
419
+ return ["METHODOLOGY: mutation-capable flow requires metadata with done_when criteria"]
420
+
421
+ exemption = str(tool_input.get("exemption", "")).strip().lower()
422
+ if exemption == "docs":
423
+ return []
424
+
425
+ metadata = tool_input.get("metadata")
426
+ if not isinstance(metadata, dict):
427
+ return [] # No metadata provided -- do not block; firewall handles actual mutation gates
428
+
429
+ done_when = metadata.get("done_when")
430
+ if isinstance(done_when, str) and done_when.strip():
431
+ return []
432
+ if isinstance(done_when, list):
433
+ if any(str(item).strip() for item in done_when):
434
+ return []
435
+ if isinstance(done_when, dict):
436
+ criteria = done_when.get("criteria")
437
+ if isinstance(criteria, str) and criteria.strip():
438
+ return []
439
+ if isinstance(criteria, list) and any(str(item).strip() for item in criteria):
440
+ return []
441
+ if str(done_when.get("summary", "")).strip():
442
+ return []
443
+
444
+ return ["METHODOLOGY: done_when criteria required before mutation-capable execution"]
445
+
446
+
447
+ # Standalone execution (backward compat: invoked directly by hook runner)
448
+ if __name__ == "__main__":
449
+ try:
450
+ data = json.load(sys.stdin)
451
+ except (json.JSONDecodeError, EOFError):
452
+ sys.exit(0)
453
+
454
+ if should_skip_stop_hooks(data):
455
+ sys.exit(0)
456
+
457
+ project_dir = _resolve_project_dir()
458
+ blocks = check_test_quality(data, project_dir)
459
+ blocks.extend(check_methodology_contract(data))
460
+ if blocks:
461
+ json.dump({"decision": "block", "reason": blocks[0]}, sys.stdout)
462
+ sys.exit(0)
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostToolUse Hook: Test Generation Suggestion
4
+ Suggests test generation when source files are modified without corresponding tests.
5
+ Feature-gated under TEST_GENERATION flag.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import importlib.util
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+
15
+ HOOKS_DIR = os.path.dirname(os.path.abspath(__file__))
16
+
17
+
18
+ def _load_common():
19
+ path = os.path.join(HOOKS_DIR, "_common.py")
20
+ spec = importlib.util.spec_from_file_location("_common", path)
21
+ if spec is None or spec.loader is None:
22
+ raise RuntimeError("Unable to load _common.py")
23
+ module = importlib.util.module_from_spec(spec)
24
+ spec.loader.exec_module(module)
25
+ return module
26
+
27
+
28
+ _common = _load_common()
29
+ setup_crash_handler = _common.setup_crash_handler
30
+ json_input = _common.json_input
31
+ get_feature_flag = _common.get_feature_flag
32
+ get_project_dir = _common.get_project_dir
33
+
34
+ # Tool names that modify files
35
+ WRITE_TOOLS = frozenset({"Write", "Edit", "MultiEdit"})
36
+
37
+ # Patterns indicating a file is a test file
38
+ TEST_FILE_PATTERNS = [
39
+ r"(?:^|/)test_", # test_ prefix (Python convention)
40
+ r"_test\.", # _test suffix before extension
41
+ r"\.spec\.", # .spec. (JS/TS convention)
42
+ r"\.test\.", # .test. (JS/TS convention)
43
+ r"(?:^|/)tests/", # inside tests/ directory
44
+ r"(?:^|/)test/", # inside test/ directory
45
+ r"(?:^|/)__tests__/", # inside __tests__/ directory
46
+ ]
47
+
48
+ _TEST_RE = re.compile("|".join(TEST_FILE_PATTERNS))
49
+
50
+
51
+ def _is_test_file(file_path: str) -> bool:
52
+ """Return True if the file path looks like a test file."""
53
+ normalized = file_path.replace("\\", "/")
54
+ return bool(_TEST_RE.search(normalized))
55
+
56
+
57
+ def _find_corresponding_test(file_path: str, project_dir: str) -> bool:
58
+ """Return True if a corresponding test file exists for the given source file."""
59
+ basename = os.path.basename(file_path)
60
+ name, ext = os.path.splitext(basename)
61
+ dir_part = os.path.dirname(file_path)
62
+
63
+ # Python: tests/test_{basename}
64
+ if ext == ".py":
65
+ candidates = [
66
+ os.path.join(project_dir, "tests", f"test_{basename}"),
67
+ os.path.join(project_dir, dir_part, "tests", f"test_{basename}"),
68
+ ]
69
+ # JS/TS: same dir + .test.{ext} or .spec.{ext}
70
+ elif ext in (".js", ".ts", ".jsx", ".tsx", ".mjs"):
71
+ candidates = [
72
+ os.path.join(project_dir, dir_part, f"{name}.test{ext}"),
73
+ os.path.join(project_dir, dir_part, f"{name}.spec{ext}"),
74
+ ]
75
+ else:
76
+ # Generic: check tests/test_{name}{ext}
77
+ candidates = [
78
+ os.path.join(project_dir, "tests", f"test_{basename}"),
79
+ ]
80
+
81
+ return any(os.path.exists(c) for c in candidates)
82
+
83
+
84
+ def main() -> None:
85
+ setup_crash_handler("test-generator-hook", fail_closed=False)
86
+
87
+ payload = json_input()
88
+
89
+ # Gate: feature flag
90
+ if not get_feature_flag("TEST_GENERATION", default=False):
91
+ sys.exit(0)
92
+
93
+ # Gate: only file-modifying tools
94
+ tool_name = payload.get("tool_name", "")
95
+ if tool_name not in WRITE_TOOLS:
96
+ sys.exit(0)
97
+
98
+ # Extract file path (file_path or path)
99
+ tool_input = payload.get("tool_input", {})
100
+ file_path = tool_input.get("file_path") or tool_input.get("path", "")
101
+ if not file_path:
102
+ sys.exit(0)
103
+
104
+ # Gate: skip test files
105
+ if _is_test_file(file_path):
106
+ sys.exit(0)
107
+
108
+ # Check for corresponding test file
109
+ project_dir = get_project_dir()
110
+ if _find_corresponding_test(file_path, project_dir):
111
+ sys.exit(0)
112
+
113
+ # Inject suggestion
114
+ suggestion = (
115
+ f"No test file found for {file_path}. "
116
+ "Consider running /OMG:testgen to generate tests."
117
+ )
118
+ json.dump({"additionalContext": suggestion}, sys.stdout)
119
+ sys.exit(0)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()