@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,761 @@
1
+ """Shared utilities for OMG hooks. Pure stdlib — no external deps."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ import fcntl
8
+ import site
9
+ from contextlib import contextmanager
10
+ from datetime import datetime, timedelta, timezone
11
+ from pathlib import Path
12
+
13
+ # --- Stop-Block Loop Breaker ---
14
+ _STOP_BLOCK_TRACKER = ".omg/state/ledger/.stop-block-tracker.json"
15
+ # Max seconds between blocks to consider it a loop
16
+ _BLOCK_LOOP_WINDOW_SECS = 30
17
+ # How many consecutive blocks before we skip
18
+ _BLOCK_LOOP_THRESHOLD = 2
19
+ # Block reasons that indicate a loop scenario (Guard 5 skip-eligible)
20
+ _LOOP_BLOCK_REASONS = {"planning_gate", "ralph_loop", "quality_check", "block_decision", "unknown"}
21
+
22
+ # --- Performance Budget Constants ---
23
+ PRE_TOOL_INJECT_MAX_MS = 100
24
+ STOP_CHECK_MAX_MS = 15000
25
+ STOP_DISPATCHER_TOTAL_MAX_MS = 90000
26
+
27
+
28
+ def _managed_site_packages(runtime_root: Path) -> list[Path]:
29
+ venv_root = runtime_root / ".venv"
30
+ if not venv_root.is_dir():
31
+ return []
32
+
33
+ candidates: list[Path] = []
34
+ for pattern in ("lib/python*/site-packages", "Lib/site-packages"):
35
+ for path in venv_root.glob(pattern):
36
+ if path.is_dir():
37
+ candidates.append(path.resolve())
38
+ return candidates
39
+
40
+
41
+ def bootstrap_runtime_paths(anchor: str | os.PathLike[str] | None = None) -> None:
42
+ """Add the repo root or portable omg-runtime root to ``sys.path``.
43
+
44
+ Installed hooks live under ``~/.claude/hooks`` while the portable runtime is
45
+ provisioned under ``~/.claude/omg-runtime``. Repo-local execution instead
46
+ keeps ``hooks/``, ``runtime/``, ``lab/`` and related packages side by side.
47
+ This helper resolves both layouts and is safe to call repeatedly.
48
+ """
49
+ _claude_dir = Path(os.path.expanduser("~/.claude"))
50
+ if (_claude_dir / ".omg-uninstalling").exists():
51
+ sys.exit(0)
52
+
53
+ anchor_path = Path(anchor).resolve() if anchor is not None else Path(__file__).resolve()
54
+ hooks_dir = anchor_path.parent
55
+ parent_dir = hooks_dir.parent
56
+
57
+ candidates: list[Path] = [hooks_dir]
58
+ for candidate in (
59
+ parent_dir,
60
+ parent_dir / "omg-runtime",
61
+ hooks_dir / "omg-runtime",
62
+ ):
63
+ if candidate not in candidates:
64
+ candidates.append(candidate)
65
+
66
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "").strip()
67
+ if project_dir:
68
+ project_path = Path(project_dir).resolve()
69
+ for candidate in (
70
+ project_path,
71
+ project_path / "omg-runtime",
72
+ ):
73
+ if candidate not in candidates:
74
+ candidates.append(candidate)
75
+
76
+ for candidate in candidates:
77
+ candidate_str = str(candidate)
78
+ if candidate.is_dir() and candidate_str not in sys.path:
79
+ sys.path.insert(0, candidate_str)
80
+ for site_packages in _managed_site_packages(candidate):
81
+ site.addsitedir(str(site_packages))
82
+
83
+
84
+ bootstrap_runtime_paths()
85
+
86
+ def json_input():
87
+ """Parse JSON from stdin. Returns dict or exits 0 on parse failure."""
88
+ try:
89
+ return json.load(sys.stdin)
90
+ except (json.JSONDecodeError, EOFError):
91
+ sys.exit(0)
92
+
93
+
94
+ def get_project_dir():
95
+ """Get project directory from env or cwd."""
96
+ return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
97
+
98
+
99
+ def _resolve_project_dir():
100
+ """Get and validate project directory; warns if .omg/ missing."""
101
+ path = get_project_dir()
102
+ if not os.path.isdir(os.path.join(path, ".omg")):
103
+ print(f"[OMG] Warning: .omg/ not found in {path}", file=sys.stderr)
104
+ return path
105
+
106
+ def deny_decision(reason):
107
+ """Emit a PreToolUse deny decision to stdout."""
108
+ json.dump({
109
+ "hookSpecificOutput": {
110
+ "hookEventName": "PreToolUse",
111
+ "permissionDecision": "deny",
112
+ "permissionDecisionReason": reason,
113
+ }
114
+ }, sys.stdout)
115
+
116
+
117
+ def block_decision(reason, *, block_reason="unknown", project_dir=None):
118
+ """Emit a Stop hook block decision to stdout.
119
+
120
+ Also records the block for loop detection. Every stop hook that calls
121
+ block_decision() contributes to the loop breaker counter, so deadlocks
122
+ are detected regardless of which specific hook triggers the block.
123
+ """
124
+ # Record block BEFORE emitting -- ensures tracker is updated even if
125
+ # the process is killed after emitting the decision.
126
+ try:
127
+ record_stop_block(project_dir=project_dir, reason=block_reason)
128
+ except Exception:
129
+ pass # never let tracker failure prevent the block decision
130
+ json.dump({"decision": "block", "reason": reason}, sys.stdout)
131
+
132
+
133
+ def setup_crash_handler(hook_name, fail_closed=False):
134
+ """Install a crash handler that prevents non-zero exits.
135
+
136
+ fail_closed=True: emit deny on crash (for security hooks like firewall, secret-guard)
137
+ fail_closed=False: silently exit 0 (for non-security hooks)
138
+ """
139
+ def _excepthook(exc_type, exc_val, exc_tb):
140
+ print(f"OMG hook error ({hook_name}): {exc_val}", file=sys.stderr)
141
+ log_hook_error(hook_name, exc_val)
142
+ if fail_closed:
143
+ try:
144
+ deny_decision(f"OMG {hook_name} crash: {exc_val}. Denying for safety.")
145
+ except Exception:
146
+ pass
147
+ os._exit(0)
148
+ sys.excepthook = _excepthook
149
+
150
+
151
+ def read_file_safe(path, max_bytes=2000):
152
+ """Read file content safely, returning None on any failure."""
153
+ try:
154
+ if not os.path.exists(path):
155
+ return None
156
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
157
+ text = f.read(max_bytes).strip()
158
+ return text or None
159
+ except Exception:
160
+ return None
161
+
162
+
163
+ def log_hook_error(hook_name, error, context=None):
164
+ """Log hook error to .omg/state/ledger/hook-errors.jsonl with file locking.
165
+
166
+ Args:
167
+ hook_name: Name of the hook that errored
168
+ error: Exception or error message
169
+ context: Optional dict with additional context
170
+
171
+ Silently fails if logging cannot be completed (crash isolation).
172
+ """
173
+ try:
174
+ project_dir = get_project_dir()
175
+ ledger_dir = os.path.join(project_dir, ".omg", "state", "ledger")
176
+ os.makedirs(ledger_dir, exist_ok=True)
177
+
178
+ ledger_path = os.path.join(ledger_dir, "hook-errors.jsonl")
179
+ entry = {
180
+ "ts": datetime.now(timezone.utc).isoformat(),
181
+ "hook": hook_name,
182
+ "error": str(error),
183
+ }
184
+ if context:
185
+ entry["context"] = context
186
+ line = json.dumps(entry, separators=(",", ":")) + "\n"
187
+
188
+ try:
189
+ with _locked_path(ledger_path):
190
+ if os.path.exists(ledger_path) and os.path.getsize(ledger_path) > 100 * 1024:
191
+ archive = ledger_path + ".1"
192
+ if os.path.exists(archive):
193
+ try:
194
+ os.remove(archive)
195
+ except OSError:
196
+ pass
197
+ try:
198
+ os.rename(ledger_path, archive)
199
+ except OSError:
200
+ pass
201
+
202
+ fd = os.open(
203
+ ledger_path,
204
+ os.O_WRONLY | os.O_CREAT | os.O_APPEND | _O_NOFOLLOW_HOOKS,
205
+ 0o600,
206
+ )
207
+ with os.fdopen(fd, "a", encoding="utf-8") as handle:
208
+ handle.seek(0, os.SEEK_END)
209
+ handle.write(line)
210
+ handle.flush()
211
+ os.fsync(handle.fileno())
212
+ except Exception as e:
213
+ print(f"[OMG] _common.py: {type(e).__name__}: {e}", file=sys.stderr)
214
+ except Exception as e:
215
+ print(f"[OMG] _common.py: {type(e).__name__}: {e}", file=sys.stderr)
216
+
217
+
218
+ _O_NOFOLLOW_HOOKS: int = getattr(os, "O_NOFOLLOW", 0)
219
+
220
+
221
+ def _fsync_dir_hooks(dirpath):
222
+ fd = os.open(dirpath, os.O_RDONLY)
223
+ try:
224
+ os.fsync(fd)
225
+ finally:
226
+ os.close(fd)
227
+
228
+
229
+ @contextmanager
230
+ def _locked_path(path):
231
+ lock_path = path + ".lock"
232
+ fd = os.open(lock_path, os.O_RDWR | os.O_CREAT | _O_NOFOLLOW_HOOKS, 0o600)
233
+ try:
234
+ fcntl.flock(fd, fcntl.LOCK_EX)
235
+ yield lock_path
236
+ finally:
237
+ try:
238
+ fcntl.flock(fd, fcntl.LOCK_UN)
239
+ finally:
240
+ os.close(fd)
241
+
242
+
243
+ def atomic_json_write(path, data):
244
+ try:
245
+ parent = os.path.dirname(path)
246
+ if parent:
247
+ os.makedirs(parent, exist_ok=True)
248
+
249
+ if os.path.islink(path):
250
+ raise OSError(f"Symlink target rejected: {path}")
251
+
252
+ tmp_path = path + ".tmp"
253
+ if os.path.islink(tmp_path):
254
+ raise OSError(f"Symlink tmp path rejected: {tmp_path}")
255
+ if os.path.exists(tmp_path):
256
+ os.unlink(tmp_path)
257
+
258
+ content = json.dumps(data, separators=(",", ":")).encode("utf-8")
259
+ open_flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | _O_NOFOLLOW_HOOKS
260
+ fd = os.open(tmp_path, open_flags, 0o600)
261
+ try:
262
+ written = 0
263
+ while written < len(content):
264
+ written += os.write(fd, content[written:])
265
+ os.fsync(fd)
266
+ except BaseException:
267
+ os.close(fd)
268
+ try:
269
+ os.unlink(tmp_path)
270
+ except OSError:
271
+ pass
272
+ raise
273
+ else:
274
+ os.close(fd)
275
+
276
+ os.replace(tmp_path, path)
277
+ _fsync_dir_hooks(parent or ".")
278
+ except Exception as e:
279
+ print(f"[OMG] _common.py: {type(e).__name__}: {e}", file=sys.stderr)
280
+
281
+
282
+ def write_checklist_session(project_dir, session_id=None):
283
+ """Persist session metadata for the active planning checklist."""
284
+ if not session_id:
285
+ session_id = _get_session_id()
286
+ sidecar = os.path.join(project_dir, ".omg", "state", "_checklist.session")
287
+ atomic_json_write(sidecar, {
288
+ "session_id": session_id,
289
+ "created_at": datetime.now(timezone.utc).isoformat(),
290
+ })
291
+
292
+
293
+ def read_checklist_session(project_dir):
294
+ """Read planning checklist session metadata."""
295
+ sidecar = os.path.join(project_dir, ".omg", "state", "_checklist.session")
296
+ try:
297
+ if os.path.exists(sidecar):
298
+ with open(sidecar, "r", encoding="utf-8") as f:
299
+ payload = json.load(f)
300
+ if isinstance(payload, dict):
301
+ return payload
302
+ except Exception:
303
+ pass
304
+ return None
305
+
306
+
307
+ def _parse_hook_timestamp(raw_value: str):
308
+ text = str(raw_value or "").strip()
309
+ if not text:
310
+ return None
311
+ try:
312
+ parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
313
+ except ValueError:
314
+ return None
315
+ if parsed.tzinfo is None:
316
+ parsed = parsed.replace(tzinfo=timezone.utc)
317
+ return parsed.astimezone(timezone.utc)
318
+
319
+
320
+ def has_recent_tool_activity(project_dir, since_minutes=60):
321
+ """Summarize recent tool-ledger activity for planning-gate advisories."""
322
+ result = {"has_writes": False, "has_tests": False, "tool_count": 0}
323
+ ledger = os.path.join(project_dir, ".omg", "state", "ledger", "tool-ledger.jsonl")
324
+ if not os.path.exists(ledger):
325
+ return result
326
+ try:
327
+ cutoff = datetime.now(timezone.utc) - timedelta(minutes=since_minutes)
328
+ with open(ledger, "r", encoding="utf-8", errors="ignore") as f:
329
+ for line in f:
330
+ try:
331
+ entry = json.loads(line.strip())
332
+ except (json.JSONDecodeError, TypeError, ValueError):
333
+ continue
334
+ if not isinstance(entry, dict):
335
+ continue
336
+ ts = _parse_hook_timestamp(entry.get("ts", ""))
337
+ if ts is not None and ts < cutoff:
338
+ continue
339
+ result["tool_count"] += 1
340
+ tool = str(entry.get("tool", ""))
341
+ if tool in ("Write", "Edit", "MultiEdit"):
342
+ result["has_writes"] = True
343
+ command = str(entry.get("command", "")).lower()
344
+ if any(kw in command for kw in ("test", "lint", "check", "build", "pytest", "jest", "vitest")):
345
+ result["has_tests"] = True
346
+ except OSError:
347
+ pass
348
+ return result
349
+
350
+
351
+ # Feature flags cache — read settings.json once per hook invocation
352
+ _FEATURE_CACHE = {}
353
+ _settings_preset = None
354
+ _MANAGED_PRESET_FLAGS = {
355
+ "SETUP",
356
+ "SETUP_WIZARD",
357
+ "MEMORY_AUTOSTART",
358
+ "SESSION_ANALYTICS",
359
+ "CONTEXT_MANAGER",
360
+ "COST_TRACKING",
361
+ "MEMORY_SERVER",
362
+ "GIT_WORKFLOW",
363
+ "TEST_GENERATION",
364
+ "DEP_HEALTH",
365
+ "CODEBASE_VIZ",
366
+ "DATA_ENFORCEMENT",
367
+ "WEB_ENFORCEMENT",
368
+ "TERMS_ENFORCEMENT",
369
+ "COUNCIL_ROUTING",
370
+ "FORGE_ALL_DOMAINS",
371
+ "NOTEBOOKLM",
372
+ }
373
+ _PRESET_FEATURES = {
374
+ "safe": {flag: False for flag in _MANAGED_PRESET_FLAGS},
375
+ "balanced": {
376
+ "SETUP": True,
377
+ "SETUP_WIZARD": True,
378
+ "MEMORY_AUTOSTART": True,
379
+ "SESSION_ANALYTICS": True,
380
+ "CONTEXT_MANAGER": True,
381
+ "COST_TRACKING": True,
382
+ "MEMORY_SERVER": False,
383
+ "GIT_WORKFLOW": False,
384
+ "TEST_GENERATION": False,
385
+ "DEP_HEALTH": False,
386
+ "CODEBASE_VIZ": False,
387
+ },
388
+ "interop": {
389
+ "SETUP": True,
390
+ "SETUP_WIZARD": True,
391
+ "MEMORY_AUTOSTART": True,
392
+ "SESSION_ANALYTICS": True,
393
+ "CONTEXT_MANAGER": True,
394
+ "COST_TRACKING": True,
395
+ "MEMORY_SERVER": True,
396
+ "GIT_WORKFLOW": False,
397
+ "TEST_GENERATION": False,
398
+ "DEP_HEALTH": False,
399
+ "CODEBASE_VIZ": False,
400
+ },
401
+ "labs": {
402
+ "SETUP": True,
403
+ "SETUP_WIZARD": True,
404
+ "MEMORY_AUTOSTART": True,
405
+ "SESSION_ANALYTICS": True,
406
+ "CONTEXT_MANAGER": True,
407
+ "COST_TRACKING": True,
408
+ "MEMORY_SERVER": True,
409
+ "GIT_WORKFLOW": True,
410
+ "TEST_GENERATION": True,
411
+ "DEP_HEALTH": True,
412
+ "CODEBASE_VIZ": True,
413
+ "DATA_ENFORCEMENT": False,
414
+ "WEB_ENFORCEMENT": False,
415
+ "TERMS_ENFORCEMENT": False,
416
+ "COUNCIL_ROUTING": False,
417
+ "FORGE_ALL_DOMAINS": False,
418
+ "NOTEBOOKLM": False,
419
+ },
420
+ "buffet": {flag: True for flag in _MANAGED_PRESET_FLAGS},
421
+ "production": {flag: True for flag in _MANAGED_PRESET_FLAGS},
422
+ }
423
+ _FEATURE_ALIASES = {
424
+ "SETUP": ("SETUP", "SETUP_WIZARD"),
425
+ "SETUP_WIZARD": ("SETUP_WIZARD", "SETUP"),
426
+ }
427
+
428
+
429
+ def _load_feature_settings():
430
+ """Populate feature cache from settings.json and return the configured preset."""
431
+ global _settings_preset
432
+
433
+ _FEATURE_CACHE.clear()
434
+ _settings_preset = None
435
+ try:
436
+ settings_path = os.path.join(get_project_dir(), "settings.json")
437
+ if os.path.exists(settings_path):
438
+ with open(settings_path, "r", encoding="utf-8") as f:
439
+ settings = json.load(f)
440
+ omg = settings.get("_omg", {})
441
+ if isinstance(omg, dict):
442
+ features = omg.get("features", {})
443
+ if isinstance(features, dict):
444
+ _FEATURE_CACHE.update(features)
445
+ preset = omg.get("preset")
446
+ if isinstance(preset, str) and preset in _PRESET_FEATURES:
447
+ _settings_preset = preset
448
+ except Exception:
449
+ pass
450
+
451
+
452
+ def get_feature_flag(flag_name, default=True):
453
+ """Get feature flag value with resolution order: env var → settings.json → default.
454
+
455
+ Env var format: OMG_{FLAG_NAME.upper()}_ENABLED
456
+ Values: "0"/"false"/"no" → False, "1"/"true"/"yes" → True
457
+
458
+ Returns default on any error (missing settings.json, malformed JSON, etc).
459
+ """
460
+ # Check environment variable first
461
+ env_key = f"OMG_{flag_name.upper()}_ENABLED"
462
+ env_val = os.environ.get(env_key, "").lower()
463
+ if env_val in ("0", "false", "no"):
464
+ return False
465
+ if env_val in ("1", "true", "yes"):
466
+ return True
467
+
468
+ # Check settings.json (cached)
469
+ if not _FEATURE_CACHE:
470
+ _load_feature_settings()
471
+
472
+ env_preset = os.environ.get("OMG_PRESET", "").lower().strip()
473
+ lookup_names = _FEATURE_ALIASES.get(flag_name, (flag_name,))
474
+
475
+ # Env preset is a session-scoped override for managed flags.
476
+ if env_preset in _PRESET_FEATURES:
477
+ for name in lookup_names:
478
+ if name in _MANAGED_PRESET_FLAGS:
479
+ return _PRESET_FEATURES[env_preset].get(name, default)
480
+
481
+ for name in lookup_names:
482
+ if name in _FEATURE_CACHE:
483
+ return _FEATURE_CACHE[name]
484
+
485
+ if _settings_preset in _PRESET_FEATURES:
486
+ for name in lookup_names:
487
+ if name in _MANAGED_PRESET_FLAGS:
488
+ return _PRESET_FEATURES[_settings_preset].get(name, default)
489
+
490
+ return default
491
+
492
+
493
+ # Permission mode helpers
494
+ BYPASS_MODES = frozenset({"bypasspermissions", "dontask"})
495
+
496
+
497
+ def is_bypass_mode(data):
498
+ """Return True if the hook input indicates permission prompts should be skipped.
499
+
500
+ Claude Code passes ``permission_mode`` in the hook input. When the user
501
+ enables *bypass permissions* or *don't ask* mode, hooks should still
502
+ enforce hard denials (critical safety) but must NOT emit ``ask`` decisions
503
+ that would re-introduce confirmation prompts.
504
+ """
505
+ if not isinstance(data, dict):
506
+ return False
507
+ mode = (data.get("permission_mode") or "").lower().strip()
508
+ return mode in BYPASS_MODES
509
+
510
+
511
+ # --- Subagent & Context-Limit Detection ---
512
+
513
+ # Stop hook feedback markers injected by Claude Code when a stop hook blocks
514
+ _STOP_HOOK_FEEDBACK_PREFIX = "Stop hook feedback:"
515
+
516
+
517
+ def should_skip_stop_hooks(data):
518
+ """Return True if stop hooks should exit immediately without blocking.
519
+
520
+ Detects four conditions:
521
+ 1. stop_hook_active flag (Claude Code's built-in re-entry guard)
522
+ 2. Stop hook feedback loop (previous block was already injected,
523
+ agent couldn't respond — blocking again is futile)
524
+ 3. Context-limit / rate-limit stop (blocking these prevents compaction
525
+ or creates infinite retry loops — must allow stop to proceed)
526
+ 4. File-based loop breaker (if hooks blocked >= 2 times within 90s,
527
+ agent cannot resolve — likely context-limited)
528
+
529
+ Safe for all stop hooks to call at the top of main().
530
+ """
531
+ if not isinstance(data, dict):
532
+ return False
533
+
534
+ # Guard 1: Claude Code's built-in re-entry prevention
535
+ if data.get("stop_hook_active", False):
536
+ return True
537
+
538
+ # Guard 3: Context-limit and rate-limit stop detection
539
+ # When context is exhausted, Claude Code needs to stop so it can compact.
540
+ # Blocking these stops causes a deadlock: can't compact because can't stop,
541
+ # can't continue because context is full.
542
+ # Similarly, rate-limit stops (429/quota) must not be blocked or they loop.
543
+ stop_reason = str(data.get("stop_reason", data.get("stopReason", ""))).lower()
544
+ end_turn_reason = str(data.get("end_turn_reason", data.get("endTurnReason", ""))).lower()
545
+ failure_reason = str(data.get("failure_reason", data.get("failureReason", ""))).lower()
546
+ signal_text = " ".join(part for part in (stop_reason, end_turn_reason, failure_reason) if part)
547
+ context_limit_markers = (
548
+ "context window",
549
+ "token limit",
550
+ "too much context",
551
+ "context length exceeded",
552
+ "maximum context length",
553
+ "prompt is too long",
554
+ "request too large",
555
+ "input too long",
556
+ "context_limit",
557
+ "context overflow",
558
+ )
559
+ if any(marker in signal_text for marker in context_limit_markers):
560
+ print(
561
+ "[OMG] Context limit detected: allowing stop so compaction can proceed. "
562
+ "If this repeats, run /OMG:handoff and resume from .omg/state/handoff.md.",
563
+ file=sys.stderr,
564
+ )
565
+ return True
566
+
567
+ # Guard 2: Check transcript for stop-hook feedback loop
568
+ # If the last user message is stop hook feedback, the hooks already
569
+ # blocked once and the agent tried (and failed) to respond.
570
+ # Blocking again creates an unrecoverable loop.
571
+ transcript_path = data.get("transcript_path", "")
572
+ if transcript_path and os.path.exists(transcript_path):
573
+ try:
574
+ last_user_text = ""
575
+ with open(transcript_path, "r", encoding="utf-8", errors="ignore") as f:
576
+ for line in f:
577
+ line = line.strip()
578
+ if not line:
579
+ continue
580
+ try:
581
+ entry = json.loads(line)
582
+ except json.JSONDecodeError:
583
+ continue
584
+ if entry.get("type") == "user":
585
+ msg = entry.get("message", {})
586
+ content = msg.get("content", "")
587
+ if isinstance(content, str):
588
+ last_user_text = content
589
+ elif isinstance(content, list):
590
+ for block in content:
591
+ if isinstance(block, dict) and block.get("type") == "text":
592
+ last_user_text = block.get("text", "")
593
+ elif isinstance(block, str):
594
+ last_user_text = block
595
+ # If last user message is stop hook feedback, we're in a loop
596
+ if last_user_text.startswith(_STOP_HOOK_FEEDBACK_PREFIX):
597
+ print("[OMG] Guard 2 triggered: stop-hook feedback loop", file=sys.stderr)
598
+ return True
599
+ except Exception:
600
+ pass # Fail open — don't skip hooks on read errors
601
+
602
+ # Guard 4: File-based loop breaker (safety net)
603
+ # If stop hooks have blocked multiple times in quick succession,
604
+ # the agent cannot meaningfully resolve the issue (likely context-limited).
605
+ # This is the last-resort safety net when Guards 1-3 all fail to detect the loop.
606
+ if is_stop_block_loop():
607
+ print("[OMG] Guard 4 triggered: stop-block loop detected, skipping hooks", file=sys.stderr)
608
+ return True
609
+
610
+ # Guard 5: Empty stop_reason + recent block = likely context-limit deadlock
611
+ # Claude Code often doesn't set stop_reason/end_turn_reason for context-limit stops.
612
+ # If we blocked recently (any count >= 1 within window) AND stop_reason is missing,
613
+ # it's almost certainly a deadlock. Allow the stop to proceed.
614
+ if not stop_reason and not end_turn_reason and not failure_reason:
615
+ try:
616
+ _pdir = get_project_dir()
617
+ _tracker_path = os.path.join(_pdir, _STOP_BLOCK_TRACKER)
618
+ if os.path.exists(_tracker_path):
619
+ with open(_tracker_path, "r", encoding="utf-8") as _f:
620
+ _state = json.load(_f)
621
+ # Session isolation: stale tracker from another session cannot suppress hooks
622
+ _tracker_session = _state.get("session_id", "")
623
+ _current_session = _get_session_id()
624
+ if (_tracker_session and _current_session != "unknown"
625
+ and _tracker_session != _current_session):
626
+ pass # Different session — not a deadlock
627
+ else:
628
+ _elapsed = (datetime.now(timezone.utc) - datetime.fromisoformat(_state["ts"])).total_seconds()
629
+ if _elapsed < _BLOCK_LOOP_WINDOW_SECS and _state.get("count", 0) >= 1:
630
+ _reason = _state.get("reason", "unknown")
631
+ if _reason in _LOOP_BLOCK_REASONS:
632
+ print(
633
+ "[OMG] Guard 5 triggered: context may be exhausted and stop hooks recently blocked. "
634
+ "Skipping stop-hook blocks so compaction can run. "
635
+ "Tip: /OMG:handoff then continue in a fresh session.",
636
+ file=sys.stderr,
637
+ )
638
+ return True
639
+ except Exception:
640
+ pass # fail open
641
+ return False
642
+
643
+
644
+ # --- Stop-Block Loop Breaker (file-based safety net) ---
645
+
646
+
647
+ def _get_session_id():
648
+ """Get current session ID from environment, falling back to 'unknown'."""
649
+ for key in ("CLAUDE_SESSION_ID", "SESSION_ID", "OMG_SESSION_ID"):
650
+ val = os.environ.get(key, "").strip()
651
+ if val:
652
+ return val
653
+ return "unknown"
654
+
655
+
656
+ def record_stop_block(project_dir=None, reason: str = "unknown", session_id: str = ""):
657
+ """Record that a stop hook block was issued. Called before block_decision().
658
+
659
+ Args:
660
+ project_dir: Project directory (auto-detected if None)
661
+ reason: Human-readable reason for the block (e.g., 'ralph_loop', 'planning_gate', 'quality_check')
662
+ session_id: Session identifier to prevent cross-session interference
663
+ """
664
+ try:
665
+ current_session_id = session_id or _get_session_id()
666
+ pdir = project_dir or get_project_dir()
667
+ path = os.path.join(pdir, _STOP_BLOCK_TRACKER)
668
+ parent = os.path.dirname(path)
669
+ if parent:
670
+ os.makedirs(parent, exist_ok=True)
671
+ state = {
672
+ "ts": datetime.now(timezone.utc).isoformat(),
673
+ "count": 1,
674
+ "session_id": current_session_id,
675
+ "reason": reason,
676
+ }
677
+ with _locked_path(path):
678
+ if os.path.exists(path):
679
+ try:
680
+ with open(path, "r", encoding="utf-8") as f:
681
+ old = json.load(f)
682
+ elapsed = (datetime.now(timezone.utc) - datetime.fromisoformat(old["ts"])).total_seconds()
683
+ if elapsed < _BLOCK_LOOP_WINDOW_SECS:
684
+ state["count"] = old.get("count", 0) + 1
685
+ if current_session_id == "unknown":
686
+ state["session_id"] = old.get("session_id", "unknown")
687
+ if reason == "unknown":
688
+ state["reason"] = old.get("reason", "unknown")
689
+ except Exception:
690
+ pass # intentional: corrupt file, start fresh
691
+ atomic_json_write(path, state)
692
+ except Exception:
693
+ pass # intentional: never crash on tracking
694
+
695
+
696
+ def is_stop_block_loop(project_dir=None, session_id: str = ""):
697
+ """Return True if stop hooks have blocked repeatedly within the loop window.
698
+
699
+ Safety net for deadlocks: if hooks blocked >= N times within M seconds,
700
+ the agent clearly cannot resolve the issue (likely context-limited).
701
+ All stop hooks should allow the stop to proceed.
702
+
703
+ Args:
704
+ project_dir: Project directory (auto-detected if None)
705
+ session_id: Current session ID. If provided and tracker has a different session_id,
706
+ returns False (cross-session, not a loop).
707
+ """
708
+ try:
709
+ if not session_id:
710
+ session_id = _get_session_id()
711
+ pdir = project_dir or get_project_dir()
712
+ path = os.path.join(pdir, _STOP_BLOCK_TRACKER)
713
+ if not os.path.exists(path):
714
+ return False
715
+ with open(path, "r", encoding="utf-8") as f:
716
+ state = json.load(f)
717
+
718
+ # Cross-session check: if tracker has session_id and it differs from current, not a loop
719
+ tracker_session_id = state.get("session_id", "")
720
+ if tracker_session_id and session_id and tracker_session_id != session_id:
721
+ return False # Different session, not a loop
722
+
723
+ ts = datetime.fromisoformat(state["ts"])
724
+ elapsed = (datetime.now(timezone.utc) - ts).total_seconds()
725
+ count = state.get("count", 0)
726
+ return elapsed < _BLOCK_LOOP_WINDOW_SECS and count >= _BLOCK_LOOP_THRESHOLD
727
+ except Exception:
728
+ return False # fail open — don't skip hooks on errors
729
+
730
+
731
+ def reset_stop_block_tracker(project_dir=None):
732
+ """Reset the stop block tracker. Called on clean (non-blocked) stop."""
733
+ try:
734
+ pdir = project_dir or get_project_dir()
735
+ path = os.path.join(pdir, _STOP_BLOCK_TRACKER)
736
+ if os.path.exists(path):
737
+ os.remove(path)
738
+ except Exception:
739
+ pass # intentional: never crash on cleanup
740
+
741
+
742
+ def check_performance_budget(hook_name: str, elapsed_ms: float, budget_ms: float) -> bool:
743
+ """Check if hook execution is within performance budget.
744
+
745
+ Args:
746
+ hook_name: Name of the hook being checked
747
+ elapsed_ms: Elapsed time in milliseconds
748
+ budget_ms: Budget threshold in milliseconds
749
+
750
+ Returns:
751
+ True if within budget, False if over budget (with warning logged)
752
+ """
753
+ if elapsed_ms <= budget_ms:
754
+ return True
755
+ # Log warning for budget overrun
756
+ log_hook_error(
757
+ hook_name,
758
+ f"Performance budget exceeded: {elapsed_ms:.1f}ms > {budget_ms}ms",
759
+ context={"elapsed_ms": elapsed_ms, "budget_ms": budget_ms}
760
+ )
761
+ return False