@trac3er/oh-my-god 1.0.2
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/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -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 +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -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/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -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:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -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 +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -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 +119 -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 +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- package/tools/__init__.py +2 -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 +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -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/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -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 +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUseFailure Hook — Logs tool failures for enhanced tracking."""
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
7
|
+
|
|
8
|
+
from _common import setup_crash_handler, json_input, get_feature_flag, log_hook_error
|
|
9
|
+
|
|
10
|
+
setup_crash_handler('post-tool-failure')
|
|
11
|
+
|
|
12
|
+
data = json_input()
|
|
13
|
+
tool_name = data.get('tool_name', 'unknown')
|
|
14
|
+
error = data.get('error', data.get('message', 'unknown error'))
|
|
15
|
+
|
|
16
|
+
# Log to hook-errors.jsonl using the shared utility
|
|
17
|
+
log_hook_error('post-tool-failure', error, context={'tool': tool_name})
|
|
18
|
+
|
|
19
|
+
sys.exit(0)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse Hook (Write/Edit/MultiEdit): Auto-Format + Secret Scan (Enterprise)
|
|
4
|
+
1. Auto-format written files if opted-in via .omg/state/quality-gate.json (non-blocking)
|
|
5
|
+
2. Scan written content for hardcoded secrets (blocking: exit 2)
|
|
6
|
+
"""
|
|
7
|
+
import json, sys, os, re, subprocess
|
|
8
|
+
import contextlib
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from _common import _resolve_project_dir
|
|
11
|
+
from state_migration import resolve_state_file
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
data = json.load(sys.stdin)
|
|
15
|
+
except (json.JSONDecodeError, EOFError):
|
|
16
|
+
sys.exit(0)
|
|
17
|
+
|
|
18
|
+
file_path = data.get("tool_input", {}).get("file_path", "")
|
|
19
|
+
if not file_path:
|
|
20
|
+
sys.exit(0)
|
|
21
|
+
|
|
22
|
+
# Resolve relative paths against project dir
|
|
23
|
+
project_dir = _resolve_project_dir()
|
|
24
|
+
if not os.path.isabs(file_path):
|
|
25
|
+
file_path = os.path.join(project_dir, file_path)
|
|
26
|
+
|
|
27
|
+
if not os.path.exists(file_path):
|
|
28
|
+
sys.exit(0)
|
|
29
|
+
|
|
30
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
31
|
+
|
|
32
|
+
# ── 1. AUTO-FORMAT (opt-in via quality-gate.json, non-blocking) ──
|
|
33
|
+
# §4.4: Auto-format only runs if the project has opted in via quality-gate.json.
|
|
34
|
+
# This avoids unintended tool execution (supply-chain risk) on projects without
|
|
35
|
+
# explicit formatter configuration.
|
|
36
|
+
format_enabled = False
|
|
37
|
+
qg_path = resolve_state_file(project_dir, "state/quality-gate.json", "quality-gate.json")
|
|
38
|
+
with contextlib.suppress(Exception): # intentional: cleanup — format stays disabled on config error
|
|
39
|
+
if os.path.exists(qg_path):
|
|
40
|
+
with open(qg_path, "r") as f:
|
|
41
|
+
qg = json.load(f)
|
|
42
|
+
# "format" key must exist and not be null/empty
|
|
43
|
+
if qg.get("format"):
|
|
44
|
+
format_enabled = True
|
|
45
|
+
|
|
46
|
+
FORMAT_MAP = {
|
|
47
|
+
".ts": ["npx", "--no-install", "prettier", "--write"],
|
|
48
|
+
".tsx": ["npx", "--no-install", "prettier", "--write"],
|
|
49
|
+
".js": ["npx", "--no-install", "prettier", "--write"],
|
|
50
|
+
".jsx": ["npx", "--no-install", "prettier", "--write"],
|
|
51
|
+
".css": ["npx", "--no-install", "prettier", "--write"],
|
|
52
|
+
".json": ["npx", "--no-install", "prettier", "--write"],
|
|
53
|
+
".py": ["ruff", "format"], ".go": ["gofmt", "-w"], ".rs": ["rustfmt"],
|
|
54
|
+
}
|
|
55
|
+
if format_enabled and ext in FORMAT_MAP:
|
|
56
|
+
fmt_cmd = FORMAT_MAP[ext]
|
|
57
|
+
# Validate formatter binary exists before running (supply-chain defense)
|
|
58
|
+
import shutil
|
|
59
|
+
if shutil.which(fmt_cmd[0]):
|
|
60
|
+
try:
|
|
61
|
+
subprocess.run(fmt_cmd + [file_path], capture_output=True, timeout=15, cwd=project_dir)
|
|
62
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
# ── 2. SECRET SCAN (blocking) ──
|
|
66
|
+
# Skip binary files and very large files
|
|
67
|
+
try:
|
|
68
|
+
file_size = os.path.getsize(file_path)
|
|
69
|
+
if file_size > 1_000_000: # 1MB limit
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
72
|
+
content = f.read()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"[OMG] post-write.py: {type(e).__name__}: {e}", file=sys.stderr)
|
|
75
|
+
sys.exit(0)
|
|
76
|
+
|
|
77
|
+
# Skip known non-secret file types
|
|
78
|
+
SKIP_EXTENSIONS = {".lock", ".sum", ".svg", ".png", ".jpg", ".gif", ".ico", ".woff", ".woff2", ".ttf"}
|
|
79
|
+
if ext in SKIP_EXTENSIONS:
|
|
80
|
+
sys.exit(0)
|
|
81
|
+
|
|
82
|
+
SECRET_PATTERNS = [
|
|
83
|
+
# AWS
|
|
84
|
+
(r"AKIA[0-9A-Z]{16}", "AWS Access Key ID"),
|
|
85
|
+
(r"(?:aws_secret_access_key|AWS_SECRET)\s*[:=]\s*['\"]?[A-Za-z0-9/+=]{40}['\"]?", "AWS Secret Key"),
|
|
86
|
+
# Private keys
|
|
87
|
+
(r"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----", "Private Key"),
|
|
88
|
+
# Generic API keys/tokens (in assignment context)
|
|
89
|
+
(r"""(?:api[_-]?key|api[_-]?secret|auth[_-]?token|access[_-]?token|secret[_-]?key)\s*[:=]\s*['"][A-Za-z0-9+/=_\-.]{20,}['"]""", "Hardcoded API Key/Token"),
|
|
90
|
+
# GitHub
|
|
91
|
+
(r"gh[ps]_[A-Za-z0-9_]{36,}", "GitHub Token"),
|
|
92
|
+
(r"github_pat_[A-Za-z0-9_]{22,}", "GitHub Fine-grained PAT"),
|
|
93
|
+
# Slack
|
|
94
|
+
(r"xoxb-[0-9]{10,}-[A-Za-z0-9]{20,}", "Slack Bot Token"),
|
|
95
|
+
(r"xoxp-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{20,}", "Slack User Token"),
|
|
96
|
+
# Stripe
|
|
97
|
+
(r"sk_live_[A-Za-z0-9]{20,}", "Stripe Live Secret Key"),
|
|
98
|
+
(r"rk_live_[A-Za-z0-9]{20,}", "Stripe Restricted Key"),
|
|
99
|
+
(r"pk_live_[A-Za-z0-9]{20,}", "Stripe Live Publishable Key (should use env)"),
|
|
100
|
+
# Supabase / Firebase
|
|
101
|
+
(r"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{20,}", "Supabase/Firebase Service Key"),
|
|
102
|
+
# Google
|
|
103
|
+
(r"AIza[A-Za-z0-9_-]{35}", "Google API Key"),
|
|
104
|
+
# Twilio
|
|
105
|
+
(r"SK[A-Za-z0-9]{32}", "Twilio API Key"),
|
|
106
|
+
# SendGrid
|
|
107
|
+
(r"SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}", "SendGrid API Key"),
|
|
108
|
+
# Passwords in config
|
|
109
|
+
(r"""(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]""", "Hardcoded Password"),
|
|
110
|
+
# Generic secret in env-like format
|
|
111
|
+
(r"""(?:SECRET|TOKEN|PRIVATE_KEY|ENCRYPTION_KEY)\s*=\s*['"]?[A-Za-z0-9+/=_\-.]{16,}['"]?""", "Hardcoded Secret"),
|
|
112
|
+
# Database connection strings with credentials
|
|
113
|
+
(r"(?:postgres|mysql|mongodb|redis)://[^:]+:[^@]+@", "Database URL with credentials"),
|
|
114
|
+
# JWT tokens (3 base64 segments separated by dots)
|
|
115
|
+
(r"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}", "JWT Token"),
|
|
116
|
+
# Hardcoded URLs with credentials
|
|
117
|
+
(r"https?://[^:]+:[^@]+@", "URL with embedded credentials"),
|
|
118
|
+
# Webhook URLs (often secret)
|
|
119
|
+
(r"""(?:webhook[_-]?url|slack[_-]?webhook|discord[_-]?webhook)\s*[:=]\s*['"]https?://""", "Hardcoded Webhook URL"),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
# URI / Security anti-patterns (WARNING, not blocking)
|
|
123
|
+
SECURITY_WARNINGS = [
|
|
124
|
+
(r"cors\s*\(\s*\{[^}]*origin\s*:\s*['\"]?\*['\"]?", "CORS wildcard origin in code — use whitelist in production"),
|
|
125
|
+
(r"httpOnly\s*:\s*false", "Cookie httpOnly disabled — session cookies should be httpOnly"),
|
|
126
|
+
(r"secure\s*:\s*false", "Cookie secure flag disabled — use HTTPS in production"),
|
|
127
|
+
(r"eval\s*\(", "eval() usage — potential code injection risk"),
|
|
128
|
+
(r"innerHTML\s*=", "innerHTML assignment — potential XSS risk"),
|
|
129
|
+
(r"dangerouslySetInnerHTML", "dangerouslySetInnerHTML — verify input is sanitized"),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
findings = []
|
|
133
|
+
patterns_matched = []
|
|
134
|
+
for i, line in enumerate(content.split("\n"), 1):
|
|
135
|
+
stripped = line.strip()
|
|
136
|
+
# Skip lines that are entirely comments (bare "*" removed — too broad)
|
|
137
|
+
if stripped.startswith(("#", "//", "/*", "* ", "<!--", "%", ";")):
|
|
138
|
+
continue
|
|
139
|
+
# Skip test files: check parent DIRECTORY for test dirs, not just filename
|
|
140
|
+
lowpath = file_path.lower()
|
|
141
|
+
is_test_file = any(d in lowpath for d in ["/__tests__/", "/test/", "/tests/", "/fixtures/", "/mocks/", "/__mocks__/"])
|
|
142
|
+
if not is_test_file:
|
|
143
|
+
basename = os.path.basename(file_path).lower()
|
|
144
|
+
is_test_file = any(p in basename for p in [".test.", ".spec.", "_test.", "test_"])
|
|
145
|
+
if is_test_file:
|
|
146
|
+
continue
|
|
147
|
+
for pattern, label in SECRET_PATTERNS:
|
|
148
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
149
|
+
findings.append(f" Line {i}: {label}")
|
|
150
|
+
if label not in patterns_matched:
|
|
151
|
+
patterns_matched.append(label)
|
|
152
|
+
break # One finding per line is enough
|
|
153
|
+
|
|
154
|
+
if findings:
|
|
155
|
+
try:
|
|
156
|
+
proj_dir = _resolve_project_dir()
|
|
157
|
+
state_dir = os.path.join(proj_dir, ".omg", "state")
|
|
158
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
159
|
+
signal_path = os.path.join(state_dir, "secret-detected.json")
|
|
160
|
+
signal_payload = {
|
|
161
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
162
|
+
"file": file_path,
|
|
163
|
+
"patterns_matched": patterns_matched,
|
|
164
|
+
"action": "blocked",
|
|
165
|
+
}
|
|
166
|
+
with open(signal_path, "w", encoding="utf-8") as f:
|
|
167
|
+
json.dump(signal_payload, f)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
print(f"[OMG] post-write.py: {type(e).__name__}: {e}", file=sys.stderr)
|
|
170
|
+
print(
|
|
171
|
+
f"⚠ SECRET DETECTED in {file_path}. Signal written to .omg/state/secret-detected.json",
|
|
172
|
+
file=sys.stderr,
|
|
173
|
+
)
|
|
174
|
+
msg = f"SECRET DETECTED in {file_path}:\n" + "\n".join(findings[:10])
|
|
175
|
+
if len(findings) > 10:
|
|
176
|
+
msg += f"\n ... and {len(findings) - 10} more"
|
|
177
|
+
msg += "\n\nRemove hardcoded secrets. Use environment variables or a secret manager."
|
|
178
|
+
print(msg, file=sys.stderr)
|
|
179
|
+
# NOTE: exit(0), not exit(2). Non-zero exits crash sibling hooks
|
|
180
|
+
# ("Sibling tool call errored"). The warning in stderr is still visible.
|
|
181
|
+
sys.exit(0)
|
|
182
|
+
|
|
183
|
+
# ── 3. SECURITY WARNING SCAN (non-blocking, advisory) ──
|
|
184
|
+
sec_warnings = []
|
|
185
|
+
for i, line in enumerate(content.split("\n"), 1):
|
|
186
|
+
stripped = line.strip()
|
|
187
|
+
if stripped.startswith(("#", "//", "/*", "*", "<!--")):
|
|
188
|
+
continue
|
|
189
|
+
for pattern, label in SECURITY_WARNINGS:
|
|
190
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
191
|
+
sec_warnings.append(f" Line {i}: ⚠ {label}")
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
if sec_warnings:
|
|
195
|
+
msg = f"SECURITY WARNINGS in {file_path}:\n" + "\n".join(sec_warnings[:5])
|
|
196
|
+
msg += "\n\nConsider running /OMG:security-review for a full audit."
|
|
197
|
+
print(msg, file=sys.stderr)
|
|
198
|
+
|
|
199
|
+
sys.exit(0)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreCompact Hook — OMG Standalone state preservation.
|
|
3
|
+
|
|
4
|
+
1) Snapshot key files from .omg/state (fallback .omc via migration)
|
|
5
|
+
2) Auto-generate handoff files in .omg/state
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import importlib
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from hooks.state_migration import resolve_state_file, resolve_state_dir
|
|
17
|
+
from hooks._common import _resolve_project_dir
|
|
18
|
+
except ImportError:
|
|
19
|
+
_state_migration = importlib.import_module("state_migration")
|
|
20
|
+
_common = importlib.import_module("_common")
|
|
21
|
+
resolve_state_file = _state_migration.resolve_state_file
|
|
22
|
+
resolve_state_dir = _state_migration.resolve_state_dir
|
|
23
|
+
_resolve_project_dir = _common._resolve_project_dir
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
MAX_SNAPSHOT_BYTES = int(os.environ.get("OMG_PRECOMPACT_MAX_SNAPSHOT_BYTES", "262144"))
|
|
27
|
+
GIT_DIFF_TIMEOUT_SEC = int(os.environ.get("OMG_PRECOMPACT_GIT_DIFF_TIMEOUT_SEC", "1"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
data = json.load(sys.stdin)
|
|
32
|
+
except (json.JSONDecodeError, EOFError):
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
project_dir = _resolve_project_dir()
|
|
36
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
37
|
+
state_dir = resolve_state_dir(project_dir, "state", "")
|
|
38
|
+
snapshot_dir = os.path.join(state_dir, "snapshots", ts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_file(path, max_lines=None):
|
|
42
|
+
try:
|
|
43
|
+
if not os.path.exists(path):
|
|
44
|
+
return None
|
|
45
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
46
|
+
content = f.read().strip()
|
|
47
|
+
if not content:
|
|
48
|
+
return None
|
|
49
|
+
if max_lines:
|
|
50
|
+
return "\n".join(content.split("\n")[:max_lines])
|
|
51
|
+
return content
|
|
52
|
+
except Exception:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def read_cache(paths):
|
|
57
|
+
cache = {}
|
|
58
|
+
for path in paths:
|
|
59
|
+
cache[path] = read_file(path)
|
|
60
|
+
return cache
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def first_lines(text, max_lines):
|
|
64
|
+
if not text:
|
|
65
|
+
return None
|
|
66
|
+
if not max_lines:
|
|
67
|
+
return text
|
|
68
|
+
return "\n".join(text.splitlines()[:max_lines])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def snapshot_file(src_path, dst_path, max_bytes):
|
|
72
|
+
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
|
73
|
+
try:
|
|
74
|
+
size = os.path.getsize(src_path)
|
|
75
|
+
except OSError:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
if max_bytes <= 0 or size <= max_bytes:
|
|
79
|
+
shutil.copy2(src_path, dst_path)
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
with open(src_path, "rb") as src_f:
|
|
83
|
+
data = src_f.read(max_bytes)
|
|
84
|
+
note = (
|
|
85
|
+
f"\n\n[TRUNCATED by pre-compact: original_bytes={size}, kept_bytes={len(data)}]"
|
|
86
|
+
).encode("utf-8")
|
|
87
|
+
with open(dst_path, "wb") as dst_f:
|
|
88
|
+
dst_f.write(data)
|
|
89
|
+
dst_f.write(note)
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
snapshot_files = [
|
|
94
|
+
resolve_state_file(project_dir, "state/profile.yaml", "profile.yaml"),
|
|
95
|
+
resolve_state_file(project_dir, "state/working-memory.md", "working-memory.md"),
|
|
96
|
+
resolve_state_file(project_dir, "state/_plan.md", "_plan.md"),
|
|
97
|
+
resolve_state_file(project_dir, "state/_checklist.md", "_checklist.md"),
|
|
98
|
+
resolve_state_file(project_dir, "state/quality-gate.json", "quality-gate.json"),
|
|
99
|
+
resolve_state_file(project_dir, "state/ledger/tool-ledger.jsonl", "ledger/tool-ledger.jsonl"),
|
|
100
|
+
resolve_state_file(project_dir, "state/ledger/failure-tracker.json", "ledger/failure-tracker.json"),
|
|
101
|
+
resolve_state_file(project_dir, "state/ralph-loop.json", "ralph-loop.json"),
|
|
102
|
+
]
|
|
103
|
+
cached = read_cache(snapshot_files)
|
|
104
|
+
saved = []
|
|
105
|
+
for src in snapshot_files:
|
|
106
|
+
if cached.get(src) is not None:
|
|
107
|
+
dst = os.path.join(snapshot_dir, os.path.basename(src))
|
|
108
|
+
if snapshot_file(src, dst, MAX_SNAPSHOT_BYTES):
|
|
109
|
+
saved.append(os.path.basename(src))
|
|
110
|
+
|
|
111
|
+
profile = first_lines(cached.get(resolve_state_file(project_dir, "state/profile.yaml", "profile.yaml")), 20)
|
|
112
|
+
wm = first_lines(cached.get(resolve_state_file(project_dir, "state/working-memory.md", "working-memory.md")), 15)
|
|
113
|
+
plan = first_lines(cached.get(resolve_state_file(project_dir, "state/_plan.md", "_plan.md")), 10)
|
|
114
|
+
checklist = first_lines(cached.get(resolve_state_file(project_dir, "state/_checklist.md", "_checklist.md")), 50)
|
|
115
|
+
tracker = cached.get(resolve_state_file(project_dir, "state/ledger/failure-tracker.json", "ledger/failure-tracker.json"))
|
|
116
|
+
ralph_loop = cached.get(resolve_state_file(project_dir, "state/ralph-loop.json", "ralph-loop.json"))
|
|
117
|
+
|
|
118
|
+
parts = [
|
|
119
|
+
f"# Handoff -- {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
120
|
+
"Auto-generated before context compaction.",
|
|
121
|
+
]
|
|
122
|
+
if profile:
|
|
123
|
+
parts.append("<!-- section: working-state -->")
|
|
124
|
+
parts.append("## Project\n" + profile)
|
|
125
|
+
if wm:
|
|
126
|
+
parts.append("## Working State\n" + wm)
|
|
127
|
+
if plan:
|
|
128
|
+
parts.append("## Plan\n" + plan)
|
|
129
|
+
if checklist:
|
|
130
|
+
lines = checklist.split("\n")
|
|
131
|
+
done = sum(1 for l in lines if "[x]" in l.lower())
|
|
132
|
+
total = sum(1 for l in lines if l.strip().startswith(("[", "- [")))
|
|
133
|
+
pending = [l.strip() for l in lines if "[ ]" in l][:3]
|
|
134
|
+
parts.append("<!-- section: progress -->")
|
|
135
|
+
parts.append(f"## Progress: {done}/{total}")
|
|
136
|
+
if pending:
|
|
137
|
+
parts.append("Next:\n" + "\n".join(pending))
|
|
138
|
+
if tracker:
|
|
139
|
+
try:
|
|
140
|
+
t = json.loads(tracker)
|
|
141
|
+
active = {k: v for k, v in t.items() if isinstance(v, dict) and v.get("count", 0) >= 2}
|
|
142
|
+
if active:
|
|
143
|
+
warns = [f"- {k}: {v['count']}x" for k, v in list(active.items())[:5]]
|
|
144
|
+
parts.append("## Failed Approaches\n" + "\n".join(warns))
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
if ralph_loop:
|
|
148
|
+
try:
|
|
149
|
+
rl = json.loads(ralph_loop)
|
|
150
|
+
if rl.get("active"):
|
|
151
|
+
rl_iter = rl.get("iteration", 0)
|
|
152
|
+
rl_max = rl.get("max_iterations", 50)
|
|
153
|
+
rl_goal = rl.get("original_prompt", "")[:80]
|
|
154
|
+
parts.append(f"## Ralph Loop\nIteration: {rl_iter}/{rl_max} | Goal: {rl_goal}")
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
diff_names = subprocess.run(
|
|
160
|
+
["git", "diff", "--name-only"],
|
|
161
|
+
capture_output=True,
|
|
162
|
+
text=True,
|
|
163
|
+
timeout=GIT_DIFF_TIMEOUT_SEC,
|
|
164
|
+
cwd=project_dir,
|
|
165
|
+
)
|
|
166
|
+
changed = [l for l in diff_names.stdout.strip().split("\n") if l]
|
|
167
|
+
if changed:
|
|
168
|
+
parts.append("## Uncommitted\n" + "\n".join(f"- {x}" for x in changed[:5]))
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
parts.append("## Resume Instructions")
|
|
173
|
+
parts.append("Read .omg/state/profile.yaml + this file.")
|
|
174
|
+
parts.append("\n---\n*Auto-generated before context compaction.*")
|
|
175
|
+
handoff = "\n\n".join(parts)
|
|
176
|
+
handoff_lines = handoff.split("\n")
|
|
177
|
+
if len(handoff_lines) > 120:
|
|
178
|
+
handoff = "\n".join(handoff_lines[:120]) + "\n\n(truncated)"
|
|
179
|
+
|
|
180
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
181
|
+
with open(os.path.join(state_dir, "handoff.md"), "w", encoding="utf-8") as f:
|
|
182
|
+
f.write(handoff)
|
|
183
|
+
|
|
184
|
+
portable = handoff + "\n\nSelf-contained handoff for other platforms."
|
|
185
|
+
portable_lines = portable.split("\n")
|
|
186
|
+
if len(portable_lines) > 150:
|
|
187
|
+
portable = "\n".join(portable_lines[:150]) + "\n\n(truncated)"
|
|
188
|
+
with open(os.path.join(state_dir, "handoff-portable.md"), "w", encoding="utf-8") as f:
|
|
189
|
+
f.write(portable)
|
|
190
|
+
|
|
191
|
+
# Keep latest 5 snapshots
|
|
192
|
+
snapshots_parent = os.path.join(state_dir, "snapshots")
|
|
193
|
+
try:
|
|
194
|
+
if os.path.isdir(snapshots_parent):
|
|
195
|
+
entries = sorted(
|
|
196
|
+
[d for d in os.listdir(snapshots_parent) if os.path.isdir(os.path.join(snapshots_parent, d))]
|
|
197
|
+
)
|
|
198
|
+
for old in entries[:-5]:
|
|
199
|
+
shutil.rmtree(os.path.join(snapshots_parent, old), ignore_errors=True)
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
print(f"[OMG pre-compact] Snapshotted {len(saved)} files -> {snapshot_dir}", file=sys.stderr)
|
|
204
|
+
sys.exit(0)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PreToolUse Hook — Injects plan reminder before each tool call.
|
|
3
|
+
|
|
4
|
+
Inspired by planning-with-files: forces re-read of plan on every tool call.
|
|
5
|
+
OMG version: lighter — checklist-aware, tool-filtered, max 200 chars.
|
|
6
|
+
Only injects for mutation tools (Write/Edit/Bash), not read-only tools.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
HOOKS_DIR = os.path.dirname(__file__)
|
|
14
|
+
if HOOKS_DIR not in sys.path:
|
|
15
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
16
|
+
|
|
17
|
+
from _common import setup_crash_handler, json_input, get_feature_flag, _resolve_project_dir
|
|
18
|
+
from state_migration import resolve_state_file
|
|
19
|
+
|
|
20
|
+
setup_crash_handler("pre-tool-inject")
|
|
21
|
+
|
|
22
|
+
MAX_INJECTION = 200 # Total injection budget (chars)
|
|
23
|
+
|
|
24
|
+
# Read-only tools that don't need plan reminders
|
|
25
|
+
READ_ONLY_TOOLS = {
|
|
26
|
+
'Read', 'Glob', 'Grep', 'LS', 'NotebookRead', 'WebFetch', 'WebSearch',
|
|
27
|
+
'TodoRead', 'mcp__filesystem__read_file', 'mcp__filesystem__list_directory',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def should_inject(tool_name):
|
|
32
|
+
"""Return True if this tool call should get a plan reminder."""
|
|
33
|
+
if not tool_name:
|
|
34
|
+
return True # unknown tool → inject (safe default)
|
|
35
|
+
return tool_name not in READ_ONLY_TOOLS
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_checklist_progress(checklist_path):
|
|
39
|
+
"""Return (done, total, first_pending) from checklist file."""
|
|
40
|
+
if not os.path.exists(checklist_path):
|
|
41
|
+
return None, None, None
|
|
42
|
+
try:
|
|
43
|
+
with open(checklist_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
44
|
+
lines = f.readlines()
|
|
45
|
+
total = sum(1 for l in lines if re.search(r'^\s*-\s*\[[ x!]\]', l))
|
|
46
|
+
done = sum(1 for l in lines if re.search(r'^\s*-\s*\[x\]', l, re.IGNORECASE))
|
|
47
|
+
# Find first pending item text
|
|
48
|
+
first_pending = None
|
|
49
|
+
for l in lines:
|
|
50
|
+
if re.search(r'^\s*-\s*\[ \]', l):
|
|
51
|
+
first_pending = re.sub(r'^\s*-\s*\[ \]\s*', '', l).strip()[:50]
|
|
52
|
+
break
|
|
53
|
+
return done, total, first_pending
|
|
54
|
+
except OSError:
|
|
55
|
+
return None, None, None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
data = json_input()
|
|
59
|
+
|
|
60
|
+
if not get_feature_flag("planning_enforcement"):
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
# Tool filtering: skip injection for read-only tools
|
|
64
|
+
tool_name = data.get("tool_name") if isinstance(data, dict) else None
|
|
65
|
+
if not should_inject(tool_name):
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
project_dir = _resolve_project_dir()
|
|
69
|
+
|
|
70
|
+
# Try to find _plan.md
|
|
71
|
+
plan_path = resolve_state_file(project_dir, "state/_plan.md", "_plan.md")
|
|
72
|
+
|
|
73
|
+
if not os.path.exists(plan_path):
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Check for checklist progress
|
|
78
|
+
checklist_path = resolve_state_file(project_dir, "state/_checklist.md", "_checklist.md")
|
|
79
|
+
done, total, first_pending = get_checklist_progress(checklist_path)
|
|
80
|
+
|
|
81
|
+
if total is not None and total > 0:
|
|
82
|
+
# Checklist-aware format
|
|
83
|
+
reminder = f"{done}/{total} done"
|
|
84
|
+
if first_pending:
|
|
85
|
+
reminder += f" | Next: {first_pending}"
|
|
86
|
+
injection = f"@plan-reminder: {reminder}"[:MAX_INJECTION]
|
|
87
|
+
else:
|
|
88
|
+
# Fallback: first 15 lines of plan
|
|
89
|
+
with open(plan_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
90
|
+
lines = f.readlines()[:15]
|
|
91
|
+
head = "".join(lines)[:MAX_INJECTION]
|
|
92
|
+
injection = f"@plan-reminder: {head}"[:MAX_INJECTION]
|
|
93
|
+
|
|
94
|
+
json.dump({"contextInjection": injection}, sys.stdout)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass # Graceful degradation
|
|
97
|
+
|
|
98
|
+
sys.exit(0)
|