@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.
- package/CHANGELOG.md +188 -0
- package/INSTALL-VERIFICATION-INDEX.md +51 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +2549 -0
- package/QUICK-REFERENCE.md +58 -0
- package/README.md +207 -0
- package/agents/__init__.py +1 -0
- package/agents/__pycache__/model_roles.cpython-313.pyc +0 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +41 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +41 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +41 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +41 -0
- package/agents/omg-implement-mode.md +49 -0
- package/agents/omg-infra-engineer.md +41 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +41 -0
- package/agents/omg-security-auditor.md +41 -0
- package/agents/omg-testing-engineer.md +41 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/bin/omg +41 -0
- package/commands/OMG:ai-commit.md +113 -0
- package/commands/OMG:api-twin.md +22 -0
- package/commands/OMG:arch.md +313 -0
- package/commands/OMG:browser.md +29 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:cost.md +181 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:create-agent.md +183 -0
- package/commands/OMG:deep-plan.md +18 -0
- package/commands/OMG:deps.md +248 -0
- package/commands/OMG:diagnose-plugins.md +33 -0
- package/commands/OMG:doctor.md +37 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:forge.md +103 -0
- package/commands/OMG:health-check.md +48 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:issue.md +56 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:playwright.md +17 -0
- package/commands/OMG:preflight.md +26 -0
- package/commands/OMG:preset.md +49 -0
- package/commands/OMG:profile-review.md +58 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:security-check.md +28 -0
- package/commands/OMG:session-branch.md +101 -0
- package/commands/OMG:session-fork.md +57 -0
- package/commands/OMG:session-merge.md +138 -0
- package/commands/OMG:setup.md +82 -0
- package/commands/OMG:ship.md +18 -0
- package/commands/OMG:stats.md +225 -0
- package/commands/OMG:teams.md +54 -0
- package/commands/OMG:theme.md +44 -0
- package/commands/OMG:validate.md +59 -0
- package/commands/__init__.py +1 -0
- package/docs/command-surface.md +55 -0
- package/docs/install/claude-code.md +53 -0
- package/docs/install/codex.md +45 -0
- package/docs/install/gemini.md +43 -0
- package/docs/install/github-action.md +81 -0
- package/docs/install/github-app-required-checks.md +107 -0
- package/docs/install/github-app.md +161 -0
- package/docs/install/kimi.md +43 -0
- package/docs/install/opencode.md +38 -0
- package/docs/proof.md +182 -0
- package/hooks/__init__.py +0 -0
- package/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_agent_registry.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_analytics.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_budget.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_common.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_compression_optimizer.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_cost_ledger.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_learnings.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_memory.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_post_write.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_protected_context.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_token_counter.cpython-313.pyc +0 -0
- package/hooks/__pycache__/branch_manager.cpython-313.pyc +0 -0
- package/hooks/__pycache__/budget_governor.cpython-313.pyc +0 -0
- package/hooks/__pycache__/circuit-breaker.cpython-313.pyc +0 -0
- package/hooks/__pycache__/compression_feedback.cpython-313.pyc +0 -0
- package/hooks/__pycache__/config-guard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/context_pressure.cpython-313.pyc +0 -0
- package/hooks/__pycache__/credential_store.cpython-313.pyc +0 -0
- package/hooks/__pycache__/fetch-rate-limits.cpython-313.pyc +0 -0
- package/hooks/__pycache__/firewall.cpython-313.pyc +0 -0
- package/hooks/__pycache__/hashline-formatter-bridge.cpython-313.pyc +0 -0
- package/hooks/__pycache__/hashline-injector.cpython-313.pyc +0 -0
- package/hooks/__pycache__/hashline-validator.cpython-313.pyc +0 -0
- package/hooks/__pycache__/idle-detector.cpython-313.pyc +0 -0
- package/hooks/__pycache__/instructions-loaded.cpython-313.pyc +0 -0
- package/hooks/__pycache__/intentgate-keyword-detector.cpython-313.pyc +0 -0
- package/hooks/__pycache__/magic-keyword-router.cpython-313.pyc +0 -0
- package/hooks/__pycache__/policy_engine.cpython-313.pyc +0 -0
- package/hooks/__pycache__/post-tool-failure.cpython-313.pyc +0 -0
- package/hooks/__pycache__/post-write.cpython-313.pyc +0 -0
- package/hooks/__pycache__/post_write.cpython-313.pyc +0 -0
- package/hooks/__pycache__/pre-compact.cpython-313.pyc +0 -0
- package/hooks/__pycache__/pre-tool-inject.cpython-313.pyc +0 -0
- package/hooks/__pycache__/prompt-enhancer.cpython-313.pyc +0 -0
- package/hooks/__pycache__/quality-runner.cpython-313.pyc +0 -0
- package/hooks/__pycache__/query.cpython-313.pyc +0 -0
- package/hooks/__pycache__/secret-guard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/secret_audit.cpython-313.pyc +0 -0
- package/hooks/__pycache__/security_validators.cpython-313.pyc +0 -0
- package/hooks/__pycache__/session-end-capture.cpython-313.pyc +0 -0
- package/hooks/__pycache__/session-start.cpython-313.pyc +0 -0
- package/hooks/__pycache__/setup_wizard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/shadow_manager.cpython-313.pyc +0 -0
- package/hooks/__pycache__/state_migration.cpython-313.pyc +0 -0
- package/hooks/__pycache__/stop-gate.cpython-313.pyc +0 -0
- package/hooks/__pycache__/stop_dispatcher.cpython-313.pyc +0 -0
- package/hooks/__pycache__/tdd-gate.cpython-313.pyc +0 -0
- package/hooks/__pycache__/terms-guard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/test-validator.cpython-313.pyc +0 -0
- package/hooks/__pycache__/test_generator_hook.cpython-313.pyc +0 -0
- package/hooks/__pycache__/todo-state-tracker.cpython-313.pyc +0 -0
- package/hooks/__pycache__/tool-ledger.cpython-313.pyc +0 -0
- package/hooks/__pycache__/trust_review.cpython-313.pyc +0 -0
- package/hooks/__pycache__/user-prompt-submit.cpython-313.pyc +0 -0
- package/hooks/_agent_registry.py +481 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +761 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_post_write.py +46 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +255 -0
- package/hooks/budget_governor.py +326 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +193 -0
- package/hooks/context_pressure.py +119 -0
- package/hooks/credential_store.py +970 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +323 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +97 -0
- package/hooks/instructions-loaded.py +26 -0
- package/hooks/intentgate-keyword-detector.py +200 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +767 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +233 -0
- package/hooks/pre-compact.py +470 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +879 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +120 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/security_validators.py +93 -0
- package/hooks/session-end-capture.py +505 -0
- package/hooks/session-start.py +261 -0
- package/hooks/setup_wizard.py +1101 -0
- package/hooks/shadow_manager.py +476 -0
- package/hooks/state_migration.py +228 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +1259 -0
- package/hooks/tdd-gate.py +10 -0
- package/hooks/terms-guard.py +98 -0
- package/hooks/test-validator.py +462 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +165 -0
- package/hooks/trust_review.py +662 -0
- package/hooks/user-prompt-submit.py +12 -0
- package/hud/omg-hud.mjs +1571 -0
- package/lab/__init__.py +1 -0
- package/lab/__pycache__/__init__.cpython-313.pyc +0 -0
- package/lab/__pycache__/axolotl_adapter.cpython-313.pyc +0 -0
- package/lab/__pycache__/forge_runner.cpython-313.pyc +0 -0
- package/lab/__pycache__/gazebo_adapter.cpython-313.pyc +0 -0
- package/lab/__pycache__/isaac_gym_adapter.cpython-313.pyc +0 -0
- package/lab/__pycache__/mock_isaac_env.cpython-313.pyc +0 -0
- package/lab/__pycache__/pipeline.cpython-313.pyc +0 -0
- package/lab/__pycache__/policies.cpython-313.pyc +0 -0
- package/lab/__pycache__/pybullet_adapter.cpython-313.pyc +0 -0
- package/lab/axolotl_adapter.py +531 -0
- package/lab/forge_runner.py +103 -0
- package/lab/gazebo_adapter.py +168 -0
- package/lab/isaac_gym_adapter.py +190 -0
- package/lab/mock_isaac_env.py +47 -0
- package/lab/pipeline.py +712 -0
- package/lab/policies.py +52 -0
- package/lab/pybullet_adapter.py +192 -0
- package/package.json +61 -0
- package/plugins/README.md +78 -0
- package/plugins/__init__.py +1 -0
- package/plugins/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/advanced/commands/OMG-code-review.md +114 -0
- package/plugins/advanced/commands/OMG-deep-plan.md +266 -0
- package/plugins/advanced/commands/OMG-handoff.md +115 -0
- package/plugins/advanced/commands/OMG-learn.md +110 -0
- package/plugins/advanced/commands/OMG-maintainer.md +31 -0
- package/plugins/advanced/commands/OMG-ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG-ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG-security-review.md +16 -0
- package/plugins/advanced/commands/OMG-sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG-ship.md +46 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +266 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +16 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +104 -0
- package/plugins/core/plugin.json +204 -0
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/cve_scanner.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/license_checker.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/manifest_detector.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/vuln_analyzer.cpython-313.pyc +0 -0
- package/plugins/dephealth/cve_scanner.py +279 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +176 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/codamosa_engine.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/edge_case_synthesizer.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/framework_detector.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/skeleton_generator.cpython-313.pyc +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/ast_parser.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/diagram_generator.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/graph_builder.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/native_parsers.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/regex_parser.cpython-313.pyc +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +143 -0
- package/registry/__init__.py +1 -0
- package/registry/__pycache__/__init__.cpython-313.pyc +0 -0
- package/registry/__pycache__/approval_artifact.cpython-313.pyc +0 -0
- package/registry/__pycache__/verify_artifact.cpython-313.pyc +0 -0
- package/registry/approval_artifact.py +236 -0
- package/registry/bundles/algorithms.yaml +45 -0
- package/registry/bundles/api-twin.yaml +48 -0
- package/registry/bundles/ast-pack.yaml +80 -0
- package/registry/bundles/claim-judge.yaml +49 -0
- package/registry/bundles/control-plane.yaml +192 -0
- package/registry/bundles/data-lineage.yaml +47 -0
- package/registry/bundles/delta-classifier.yaml +47 -0
- package/registry/bundles/eval-gate.yaml +47 -0
- package/registry/bundles/hash-edit.yaml +73 -0
- package/registry/bundles/health.yaml +45 -0
- package/registry/bundles/hook-governor.yaml +101 -0
- package/registry/bundles/incident-replay.yaml +47 -0
- package/registry/bundles/lsp-pack.yaml +80 -0
- package/registry/bundles/mcp-fabric.yaml +53 -0
- package/registry/bundles/plan-council.yaml +56 -0
- package/registry/bundles/preflight.yaml +48 -0
- package/registry/bundles/proof-gate.yaml +49 -0
- package/registry/bundles/remote-supervisor.yaml +49 -0
- package/registry/bundles/robotics.yaml +45 -0
- package/registry/bundles/secure-worktree-pipeline.yaml +69 -0
- package/registry/bundles/security-check.yaml +50 -0
- package/registry/bundles/terminal-lane.yaml +61 -0
- package/registry/bundles/test-intent-lock.yaml +49 -0
- package/registry/bundles/tracebank.yaml +47 -0
- package/registry/bundles/vision.yaml +45 -0
- package/registry/omg-capability.schema.json +378 -0
- package/registry/policy-packs/airgapped.lock.json +11 -0
- package/registry/policy-packs/airgapped.signature.json +10 -0
- package/registry/policy-packs/airgapped.yaml +16 -0
- package/registry/policy-packs/fintech.lock.json +11 -0
- package/registry/policy-packs/fintech.signature.json +10 -0
- package/registry/policy-packs/fintech.yaml +15 -0
- package/registry/policy-packs/locked-prod.lock.json +11 -0
- package/registry/policy-packs/locked-prod.signature.json +10 -0
- package/registry/policy-packs/locked-prod.yaml +18 -0
- package/registry/trusted_signers.json +44 -0
- package/registry/verify_artifact.py +493 -0
- package/runtime/__init__.py +36 -0
- package/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/__pycache__/adoption.cpython-313.pyc +0 -0
- package/runtime/__pycache__/agent_selector.cpython-313.pyc +0 -0
- package/runtime/__pycache__/api_twin.cpython-313.pyc +0 -0
- package/runtime/__pycache__/architecture_signal.cpython-313.pyc +0 -0
- package/runtime/__pycache__/artifact_parsers.cpython-313.pyc +0 -0
- package/runtime/__pycache__/asset_loader.cpython-313.pyc +0 -0
- package/runtime/__pycache__/background_verification.cpython-313.pyc +0 -0
- package/runtime/__pycache__/budget_envelopes.cpython-313.pyc +0 -0
- package/runtime/__pycache__/business_workflow.cpython-313.pyc +0 -0
- package/runtime/__pycache__/canonical_surface.cpython-313.pyc +0 -0
- package/runtime/__pycache__/canonical_taxonomy.cpython-313.pyc +0 -0
- package/runtime/__pycache__/claim_judge.cpython-313.pyc +0 -0
- package/runtime/__pycache__/cli_provider.cpython-313.pyc +0 -0
- package/runtime/__pycache__/compat.cpython-313.pyc +0 -0
- package/runtime/__pycache__/complexity_scorer.cpython-313.pyc +0 -0
- package/runtime/__pycache__/compliance_governor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/config_transaction.cpython-313.pyc +0 -0
- package/runtime/__pycache__/context_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/context_engine.cpython-313.pyc +0 -0
- package/runtime/__pycache__/context_limits.cpython-313.pyc +0 -0
- package/runtime/__pycache__/contract_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/custom_agent_loader.cpython-313.pyc +0 -0
- package/runtime/__pycache__/data_lineage.cpython-313.pyc +0 -0
- package/runtime/__pycache__/defense_state.cpython-313.pyc +0 -0
- package/runtime/__pycache__/delta_classifier.cpython-313.pyc +0 -0
- package/runtime/__pycache__/dispatcher.cpython-313.pyc +0 -0
- package/runtime/__pycache__/doc_generator.cpython-313.pyc +0 -0
- package/runtime/__pycache__/domain_packs.cpython-313.pyc +0 -0
- package/runtime/__pycache__/ecosystem.cpython-313.pyc +0 -0
- package/runtime/__pycache__/equalizer.cpython-313.pyc +0 -0
- package/runtime/__pycache__/eval_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_narrator.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_query.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_requirements.cpython-313.pyc +0 -0
- package/runtime/__pycache__/exec_kernel.cpython-313.pyc +0 -0
- package/runtime/__pycache__/explainer_formatter.cpython-313.pyc +0 -0
- package/runtime/__pycache__/feature_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_agents.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_contracts.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_domains.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_run_id.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_integration.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_review_bot.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_review_contract.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_review_formatter.cpython-313.pyc +0 -0
- package/runtime/__pycache__/guide_assert.cpython-313.pyc +0 -0
- package/runtime/__pycache__/hook_governor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/host_parity.cpython-313.pyc +0 -0
- package/runtime/__pycache__/incident_replay.cpython-313.pyc +0 -0
- package/runtime/__pycache__/install_planner.cpython-313.pyc +0 -0
- package/runtime/__pycache__/interaction_journal.cpython-313.pyc +0 -0
- package/runtime/__pycache__/issue_surface.cpython-313.pyc +0 -0
- package/runtime/__pycache__/legacy_compat.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mcp_config_writers.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mcp_lifecycle.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mcp_memory_server.cpython-313.pyc +0 -0
- package/runtime/__pycache__/memory_store.cpython-313.pyc +0 -0
- package/runtime/__pycache__/merge_writer.cpython-313.pyc +0 -0
- package/runtime/__pycache__/music_omr_testbed.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mutation_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/omc_compat.cpython-313.pyc +0 -0
- package/runtime/__pycache__/omg_browser_cli.cpython-313.pyc +0 -0
- package/runtime/__pycache__/omg_mcp_server.cpython-313.pyc +0 -0
- package/runtime/__pycache__/opus_plan.cpython-313.pyc +0 -0
- package/runtime/__pycache__/playwright_adapter.cpython-313.pyc +0 -0
- package/runtime/__pycache__/playwright_pack.cpython-313.pyc +0 -0
- package/runtime/__pycache__/plugin_diagnostics.cpython-313.pyc +0 -0
- package/runtime/__pycache__/plugin_interop.cpython-313.pyc +0 -0
- package/runtime/__pycache__/policy_pack_loader.cpython-313.pyc +0 -0
- package/runtime/__pycache__/preflight.cpython-313.pyc +0 -0
- package/runtime/__pycache__/profile_io.cpython-313.pyc +0 -0
- package/runtime/__pycache__/prompt_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/proof_chain.cpython-313.pyc +0 -0
- package/runtime/__pycache__/proof_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/provider_parity_eval.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_artifact_audit.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_run_coordinator.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_surface_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_surface_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_surfaces.cpython-313.pyc +0 -0
- package/runtime/__pycache__/remote_supervisor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/repro_pack.cpython-313.pyc +0 -0
- package/runtime/__pycache__/rollback_manifest.cpython-313.pyc +0 -0
- package/runtime/__pycache__/router_critics.cpython-313.pyc +0 -0
- package/runtime/__pycache__/router_executor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/router_selector.cpython-313.pyc +0 -0
- package/runtime/__pycache__/runtime_contracts.cpython-313.pyc +0 -0
- package/runtime/__pycache__/runtime_profile.cpython-313.pyc +0 -0
- package/runtime/__pycache__/security_check.cpython-313.pyc +0 -0
- package/runtime/__pycache__/session_health.cpython-313.pyc +0 -0
- package/runtime/__pycache__/skill_evolution.cpython-313.pyc +0 -0
- package/runtime/__pycache__/skill_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/subagent_dispatcher.cpython-313.pyc +0 -0
- package/runtime/__pycache__/subscription_tiers.cpython-313.pyc +0 -0
- package/runtime/__pycache__/team_router.cpython-313.pyc +0 -0
- package/runtime/__pycache__/test_intent_lock.cpython-313-pytest-9.0.2.pyc +0 -0
- package/runtime/__pycache__/test_intent_lock.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tmux_session_manager.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tool_fabric.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tool_plan_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tool_relevance.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tracebank.cpython-313.pyc +0 -0
- package/runtime/__pycache__/untrusted_content.cpython-313.pyc +0 -0
- package/runtime/__pycache__/validate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/verdict_schema.cpython-313.pyc +0 -0
- package/runtime/__pycache__/verification_controller.cpython-313.pyc +0 -0
- package/runtime/__pycache__/verification_loop.cpython-313.pyc +0 -0
- package/runtime/__pycache__/vision_artifacts.cpython-313.pyc +0 -0
- package/runtime/__pycache__/vision_cache.cpython-313.pyc +0 -0
- package/runtime/__pycache__/vision_jobs.cpython-313.pyc +0 -0
- package/runtime/__pycache__/worker_watchdog.cpython-313.pyc +0 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/adapters/__pycache__/claude.cpython-313.pyc +0 -0
- package/runtime/adapters/__pycache__/gpt.cpython-313.pyc +0 -0
- package/runtime/adapters/__pycache__/local.cpython-313.pyc +0 -0
- package/runtime/adapters/claude.py +63 -0
- package/runtime/adapters/gpt.py +56 -0
- package/runtime/adapters/local.py +56 -0
- package/runtime/adoption.py +280 -0
- package/runtime/api_twin.py +450 -0
- package/runtime/architecture_signal.py +226 -0
- package/runtime/artifact_parsers.py +161 -0
- package/runtime/asset_loader.py +62 -0
- package/runtime/background_verification.py +178 -0
- package/runtime/budget_envelopes.py +398 -0
- package/runtime/business_workflow.py +234 -0
- package/runtime/canonical_surface.py +53 -0
- package/runtime/canonical_taxonomy.py +27 -0
- package/runtime/claim_judge.py +648 -0
- package/runtime/cli_provider.py +105 -0
- package/runtime/compat.py +2222 -0
- package/runtime/complexity_scorer.py +148 -0
- package/runtime/compliance_governor.py +505 -0
- package/runtime/config_transaction.py +304 -0
- package/runtime/context_compiler.py +131 -0
- package/runtime/context_engine.py +708 -0
- package/runtime/context_limits.py +363 -0
- package/runtime/contract_compiler.py +3664 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/data_lineage.py +244 -0
- package/runtime/defense_state.py +261 -0
- package/runtime/delta_classifier.py +231 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/doc_generator.py +319 -0
- package/runtime/domain_packs.py +75 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/equalizer.py +268 -0
- package/runtime/eval_gate.py +96 -0
- package/runtime/evidence_narrator.py +147 -0
- package/runtime/evidence_query.py +303 -0
- package/runtime/evidence_registry.py +16 -0
- package/runtime/evidence_requirements.py +157 -0
- package/runtime/exec_kernel.py +267 -0
- package/runtime/explainer_formatter.py +82 -0
- package/runtime/feature_registry.py +109 -0
- package/runtime/forge_agents.py +915 -0
- package/runtime/forge_contracts.py +519 -0
- package/runtime/forge_domains.py +68 -0
- package/runtime/forge_run_id.py +86 -0
- package/runtime/guide_assert.py +135 -0
- package/runtime/hook_governor.py +156 -0
- package/runtime/host_parity.py +373 -0
- package/runtime/incident_replay.py +310 -0
- package/runtime/install_planner.py +617 -0
- package/runtime/interaction_journal.py +566 -0
- package/runtime/issue_surface.py +472 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +360 -0
- package/runtime/mcp_lifecycle.py +175 -0
- package/runtime/mcp_memory_server.py +220 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/chatgpt_parser.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/claude_import.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/export.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/gemini_import.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/kimi_import.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +1182 -0
- package/runtime/merge_writer.py +445 -0
- package/runtime/music_omr_testbed.py +336 -0
- package/runtime/mutation_gate.py +320 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omg_browser_cli.py +95 -0
- package/runtime/omg_compat_contract_snapshot.json +936 -0
- package/runtime/omg_contract_snapshot.json +936 -0
- package/runtime/omg_mcp_server.py +306 -0
- package/runtime/playwright_adapter.py +39 -0
- package/runtime/playwright_pack.py +253 -0
- package/runtime/plugin_diagnostics.py +308 -0
- package/runtime/plugin_interop.py +1060 -0
- package/runtime/policy_pack_loader.py +147 -0
- package/runtime/preflight.py +135 -0
- package/runtime/profile_io.py +328 -0
- package/runtime/proof_chain.py +472 -0
- package/runtime/proof_gate.py +442 -0
- package/runtime/provider_parity_eval.py +109 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/codex_provider.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/gemini_provider.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/kimi_provider.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/opencode_provider.cpython-313.pyc +0 -0
- package/runtime/providers/codex_provider.py +129 -0
- package/runtime/providers/gemini_provider.py +143 -0
- package/runtime/providers/kimi_provider.py +167 -0
- package/runtime/providers/opencode_provider.py +99 -0
- package/runtime/release_artifact_audit.py +556 -0
- package/runtime/release_run_coordinator.py +574 -0
- package/runtime/release_surface_compiler.py +643 -0
- package/runtime/release_surface_registry.py +283 -0
- package/runtime/release_surfaces.py +320 -0
- package/runtime/remote_supervisor.py +79 -0
- package/runtime/repro_pack.py +398 -0
- package/runtime/rollback_manifest.py +143 -0
- package/runtime/router_critics.py +229 -0
- package/runtime/router_executor.py +142 -0
- package/runtime/router_selector.py +99 -0
- package/runtime/runtime_contracts.py +292 -0
- package/runtime/runtime_profile.py +133 -0
- package/runtime/security_check.py +1094 -0
- package/runtime/session_health.py +546 -0
- package/runtime/skill_evolution.py +221 -0
- package/runtime/skill_registry.py +53 -0
- package/runtime/subagent_dispatcher.py +604 -0
- package/runtime/subscription_tiers.py +258 -0
- package/runtime/team_router.py +1399 -0
- package/runtime/test_intent_lock.py +543 -0
- package/runtime/tmux_session_manager.py +172 -0
- package/runtime/tool_fabric.py +570 -0
- package/runtime/tool_plan_gate.py +460 -0
- package/runtime/tracebank.py +125 -0
- package/runtime/untrusted_content.py +360 -0
- package/runtime/validate.py +293 -0
- package/runtime/verdict_schema.py +198 -0
- package/runtime/verification_controller.py +235 -0
- package/runtime/verification_loop.py +73 -0
- package/runtime/vision_artifacts.py +31 -0
- package/runtime/vision_cache.py +38 -0
- package/runtime/vision_jobs.py +92 -0
- package/runtime/worker_watchdog.py +526 -0
- package/scripts/__pycache__/audit-published-artifact.cpython-313.pyc +0 -0
- package/scripts/__pycache__/check-doc-parity.cpython-313.pyc +0 -0
- package/scripts/__pycache__/check-omg-standalone-clean.cpython-313.pyc +0 -0
- package/scripts/__pycache__/github_review_helpers.cpython-313.pyc +0 -0
- package/scripts/__pycache__/omg.cpython-313.pyc +0 -0
- package/scripts/__pycache__/prepare-release-proof-fixtures.cpython-313.pyc +0 -0
- package/scripts/__pycache__/sync-release-identity.cpython-313.pyc +0 -0
- package/scripts/__pycache__/validate-release-identity.cpython-313.pyc +0 -0
- package/scripts/audit-published-artifact.py +59 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +273 -0
- package/scripts/check-omg-standalone-clean.py +133 -0
- package/scripts/emit_host_parity.py +72 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +2962 -0
- package/scripts/pre-release-check.sh +38 -0
- package/scripts/prepare-release-proof-fixtures.py +602 -0
- package/scripts/print-canonical-version.py +80 -0
- package/scripts/settings-merge.py +289 -0
- package/scripts/sync-release-identity.py +481 -0
- package/scripts/validate-release-identity.py +632 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +35 -0
- package/settings.json +751 -0
- package/tools/__init__.py +2 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/browser_consent.cpython-313.pyc +0 -0
- package/tools/__pycache__/browser_stealth.cpython-313.pyc +0 -0
- package/tools/__pycache__/browser_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/changelog_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/commit_splitter.cpython-313.pyc +0 -0
- package/tools/__pycache__/config_discovery.cpython-313.pyc +0 -0
- package/tools/__pycache__/config_merger.cpython-313.pyc +0 -0
- package/tools/__pycache__/dashboard_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/git_inspector.cpython-313.pyc +0 -0
- package/tools/__pycache__/lsp_client.cpython-313.pyc +0 -0
- package/tools/__pycache__/lsp_operations.cpython-313.pyc +0 -0
- package/tools/__pycache__/pr_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/python_repl.cpython-313.pyc +0 -0
- package/tools/__pycache__/python_sandbox.cpython-313.pyc +0 -0
- package/tools/__pycache__/session_snapshot.cpython-313.pyc +0 -0
- package/tools/__pycache__/ssh_manager.cpython-313.pyc +0 -0
- package/tools/__pycache__/theme_engine.cpython-313.pyc +0 -0
- package/tools/__pycache__/theme_selector.cpython-313.pyc +0 -0
- package/tools/__pycache__/web_search.cpython-313.pyc +0 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +749 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +712 -0
- package/tools/python_sandbox.py +768 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/brave.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/exa.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/jina.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/perplexity.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/synthetic.cpython-313.pyc +0 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +851 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +296 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +675 -0
package/hooks/_common.py
ADDED
|
@@ -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
|