@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,1259 @@
1
+ #!/usr/bin/env python3
2
+ """Stop Hook Dispatcher — Priority-based multiplexer for stop checks."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import importlib.util
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from datetime import datetime, timedelta, timezone
13
+ from pathlib import Path
14
+ import warnings
15
+
16
+ HOOKS_DIR = str(Path(__file__).resolve().parent)
17
+ PROJECT_ROOT = str(Path(HOOKS_DIR).parent)
18
+ PORTABLE_RUNTIME_ROOT = str(Path(PROJECT_ROOT) / "omg-runtime")
19
+ for path in (HOOKS_DIR, PROJECT_ROOT, PORTABLE_RUNTIME_ROOT):
20
+ if path not in sys.path:
21
+ sys.path.insert(0, path)
22
+
23
+ from hooks._common import ( # noqa: E402
24
+ _get_session_id,
25
+ atomic_json_write,
26
+ block_decision,
27
+ bootstrap_runtime_paths,
28
+ check_performance_budget,
29
+ get_feature_flag,
30
+ get_project_dir,
31
+ has_recent_tool_activity,
32
+ json_input,
33
+ log_hook_error,
34
+ read_checklist_session,
35
+ record_stop_block,
36
+ reset_stop_block_tracker,
37
+ _resolve_project_dir,
38
+ setup_crash_handler,
39
+ should_skip_stop_hooks,
40
+ STOP_CHECK_MAX_MS,
41
+ STOP_DISPATCHER_TOTAL_MAX_MS,
42
+ write_checklist_session,
43
+ )
44
+ from hooks.state_migration import resolve_state_file # noqa: E402
45
+
46
+ bootstrap_runtime_paths(__file__)
47
+
48
+ from runtime.release_run_coordinator import resolve_current_run_id # noqa: E402
49
+ from runtime import test_intent_lock # noqa: E402
50
+
51
+
52
+ setup_crash_handler("stop_dispatcher")
53
+
54
+
55
+ NON_SOURCE_PATTERNS = [
56
+ ".test.",
57
+ "__test",
58
+ "_test.",
59
+ "/tests/",
60
+ "tests/",
61
+ "/test/",
62
+ "test/",
63
+ "spec",
64
+ "__tests__",
65
+ ".spec.",
66
+ "script/",
67
+ "scripts/",
68
+ "/config/",
69
+ ".config.",
70
+ "package.json",
71
+ "tsconfig",
72
+ "eslint",
73
+ "prettier",
74
+ ".env",
75
+ "mock",
76
+ "fixture",
77
+ "snapshot",
78
+ "__mocks__",
79
+ "jest.",
80
+ "vitest.",
81
+ "setup.",
82
+ ".omg/",
83
+ ".omc/",
84
+ "omg-",
85
+ "CLAUDE.md",
86
+ "AGENTS.md",
87
+ "readme",
88
+ "changelog",
89
+ "license",
90
+ ".gitignore",
91
+ ".dockerignore",
92
+ "dockerfile",
93
+ "docker-compose",
94
+ "makefile",
95
+ ".github/",
96
+ ".vscode/",
97
+ ".idea/",
98
+ ]
99
+
100
+ INTERNAL_CONTROL_PATH_PATTERNS = [
101
+ ".omg/",
102
+ ".omc/",
103
+ "hooks/",
104
+ "CLAUDE.md",
105
+ "AGENTS.md",
106
+ ]
107
+
108
+
109
+ def _to_bool(value: str | None, default: bool) -> bool:
110
+ if value is None:
111
+ return default
112
+ normalized = value.strip().lower()
113
+ if normalized in {"1", "true", "yes", "on"}:
114
+ return True
115
+ if normalized in {"0", "false", "no", "off"}:
116
+ return False
117
+ return default
118
+
119
+
120
+ def _read_policy_flags(project_root: str) -> tuple[str, bool]:
121
+ mode = "warn_and_run"
122
+ require_evidence_pack = False
123
+ policy_path = os.path.join(project_root, ".omg", "policy.yaml")
124
+ if not os.path.exists(policy_path):
125
+ return mode, require_evidence_pack
126
+
127
+ try:
128
+ with open(policy_path, "r", encoding="utf-8", errors="ignore") as f:
129
+ for raw in f:
130
+ line = raw.strip()
131
+ if not line or line.startswith("#"):
132
+ continue
133
+ if line.startswith("mode:"):
134
+ mode = line.split(":", 1)[1].strip().strip("'\"") or mode
135
+ elif line.startswith("require_evidence_pack:"):
136
+ value = line.split(":", 1)[1].strip().strip("'\"")
137
+ require_evidence_pack = _to_bool(value, require_evidence_pack)
138
+ except Exception as e: # security: policy enforcement
139
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
140
+ return mode, require_evidence_pack
141
+
142
+
143
+ def _is_non_source_path(file_path: str) -> bool:
144
+ fl = str(file_path).lower()
145
+ return any(p in fl for p in NON_SOURCE_PATTERNS)
146
+
147
+
148
+ def _is_internal_control_path(file_path: str) -> bool:
149
+ fl = str(file_path).lower()
150
+ return any(p.lower() in fl for p in INTERNAL_CONTROL_PATH_PATTERNS)
151
+
152
+
153
+ _RALPH_DEFAULT_TIMEOUT_MINUTES = 10
154
+
155
+
156
+ def _watchdog_check(start_time):
157
+ """Return True if the dispatcher has exceeded its wall-clock budget."""
158
+ return (time.time() - start_time) >= (STOP_DISPATCHER_TOTAL_MAX_MS / 1000)
159
+
160
+
161
+ try:
162
+ from hooks.shadow_manager import has_recent_evidence # type: ignore
163
+ except Exception: # intentional: optional feature — shadow_manager may not exist
164
+ has_recent_evidence = None
165
+
166
+ # Import hyphenated modules via importlib
167
+ _test_validator_check = None
168
+ _quality_runner_check = None
169
+ try:
170
+ _tv_spec = importlib.util.spec_from_file_location(
171
+ "test_validator", os.path.join(os.path.dirname(__file__), "test-validator.py"))
172
+ if _tv_spec and _tv_spec.loader:
173
+ _tv_mod = importlib.util.module_from_spec(_tv_spec)
174
+ _tv_spec.loader.exec_module(_tv_mod)
175
+ _test_validator_check = getattr(_tv_mod, "check_test_quality", None)
176
+ except Exception: # intentional: crash isolation for optional module
177
+ pass
178
+ try:
179
+ _qr_spec = importlib.util.spec_from_file_location(
180
+ "quality_runner", os.path.join(os.path.dirname(__file__), "quality-runner.py"))
181
+ if _qr_spec and _qr_spec.loader:
182
+ _qr_mod = importlib.util.module_from_spec(_qr_spec)
183
+ _qr_spec.loader.exec_module(_qr_mod)
184
+ _quality_runner_check = getattr(_qr_mod, "check_quality_runner", None)
185
+ except Exception: # intentional: crash isolation for optional module
186
+ pass
187
+
188
+ def _build_context(project_dir: str, stop_payload: dict | None = None) -> dict[str, object]:
189
+ ledger_path = resolve_state_file(
190
+ project_dir,
191
+ "state/ledger/tool-ledger.jsonl",
192
+ "ledger/tool-ledger.jsonl",
193
+ )
194
+ ledger_entries = []
195
+ if os.path.exists(ledger_path):
196
+ try:
197
+ with open(ledger_path, "r", encoding="utf-8", errors="ignore") as f:
198
+ for line in f:
199
+ line = line.strip()
200
+ if not line:
201
+ continue
202
+ try:
203
+ entry = json.loads(line)
204
+ ledger_entries.append(entry)
205
+ except json.JSONDecodeError:
206
+ pass # intentional: skip malformed ledger lines
207
+ except Exception as e: # security: dispatch context building
208
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
209
+
210
+ cutoff = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
211
+ recent_entries = []
212
+ for entry in ledger_entries:
213
+ ts = entry.get("ts", "")
214
+ if ts >= cutoff:
215
+ recent_entries.append(entry)
216
+
217
+ recent_commands = [
218
+ e.get("command", "").lower()[:300]
219
+ for e in recent_entries
220
+ if e.get("command")
221
+ ]
222
+ recent_tools = {e.get("tool", "") for e in recent_entries}
223
+ recent_exit_codes = [
224
+ (e.get("command", ""), e.get("exit_code"))
225
+ for e in recent_entries
226
+ if e.get("exit_code") is not None
227
+ ]
228
+ write_entries = [
229
+ e
230
+ for e in recent_entries
231
+ if e.get("tool") in ("Write", "Edit", "MultiEdit")
232
+ ]
233
+ material_write_entries = [
234
+ e for e in write_entries if not _is_internal_control_path(str(e.get("file", "")))
235
+ ]
236
+ source_write_entries = [
237
+ e for e in material_write_entries if not _is_non_source_path(str(e.get("file", "")))
238
+ ]
239
+
240
+ # --- Current-turn provenance from stop-hook payload ---
241
+ # The stop payload (data from json_input()) may contain tool_use_results
242
+ # representing the CURRENT TURN's tool calls. We extract Write/Edit/MultiEdit
243
+ # entries from the payload to determine current-turn source writes independently
244
+ # of the 2-hour ledger window.
245
+ current_turn_source_write_entries: list[dict] = []
246
+ current_turn_run_id: str | None = None
247
+
248
+ try:
249
+ current_turn_run_id = resolve_current_run_id(project_dir)
250
+ except Exception as e: # security: run_id resolution is best-effort
251
+ print(f"[OMG] stop_dispatcher: resolve_current_run_id: {type(e).__name__}: {e}", file=sys.stderr)
252
+
253
+ if stop_payload and isinstance(stop_payload, dict):
254
+ # Claude Code stop hooks use "tool_use_results" key
255
+ raw_tool_results = stop_payload.get("tool_use_results") or stop_payload.get("tool_results") or []
256
+ if isinstance(raw_tool_results, list):
257
+ for result in raw_tool_results:
258
+ if not isinstance(result, dict):
259
+ continue
260
+ tool_name = result.get("tool_name") or result.get("tool") or ""
261
+ file_path = str(result.get("file") or result.get("path") or "")
262
+ if tool_name in ("Write", "Edit", "MultiEdit") and file_path:
263
+ if (not _is_internal_control_path(file_path)
264
+ and not _is_non_source_path(file_path)):
265
+ current_turn_source_write_entries.append(result)
266
+
267
+ return {
268
+ "ledger_path": ledger_path,
269
+ "ledger_entries": ledger_entries,
270
+ "recent_entries": recent_entries,
271
+ "recent_commands": recent_commands,
272
+ "recent_tools": recent_tools,
273
+ "recent_exit_codes": recent_exit_codes,
274
+ "write_entries": write_entries,
275
+ "material_write_entries": material_write_entries,
276
+ "source_write_entries": source_write_entries,
277
+ "has_writes": bool(write_entries),
278
+ "has_material_writes": bool(material_write_entries),
279
+ "has_source_writes": bool(source_write_entries),
280
+ # Current-turn provenance (additive — does not replace ledger fields)
281
+ "current_turn_source_write_entries": current_turn_source_write_entries,
282
+ "current_turn_has_source_writes": bool(current_turn_source_write_entries),
283
+ "current_turn_run_id": current_turn_run_id,
284
+ }
285
+
286
+
287
+ def check_verification(data, project_dir):
288
+ if not get_feature_flag("verification", True):
289
+ return []
290
+
291
+ context = data["_stop_ctx"]
292
+ blocks = []
293
+ advisories = data.setdefault("_stop_advisories", [])
294
+
295
+ recent_commands = context["recent_commands"]
296
+ has_source_writes = context["has_source_writes"]
297
+
298
+ has_test = any(
299
+ any(kw in cmd for kw in ["test", "jest", "vitest", "pytest", "cargo test", "go test"])
300
+ for cmd in recent_commands
301
+ )
302
+ has_lint = any(
303
+ any(kw in cmd for kw in ["lint", "eslint", "ruff check", "golint", "clippy"])
304
+ for cmd in recent_commands
305
+ )
306
+ has_build = any(
307
+ any(kw in cmd for kw in ["build", "compile", "tsc", "cargo build", "go build", "make"])
308
+ for cmd in recent_commands
309
+ )
310
+ has_any_verification = has_test or has_lint or has_build
311
+
312
+ data["_has_test"] = has_test
313
+
314
+ qg_path = resolve_state_file(project_dir, "state/quality-gate.json", "quality-gate.json")
315
+ expected_checks = []
316
+ if os.path.exists(qg_path):
317
+ try:
318
+ with open(qg_path, "r", encoding="utf-8", errors="ignore") as f:
319
+ qg = json.load(f)
320
+ for step in ["format", "lint", "typecheck", "test"]:
321
+ cmd = qg.get(step)
322
+ if isinstance(cmd, str) and cmd.strip():
323
+ expected_checks.append(step)
324
+ except Exception as e: # security: quality gate loading
325
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
326
+
327
+ if has_source_writes and not has_any_verification:
328
+ if expected_checks:
329
+ blocks.append(
330
+ "Code was modified but NO verification commands were run.\n"
331
+ f"Quality gate expects: {', '.join(expected_checks)}.\n"
332
+ "Run your verification commands before completing.\n"
333
+ "If you can't run them, explicitly state what is **Unverified** and why."
334
+ )
335
+ else:
336
+ blocks.append(
337
+ "Code was modified but NO verification commands were run.\n"
338
+ "No quality-gate.json configured, but at minimum run lint/test/build.\n"
339
+ "Run /OMG:init to configure quality gates, or explicitly state\n"
340
+ "what is **Unverified** and why."
341
+ )
342
+
343
+ policy_mode, policy_require_evidence = _read_policy_flags(project_dir)
344
+ env_evidence_required = os.environ.get("OMG_EVIDENCE_REQUIRED")
345
+ evidence_required = _to_bool(env_evidence_required, policy_require_evidence)
346
+ strict_evidence_gate = policy_mode.strip().lower() not in {
347
+ "warn_and_run",
348
+ "warn",
349
+ "advisory",
350
+ "report_only",
351
+ }
352
+
353
+ if has_source_writes and evidence_required:
354
+ has_ev = False
355
+ if has_recent_evidence is not None:
356
+ try:
357
+ has_ev = bool(has_recent_evidence(project_dir, hours=24))
358
+ except Exception as e: # security: evidence verification
359
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
360
+ has_ev = False
361
+ else:
362
+ ev_dir = os.path.join(project_dir, ".omg", "evidence")
363
+ has_ev = os.path.isdir(ev_dir) and any(n.endswith(".json") for n in os.listdir(ev_dir))
364
+
365
+ if not has_ev:
366
+ message = (
367
+ "OMG v1 evidence gate: source code was modified but no EvidencePack was found.\n"
368
+ "Create .omg/evidence/<run-id>.json before completing.\n"
369
+ "Required fields: tests, security_scans, diff_summary, reproducibility, unresolved_risks."
370
+ )
371
+ if strict_evidence_gate:
372
+ blocks.append(message)
373
+ else:
374
+ advisories.append(
375
+ f"[OMG advisory] {message} (policy mode: {policy_mode or 'warn_and_run'})"
376
+ )
377
+
378
+ return blocks
379
+
380
+
381
+ def check_diff_budget(data, project_dir):
382
+ if not get_feature_flag("diff_budget", True):
383
+ return []
384
+
385
+ blocks = []
386
+ changed_files = []
387
+ try:
388
+ max_files, max_loc = 3, 120
389
+ plan_path = resolve_state_file(project_dir, "state/_plan.md", "_plan.md")
390
+ if os.path.exists(plan_path):
391
+ with open(plan_path, "r", encoding="utf-8", errors="ignore") as f:
392
+ plan = f.read()
393
+ if "CHANGE_BUDGET=large" in plan:
394
+ max_files, max_loc = 999, 999999
395
+ elif "CHANGE_BUDGET=medium" in plan:
396
+ max_files, max_loc = 8, 400
397
+
398
+ result = subprocess.run(
399
+ ["git", "diff", "--name-only"],
400
+ capture_output=True,
401
+ text=True,
402
+ timeout=10,
403
+ cwd=project_dir,
404
+ )
405
+ changed_files = [line for line in result.stdout.strip().split("\n") if line]
406
+ files_changed = len(changed_files)
407
+
408
+ result2 = subprocess.run(
409
+ ["git", "diff", "--numstat"],
410
+ capture_output=True,
411
+ text=True,
412
+ timeout=10,
413
+ cwd=project_dir,
414
+ )
415
+ total_loc = 0
416
+ for line in result2.stdout.strip().split("\n"):
417
+ parts = line.split("\t")
418
+ if len(parts) >= 2:
419
+ try:
420
+ added = int(parts[0]) if parts[0] != "-" else 0
421
+ removed = int(parts[1]) if parts[1] != "-" else 0
422
+ total_loc += added + removed
423
+ except ValueError:
424
+ pass # intentional: skip unparseable numstat lines
425
+
426
+ if files_changed > max_files or total_loc > max_loc:
427
+ blocks.append(
428
+ f"Diff exceeds budget: {files_changed} files / {total_loc} LOC "
429
+ f"(limit: {max_files} / {max_loc}).\n"
430
+ "Reduce scope OR set CHANGE_BUDGET=medium/large in .omg/state/_plan.md."
431
+ )
432
+ except Exception as e: # security: diff budget enforcement
433
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
434
+
435
+ data["_changed_files"] = changed_files
436
+ return blocks
437
+
438
+
439
+ def check_recent_failures(data, project_dir):
440
+ if not get_feature_flag("recent_failures", True):
441
+ return []
442
+
443
+ del project_dir
444
+
445
+ recent_entries = data["_stop_ctx"]["recent_entries"]
446
+ blocks = []
447
+ recent_bash = [
448
+ (e.get("command", "")[:80], e.get("exit_code"))
449
+ for e in recent_entries
450
+ if e.get("tool") == "Bash" and e.get("exit_code") is not None
451
+ ]
452
+ if len(recent_bash) >= 3:
453
+ last_three = recent_bash[-3:]
454
+ all_failed = all(exit_code != 0 for _, exit_code in last_three)
455
+ if all_failed:
456
+ cmds = [f" {cmd} (exit {exit_code})" for cmd, exit_code in last_three]
457
+ blocks.append(
458
+ "Last 3 commands ALL FAILED:\n"
459
+ + "\n".join(cmds)
460
+ + "\n"
461
+ + "Do not claim completion with unresolved failures.\n"
462
+ + "Fix the failures, or document them as **Unverified**."
463
+ )
464
+ return blocks
465
+
466
+
467
+ _TEST_DIR_NAMES = frozenset({"tests", "test", "__tests__"})
468
+ _SKIP_DIR_SEGMENTS = frozenset({"build", "dist", "node_modules", ".git"})
469
+
470
+
471
+ def _is_test_file_path(rel_path):
472
+ parts = rel_path.replace("\\", "/").split("/")
473
+ if any(seg in _SKIP_DIR_SEGMENTS for seg in parts[:-1]):
474
+ return False
475
+ basename = parts[-1].lower() if parts else ""
476
+ if any(p in basename for p in (".test.", ".spec.", "_test.", ".tests.")):
477
+ return True
478
+ if "__tests__" in rel_path:
479
+ return True
480
+ if basename.startswith("test_"):
481
+ parent_dirs = {p.lower() for p in parts[:-1]}
482
+ return not parent_dirs or bool(parent_dirs & _TEST_DIR_NAMES)
483
+ return False
484
+
485
+
486
+ def check_test_execution(data, project_dir):
487
+ if not get_feature_flag("test_execution", True):
488
+ return []
489
+
490
+ del project_dir
491
+
492
+ context = data["_stop_ctx"]
493
+ has_material_writes = context["has_material_writes"]
494
+ has_test = bool(data.get("_has_test", False))
495
+ changed_files = data.get("_changed_files", [])
496
+ blocks = []
497
+
498
+ if has_material_writes:
499
+ test_files_modified = False
500
+ try:
501
+ for file_path in changed_files:
502
+ if _is_test_file_path(file_path):
503
+ test_files_modified = True
504
+ break
505
+ except Exception as e: # security: test execution check
506
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
507
+
508
+ if test_files_modified and not has_test:
509
+ blocks.append(
510
+ "Test files were modified but test suite was never executed.\n"
511
+ "Run your test command to verify the tests actually pass."
512
+ )
513
+
514
+ return blocks
515
+
516
+
517
+ def check_test_validator_coverage(data, project_dir):
518
+ if not get_feature_flag("test_validator_coverage", True):
519
+ return []
520
+
521
+ del project_dir
522
+
523
+ has_source_writes = data["_stop_ctx"]["has_source_writes"]
524
+ changed_files = data.get("_changed_files", [])
525
+ if not has_source_writes or not changed_files:
526
+ return []
527
+
528
+ source_changed = False
529
+ test_or_qa_changed = False
530
+ for file_path in changed_files:
531
+ fl = file_path.lower()
532
+ is_test_like = any(
533
+ token in fl
534
+ for token in (
535
+ "test",
536
+ "spec",
537
+ "__tests__",
538
+ ".test.",
539
+ ".spec.",
540
+ "qa",
541
+ "quality",
542
+ "e2e",
543
+ )
544
+ )
545
+ if is_test_like:
546
+ test_or_qa_changed = True
547
+ elif not _is_non_source_path(fl):
548
+ source_changed = True
549
+
550
+ if source_changed and not test_or_qa_changed:
551
+ return [
552
+ "TEST-VALIDATOR: Source changes detected without test/QA updates.\n"
553
+ "Add or update user-journey tests (including edge/error cases) for every new behavior."
554
+ ]
555
+ return []
556
+
557
+
558
+ def check_false_fix(data, project_dir):
559
+ if not get_feature_flag("false_fix", True):
560
+ return []
561
+
562
+ del project_dir
563
+
564
+ has_material_writes = data["_stop_ctx"]["has_material_writes"]
565
+ changed_files = data.get("_changed_files", [])
566
+ blocks = []
567
+
568
+ if has_material_writes:
569
+ non_source_only = True
570
+ try:
571
+ for file_path in changed_files:
572
+ fl = file_path.lower()
573
+ is_non_source = any(p in fl for p in NON_SOURCE_PATTERNS)
574
+ if not is_non_source:
575
+ non_source_only = False
576
+ break
577
+ except Exception as e: # security: false fix detection
578
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
579
+ non_source_only = False
580
+
581
+ if non_source_only and has_material_writes and len(changed_files) > 0:
582
+ blocks.append(
583
+ "⚠ FALSE FIX DETECTED: Only test/script/config files were modified.\n"
584
+ "No actual source code was changed. If the task was to fix a bug or\n"
585
+ "implement a feature, you likely changed test expectations to match\n"
586
+ "broken behavior instead of fixing the real code.\n\n"
587
+ "Go back and modify the actual SOURCE files, not just tests/configs."
588
+ )
589
+
590
+ return blocks
591
+
592
+
593
+ def check_write_failures(data, project_dir):
594
+ if not get_feature_flag("write_failures", True):
595
+ return []
596
+
597
+ del project_dir
598
+
599
+ recent_entries = data["_stop_ctx"]["recent_entries"]
600
+ has_material_writes = data["_stop_ctx"]["has_material_writes"]
601
+ blocks = []
602
+
603
+ if has_material_writes:
604
+ failed_writes = []
605
+ for entry in recent_entries[-30:]:
606
+ if entry.get("tool") in ("Write", "Edit", "MultiEdit"):
607
+ success = entry.get("success")
608
+ file_path = entry.get("file", "unknown")
609
+ if _is_internal_control_path(str(file_path)):
610
+ continue
611
+ if success is False:
612
+ failed_writes.append(file_path)
613
+ if failed_writes:
614
+ unique_fails = list(dict.fromkeys(failed_writes))[:5]
615
+ blocks.append(
616
+ "⚠ WRITE/EDIT FAILURE DETECTED:\n"
617
+ f"These file operations may have failed: {', '.join(unique_fails)}\n\n"
618
+ "Before claiming success, you MUST:\n"
619
+ "1. Read the file to verify your changes are actually there\n"
620
+ "2. If the file wasn't updated, retry with a different method:\n"
621
+ " - If Write failed (file exists): use Edit or Bash heredoc\n"
622
+ " - If Edit failed (hook error): verify file, then retry\n"
623
+ " - If hook error from external plugin: the write may have succeeded —\n"
624
+ " READ the file to check before retrying\n"
625
+ "3. Report honestly: 'Write failed' not 'Updated successfully'"
626
+ )
627
+
628
+ return blocks
629
+
630
+
631
+ def check_bare_done(data, project_dir):
632
+ """CHECK: Bare completion detection — blocks lazy 'Done.' responses."""
633
+ if not get_feature_flag("bare_done", True):
634
+ return []
635
+
636
+ del project_dir
637
+
638
+ transcript_path = data.get("transcript_path", "")
639
+ if not transcript_path or not os.path.isfile(transcript_path):
640
+ return []
641
+
642
+ # Find the last assistant message in the transcript
643
+ last_assistant_text = ""
644
+ try:
645
+ with open(transcript_path, "r", encoding="utf-8", errors="ignore") as f:
646
+ for line in f:
647
+ line = line.strip()
648
+ if not line:
649
+ continue
650
+ try:
651
+ entry = json.loads(line)
652
+ except (json.JSONDecodeError, ValueError):
653
+ continue
654
+ if entry.get("type") != "assistant":
655
+ continue
656
+ msg = entry.get("message", {})
657
+ content = msg.get("content", "")
658
+ if isinstance(content, str):
659
+ last_assistant_text = content
660
+ elif isinstance(content, list):
661
+ for block in content:
662
+ if isinstance(block, dict) and block.get("type") == "text":
663
+ last_assistant_text = block.get("text", "")
664
+ elif isinstance(block, str):
665
+ last_assistant_text = block
666
+ except Exception:
667
+ return []
668
+
669
+ if not last_assistant_text:
670
+ return []
671
+
672
+ # Only flag short responses
673
+ if len(last_assistant_text) >= 200:
674
+ return []
675
+
676
+ # Check for structured content markers — these indicate a real report
677
+ structured_markers = ["##", "- ", "```", "**Checks**", "**Files**"]
678
+ for marker in structured_markers:
679
+ if marker in last_assistant_text:
680
+ return []
681
+
682
+ # Check for bare completion patterns
683
+ bare_patterns = [
684
+ r"^\s*done\.?\s*$",
685
+ r"^\s*complete\.?\s*$",
686
+ r"^\s*completed\.?\s*$",
687
+ r"^\s*finished\.?\s*$",
688
+ r"^\s*all\s+done\.?\s*$",
689
+ ]
690
+ text_lower = last_assistant_text.strip()
691
+ for pat in bare_patterns:
692
+ if re.match(pat, text_lower, re.IGNORECASE):
693
+ return [
694
+ "Bare completion detected. Provide a structured report with: "
695
+ "files modified, checks run, and confidence level."
696
+ ]
697
+
698
+ return []
699
+
700
+
701
+ def _proof_chain_strict_enabled() -> bool:
702
+ return os.environ.get("OMG_PROOF_CHAIN_STRICT", "1").strip() != "0"
703
+
704
+
705
+ def _load_test_delta_from_evidence(project_dir: str, run_id: str | None) -> dict[str, object]:
706
+ evidence_dir = os.path.join(project_dir, ".omg", "evidence")
707
+ if not os.path.isdir(evidence_dir):
708
+ return {}
709
+
710
+ candidates = sorted(
711
+ [
712
+ os.path.join(evidence_dir, name)
713
+ for name in os.listdir(evidence_dir)
714
+ if name.endswith(".json")
715
+ ],
716
+ key=os.path.getmtime,
717
+ reverse=True,
718
+ )
719
+ for path in candidates:
720
+ try:
721
+ with open(path, "r", encoding="utf-8", errors="ignore") as handle:
722
+ payload = json.load(handle)
723
+ except (OSError, json.JSONDecodeError):
724
+ continue
725
+ if not isinstance(payload, dict):
726
+ continue
727
+ if payload.get("schema") != "EvidencePack":
728
+ continue
729
+ payload_run_id = str(payload.get("run_id", "")).strip()
730
+ if run_id and payload_run_id and payload_run_id != run_id:
731
+ continue
732
+ test_delta = payload.get("test_delta")
733
+ if isinstance(test_delta, dict):
734
+ return test_delta
735
+ return {}
736
+
737
+
738
+ def _has_waiver_artifact(delta_summary: dict[str, object]) -> bool:
739
+ waiver = delta_summary.get("waiver_artifact")
740
+ if isinstance(waiver, dict):
741
+ for field in ("artifact_path", "path", "id", "reason"):
742
+ if str(waiver.get(field, "")).strip():
743
+ return True
744
+ return False
745
+
746
+
747
+ def _has_weakened_or_drift(delta_summary: dict[str, object]) -> bool:
748
+ flags = delta_summary.get("flags")
749
+ if not isinstance(flags, list):
750
+ return False
751
+ risk_flags = {
752
+ "weakened_assertions",
753
+ "tests_mismatch",
754
+ "selector_drift",
755
+ "removed_touched_area_coverage",
756
+ "integration_to_mock_downgrade",
757
+ "snapshot_only_refresh",
758
+ }
759
+ normalized = {str(item).strip().lower() for item in flags if str(item).strip()}
760
+ return bool(normalized & risk_flags)
761
+
762
+
763
+ def check_tdd_proof_chain(data, project_dir):
764
+ if not get_feature_flag("tdd_proof_chain", True):
765
+ return []
766
+
767
+ context = data["_stop_ctx"]
768
+ has_current_turn_writes = context.get("current_turn_has_source_writes")
769
+ has_writes = has_current_turn_writes if has_current_turn_writes is not None else context.get("has_source_writes", False)
770
+ if not has_writes:
771
+ return []
772
+
773
+ run_id = resolve_current_run_id(project_dir=project_dir)
774
+ lock_verdict = test_intent_lock.verify_lock(project_dir, run_id=run_id)
775
+ delta_summary = data.get("_test_delta") if isinstance(data.get("_test_delta"), dict) else {}
776
+ if not delta_summary:
777
+ delta_summary = _load_test_delta_from_evidence(project_dir, run_id)
778
+
779
+ lock_missing = str(lock_verdict.get("status", "")).strip() != "ok"
780
+ weakened_without_waiver = _has_weakened_or_drift(delta_summary) and not _has_waiver_artifact(delta_summary)
781
+ if not lock_missing and not weakened_without_waiver:
782
+ return []
783
+
784
+ strict_mode = _proof_chain_strict_enabled()
785
+ if strict_mode:
786
+ _tdd_reason_code = "tdd_proof_chain_incomplete"
787
+ try:
788
+ from runtime.evidence_narrator import format_block_explanation
789
+ _tdd_explanation = format_block_explanation(_tdd_reason_code, {"tool": "stop_dispatcher"})
790
+ _tdd_enhanced_reason = f"{_tdd_reason_code}: {_tdd_explanation}"
791
+ except Exception:
792
+ _tdd_enhanced_reason = _tdd_reason_code
793
+ try:
794
+ import os as _tdd_os
795
+ from datetime import datetime as _tdd_dt, timezone as _tdd_tz
796
+ _tdd_artifact_dir = _tdd_os.path.join(project_dir, ".omg", "state")
797
+ _tdd_os.makedirs(_tdd_artifact_dir, exist_ok=True)
798
+ with open(_tdd_os.path.join(_tdd_artifact_dir, "last-block-explanation.json"), "w", encoding="utf-8") as _tdd_f:
799
+ json.dump({
800
+ "reason_code": _tdd_reason_code,
801
+ "explanation": _tdd_enhanced_reason,
802
+ "tool": "stop_dispatcher",
803
+ "timestamp": _tdd_dt.now(_tdd_tz.utc).isoformat(),
804
+ }, _tdd_f, indent=2)
805
+ except Exception:
806
+ pass
807
+ return [json.dumps({"status": "blocked", "reason": _tdd_enhanced_reason}, sort_keys=True)]
808
+
809
+ warnings.warn(
810
+ "tdd_proof_chain_incomplete_permissive",
811
+ RuntimeWarning,
812
+ stacklevel=2,
813
+ )
814
+ advisories = data.setdefault("_stop_advisories", [])
815
+ advisories.append(
816
+ "[OMG advisory] tdd proof chain incomplete: active lock evidence or waiver artifact is missing. "
817
+ "Production default is fail-closed; OMG_PROOF_CHAIN_STRICT=0 downgrades to advisory."
818
+ )
819
+ return []
820
+
821
+ def check_simplifier(data, project_dir):
822
+ """CHECK 7: Code simplifier — advisory only, never blocks."""
823
+ if not get_feature_flag("simplifier", True):
824
+ return []
825
+
826
+ context = data["_stop_ctx"]
827
+ source_write_entries = context.get("source_write_entries", [])
828
+ if not source_write_entries:
829
+ return []
830
+
831
+ generic_name_re = re.compile(
832
+ r'\b(data|result|item|temp|val|obj|info|stuff|thing)\b'
833
+ )
834
+ noise_comment_re = re.compile(
835
+ r'^\s*(#|//) (get|set|return|check|create|update|delete) '
836
+ )
837
+ def_line_re = re.compile(r'^\s*(def |let |const |var )')
838
+
839
+ advisories = []
840
+ seen = set()
841
+
842
+ for entry in source_write_entries:
843
+ file_path = str(entry.get("file", ""))
844
+ if not file_path or file_path in seen:
845
+ continue
846
+ seen.add(file_path)
847
+
848
+ full_path = (
849
+ file_path
850
+ if os.path.isabs(file_path)
851
+ else os.path.join(project_dir, file_path)
852
+ )
853
+
854
+ try:
855
+ size = os.path.getsize(full_path)
856
+ if size > 10240: # Skip files >10KB
857
+ continue
858
+ with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
859
+ lines = f.readlines()
860
+ except (OSError, IOError):
861
+ continue # intentional: skip unreadable files
862
+
863
+ if not lines:
864
+ continue
865
+
866
+ total = len(lines)
867
+ comment_count = sum(
868
+ 1 for line in lines
869
+ if line.strip() and re.match(r'^\s*(#|//|/\*|\*)', line)
870
+ )
871
+
872
+ if total > 0 and comment_count / total > 0.40:
873
+ pct = comment_count * 100 // total
874
+ advisories.append(
875
+ f"@simplifier: {file_path} — {pct}% comment lines ({comment_count}/{total})"
876
+ )
877
+
878
+ for line in lines:
879
+ if def_line_re.match(line) and generic_name_re.search(line):
880
+ advisories.append(
881
+ f"@simplifier: {file_path} — generic name: {line.strip()[:80]}"
882
+ )
883
+ break
884
+
885
+ for line in lines:
886
+ if noise_comment_re.match(line):
887
+ advisories.append(
888
+ f"@simplifier: {file_path} — noise comment: {line.strip()[:60]}"
889
+ )
890
+ break
891
+
892
+ if advisories:
893
+ for adv in advisories:
894
+ print(adv, file=sys.stderr)
895
+
896
+ return [] # Never blocks
897
+
898
+
899
+ def format_ralph_block_reason(state, project_dir):
900
+ """Build the rich reason string that Claude sees as its next prompt."""
901
+ original = state.get('original_prompt', '')
902
+ iteration = state.get('iteration', 0)
903
+ max_iter = state.get('max_iterations', 50)
904
+ checklist_path = state.get('checklist_path', '')
905
+
906
+ progress = ''
907
+ if checklist_path:
908
+ full = os.path.join(project_dir, checklist_path)
909
+ if os.path.exists(full):
910
+ try:
911
+ with open(full) as f:
912
+ lines = f.readlines()
913
+ done = sum(1 for l in lines if re.search(r'\[x\]', l, re.IGNORECASE))
914
+ total = sum(1 for l in lines if re.search(r'^\s*-\s*\[[ x!]\]', l))
915
+ progress = f' | Progress: {done}/{total}'
916
+ except OSError:
917
+ pass # intentional: progress display is optional
918
+
919
+ return (
920
+ f"Ralph loop iteration {iteration}/{max_iter}{progress}. "
921
+ f"Continue working on: {original}\n"
922
+ f"If truly done, run: /OMG:ralph-stop"
923
+ )
924
+
925
+ def persist_ralph_question(project_dir, question_text):
926
+ """Persist a pending clarification question in the Ralph loop state.
927
+
928
+ Called when any hook or the context engine detects ambiguity during a
929
+ Ralph iteration. The next stop-hook check will emit the question via
930
+ block_decision and end the turn — no tool action may proceed.
931
+ """
932
+ ralph_path = os.path.join(project_dir, ".omg", "state", "ralph-loop.json")
933
+ if not os.path.exists(ralph_path):
934
+ return
935
+ try:
936
+ with open(ralph_path, "r", encoding="utf-8") as f:
937
+ state = json.load(f)
938
+ except (json.JSONDecodeError, OSError):
939
+ return
940
+ state["question_pending"] = True
941
+ state["question_text"] = str(question_text).strip()[:500]
942
+ state["question_emitted_at"] = datetime.now(timezone.utc).isoformat()
943
+ atomic_json_write(ralph_path, state)
944
+
945
+
946
+ def check_ralph_loop(project_dir, data):
947
+ """Check Ralph loop state and return (block_reasons, advisories, is_question).
948
+
949
+ When *is_question* is True the block is a clarification question and the
950
+ caller MUST use block_reason="clarification_required" so that the turn
951
+ ends without any further tool action.
952
+ """
953
+ del data
954
+
955
+ if not get_feature_flag("ralph_loop"):
956
+ return [], [], False
957
+ ralph_path = os.path.join(project_dir, ".omg", "state", "ralph-loop.json")
958
+ if not os.path.exists(ralph_path):
959
+ return [], [], False
960
+ try:
961
+ with open(ralph_path, "r", encoding="utf-8") as f:
962
+ state = json.load(f)
963
+ except (json.JSONDecodeError, OSError):
964
+ return [], [], False
965
+ if not state.get("active"):
966
+ return [], [], False
967
+
968
+ # --- Pending clarification question: block and end turn immediately ---
969
+ if state.get("question_pending"):
970
+ question_text = str(state.get("question_text", "")).strip()
971
+ if not question_text:
972
+ question_text = "Clarification required before continuing Ralph loop"
973
+ # Do NOT increment iteration — the loop is paused on the question
974
+ return [question_text], [], True
975
+
976
+ # Check if Ralph loop has expired
977
+ _raw_timeout = os.environ.get("OMG_RALPH_TIMEOUT_MINUTES", "")
978
+ try:
979
+ timeout_minutes = int(_raw_timeout) if _raw_timeout.strip() else _RALPH_DEFAULT_TIMEOUT_MINUTES
980
+ except (ValueError, TypeError):
981
+ timeout_minutes = _RALPH_DEFAULT_TIMEOUT_MINUTES
982
+ started_at_str = state.get("started_at")
983
+ if started_at_str:
984
+ try:
985
+ started_at = datetime.fromisoformat(started_at_str.replace("Z", "+00:00"))
986
+ now = datetime.now(timezone.utc)
987
+ elapsed = now - started_at
988
+ if elapsed.total_seconds() > timeout_minutes * 60:
989
+ state["active"] = False
990
+ atomic_json_write(ralph_path, state)
991
+ return [], [f"Ralph loop expired after {timeout_minutes} minutes. Stopping."], False
992
+ except (ValueError, TypeError):
993
+ pass
994
+
995
+ iteration = state.get("iteration", 0)
996
+ max_iter = state.get("max_iterations", 50)
997
+ if iteration >= max_iter:
998
+ state["active"] = False
999
+ atomic_json_write(ralph_path, state)
1000
+ return [], ["Ralph loop reached max iterations. Stopping."], False
1001
+ state["iteration"] = iteration + 1
1002
+ atomic_json_write(ralph_path, state)
1003
+ reason = format_ralph_block_reason(state, project_dir)
1004
+ return [reason], [], False
1005
+
1006
+
1007
+ def check_planning_gate(project_dir, data=None):
1008
+ if not get_feature_flag("planning_enforcement"):
1009
+ return [], []
1010
+ current_turn_has_writes = (data or {}).get("_stop_ctx", {}).get("current_turn_has_source_writes", True)
1011
+ if not current_turn_has_writes:
1012
+ return [], []
1013
+ checklist = resolve_state_file(project_dir, "state/_checklist.md", "_checklist.md")
1014
+ if not os.path.exists(checklist):
1015
+ return [], []
1016
+ try:
1017
+ with open(checklist, "r", encoding="utf-8") as f:
1018
+ lines = f.readlines()
1019
+ except OSError:
1020
+ return [], []
1021
+ total = sum(1 for l in lines if re.search(r"^\s*-\s*\[[ x!]\]", l))
1022
+ done = sum(1 for l in lines if re.search(r"^\s*-\s*\[x\]", l, re.IGNORECASE))
1023
+ blocked = sum(1 for l in lines if re.search(r"^\s*-\s*\[!\]", l))
1024
+ pending = total - done - blocked
1025
+ if pending > 0:
1026
+ sidecar_path = os.path.join(project_dir, ".omg", "state", "_checklist.session")
1027
+ if not os.path.exists(sidecar_path):
1028
+ write_checklist_session(project_dir)
1029
+
1030
+ session_data = read_checklist_session(project_dir)
1031
+ current_session = _get_session_id()
1032
+ stale_reason = None
1033
+ if session_data:
1034
+ checklist_session = str(session_data.get("session_id", "")).strip()
1035
+ if (
1036
+ checklist_session
1037
+ and current_session != "unknown"
1038
+ and checklist_session != current_session
1039
+ ):
1040
+ stale_reason = "different session"
1041
+ created_at = str(session_data.get("created_at", "")).strip()
1042
+ if not stale_reason and created_at:
1043
+ try:
1044
+ created_dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
1045
+ if created_dt.tzinfo is None:
1046
+ created_dt = created_dt.replace(tzinfo=timezone.utc)
1047
+ age = datetime.now(timezone.utc) - created_dt.astimezone(timezone.utc)
1048
+ if age.total_seconds() > 2 * 3600:
1049
+ stale_reason = f"{age.total_seconds() / 3600:.1f}h old"
1050
+ except (ValueError, TypeError):
1051
+ pass
1052
+ else:
1053
+ try:
1054
+ age_hours = (time.time() - os.path.getmtime(checklist)) / 3600
1055
+ if age_hours > 2:
1056
+ stale_reason = f"{age_hours:.1f}h old (mtime fallback)"
1057
+ except OSError:
1058
+ pass
1059
+
1060
+ if stale_reason:
1061
+ return [], [
1062
+ f"[OMG advisory] Planning gate: stale checklist ({stale_reason}). "
1063
+ f"{done}/{total} complete, {pending} pending. "
1064
+ f"Clear with: rm .omg/state/_checklist.md"
1065
+ ]
1066
+
1067
+ # Check context pressure — demote to advisory if high
1068
+ _pressure_path = os.path.join(project_dir, ".omg", "state", ".context-pressure.json")
1069
+ _is_high_pressure = False
1070
+ try:
1071
+ if os.path.exists(_pressure_path):
1072
+ with open(_pressure_path, "r") as _f:
1073
+ _pressure = json.load(_f)
1074
+ _is_high_pressure = _pressure.get("is_high", False)
1075
+ except Exception:
1076
+ pass
1077
+
1078
+ if _is_high_pressure:
1079
+ # Demote to advisory — don't block when context is exhausted
1080
+ return [], [f"[OMG advisory] Planning gate: {done}/{total} complete, {pending} pending. (demoted: context pressure high)"]
1081
+
1082
+ return [
1083
+ f"Planning gate: {done}/{total} complete, {pending} pending. Complete checklist before finishing."
1084
+ ], []
1085
+ if pending == 0 and done >= 3:
1086
+ activity = has_recent_tool_activity(project_dir, since_minutes=60)
1087
+ if not activity.get("has_writes") and not activity.get("has_tests"):
1088
+ return [], [
1089
+ f"[OMG advisory] All {done} checklist items marked [x] but "
1090
+ f"no code changes or test runs found in tool-ledger. "
1091
+ f"If work was done externally, this can be ignored."
1092
+ ]
1093
+ return [], []
1094
+
1095
+
1096
+ def check_scope_drift(project_dir):
1097
+ try:
1098
+ result = subprocess.run(
1099
+ ["git", "diff", "--name-only", "HEAD"],
1100
+ capture_output=True,
1101
+ text=True,
1102
+ timeout=5,
1103
+ cwd=project_dir,
1104
+ )
1105
+ changed_files = [f.strip() for f in result.stdout.splitlines() if f.strip()]
1106
+ if not changed_files:
1107
+ return []
1108
+ plan_path = resolve_state_file(project_dir, "state/_plan.md", "_plan.md")
1109
+ if not os.path.exists(plan_path):
1110
+ return []
1111
+ with open(plan_path, "r", encoding="utf-8") as f:
1112
+ plan_content = f.read()
1113
+ mentioned = sum(1 for f in changed_files if os.path.basename(f) in plan_content)
1114
+ outside = len(changed_files) - mentioned
1115
+ if changed_files and (outside / len(changed_files)) > 0.3:
1116
+ return [f"Scope drift: {outside}/{len(changed_files)} changed files not in plan."]
1117
+ except Exception as e: # security: scope drift detection
1118
+ print(f"[OMG] stop_dispatcher: {type(e).__name__}: {e}", file=sys.stderr)
1119
+ return []
1120
+
1121
+
1122
+
1123
+ def check_todo_continuation(data: dict[str, object]) -> dict[str, str] | None:
1124
+ """Check if agent should continue due to incomplete todos.
1125
+ Returns a dict with continuation response if idle, None otherwise.
1126
+ Budget: STOP_CHECK_MAX_MS (15s)
1127
+ Feature flag: OMG_TODO_ENFORCEMENT_ENABLED
1128
+ """
1129
+ if not get_feature_flag("TODO_ENFORCEMENT", default=False):
1130
+ return None
1131
+
1132
+ project_dir = get_project_dir()
1133
+ signal_path = os.path.join(project_dir, ".omg", "state", "idle_signal.json")
1134
+
1135
+ if not os.path.exists(signal_path):
1136
+ return None
1137
+
1138
+ try:
1139
+ with open(signal_path, "r", encoding="utf-8") as f:
1140
+ idle_signal = json.load(f)
1141
+ except (json.JSONDecodeError, OSError):
1142
+ return None
1143
+
1144
+ if not isinstance(idle_signal, dict):
1145
+ return None
1146
+
1147
+ if not idle_signal.get("idle_detected", False):
1148
+ return None
1149
+
1150
+ incomplete_count = idle_signal.get("incomplete_count", 0)
1151
+ incomplete_items = idle_signal.get("incomplete_items", [])
1152
+
1153
+ return {
1154
+ "decision": "block",
1155
+ "reason": f"Incomplete todos detected ({incomplete_count} items). Please complete: {', '.join(incomplete_items[:3])}"
1156
+ }
1157
+
1158
+
1159
+ def main():
1160
+ _watchdog_start = time.time()
1161
+
1162
+ data = json_input()
1163
+
1164
+ # Unified guard: stop-hook loop, context-limit, and re-entry detection
1165
+ if should_skip_stop_hooks(data):
1166
+ sys.exit(0)
1167
+
1168
+ # Watchdog: bail out if we already burned too much wall-clock time
1169
+ if _watchdog_check(_watchdog_start):
1170
+ print("[OMG] stop_dispatcher: wall-clock watchdog expired", file=sys.stderr)
1171
+ sys.exit(0)
1172
+
1173
+ project_dir = _resolve_project_dir()
1174
+ data["_stop_ctx"] = _build_context(project_dir, stop_payload=data)
1175
+ data["_stop_advisories"] = []
1176
+
1177
+ # P1: Ralph loop check (implemented in Task 18)
1178
+ block_reasons, advisories, is_question = check_ralph_loop(project_dir, data)
1179
+ if advisories:
1180
+ data["_stop_advisories"].extend(advisories)
1181
+ if block_reasons:
1182
+ # Clarification questions use a distinct block_reason so the turn
1183
+ # ends cleanly — no tool action may follow a question emission.
1184
+ br = "clarification_required" if is_question else "ralph_loop"
1185
+ block_decision(block_reasons[0], block_reason=br, project_dir=project_dir)
1186
+ return
1187
+
1188
+ if _watchdog_check(_watchdog_start):
1189
+ print("[OMG] stop_dispatcher: wall-clock watchdog expired", file=sys.stderr)
1190
+ sys.exit(0)
1191
+
1192
+ # P2: Planning enforcement (implemented in Task 22)
1193
+ block_reasons, advisories = check_planning_gate(project_dir, data=data)
1194
+ if block_reasons:
1195
+ block_decision(block_reasons[0], block_reason="planning_gate", project_dir=project_dir)
1196
+ return
1197
+ advisories.extend(check_scope_drift(project_dir))
1198
+ if advisories:
1199
+ data["_stop_advisories"].extend(advisories)
1200
+
1201
+ if _watchdog_check(_watchdog_start):
1202
+ print("[OMG] stop_dispatcher: wall-clock watchdog expired", file=sys.stderr)
1203
+ sys.exit(0)
1204
+
1205
+ # P3: Todo continuation enforcement (Task 1.6)
1206
+ _p3_start = time.monotonic()
1207
+ todo_result = check_todo_continuation(data)
1208
+ _p3_elapsed = (time.monotonic() - _p3_start) * 1000
1209
+ check_performance_budget("check_todo_continuation", _p3_elapsed, STOP_CHECK_MAX_MS)
1210
+ if todo_result and todo_result.get("decision") == "block":
1211
+ block_decision(todo_result["reason"], block_reason="todo_continuation", project_dir=project_dir)
1212
+ return
1213
+
1214
+ if _watchdog_check(_watchdog_start):
1215
+ print("[OMG] stop_dispatcher: wall-clock watchdog expired", file=sys.stderr)
1216
+ sys.exit(0)
1217
+
1218
+ blocks = []
1219
+ for check_fn in [
1220
+ check_verification,
1221
+ check_diff_budget,
1222
+ check_recent_failures,
1223
+ check_test_execution,
1224
+ check_tdd_proof_chain,
1225
+ check_test_validator_coverage,
1226
+ check_false_fix,
1227
+ check_write_failures,
1228
+ check_bare_done,
1229
+ _test_validator_check,
1230
+ _quality_runner_check,
1231
+ ]:
1232
+ if _watchdog_check(_watchdog_start):
1233
+ print("[OMG] stop_dispatcher: wall-clock watchdog expired during quality checks", file=sys.stderr)
1234
+ sys.exit(0)
1235
+ if check_fn is None:
1236
+ continue
1237
+ try:
1238
+ result = check_fn(data, project_dir)
1239
+ if result:
1240
+ blocks.extend(result)
1241
+ except Exception as exc:
1242
+ name = getattr(check_fn, "__name__", str(check_fn))
1243
+ log_hook_error("stop_dispatcher", exc, {"check": name})
1244
+
1245
+ advisories = data.get("_stop_advisories", [])
1246
+ if advisories:
1247
+ print("\n".join(advisories), file=sys.stderr)
1248
+
1249
+ if blocks:
1250
+ block_decision("\n\n---\n\n".join(blocks), block_reason="quality_check", project_dir=project_dir)
1251
+ return
1252
+
1253
+ check_simplifier(data, project_dir)
1254
+ reset_stop_block_tracker(project_dir)
1255
+ sys.exit(0)
1256
+
1257
+
1258
+ if __name__ == "__main__":
1259
+ main()