@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
|
@@ -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()
|