@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.
Files changed (229) hide show
  1. package/.claude-plugin/marketplace.json +36 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.claude-plugin/scripts/install.sh +49 -0
  4. package/.claude-plugin/scripts/uninstall.sh +80 -0
  5. package/.claude-plugin/scripts/update.sh +84 -0
  6. package/.mcp.json +20 -0
  7. package/LICENSE +21 -0
  8. package/OMG-setup.sh +1093 -0
  9. package/README.md +335 -0
  10. package/THIRD_PARTY_NOTICES.md +24 -0
  11. package/UPSTREAM_DIFF.md +20 -0
  12. package/agents/__init__.py +1 -0
  13. package/agents/_model_roles.yaml +26 -0
  14. package/agents/designer.md +67 -0
  15. package/agents/explore.md +60 -0
  16. package/agents/model_roles.py +196 -0
  17. package/agents/omg-api-builder.md +23 -0
  18. package/agents/omg-architect-mode.md +43 -0
  19. package/agents/omg-architect.md +13 -0
  20. package/agents/omg-backend-engineer.md +43 -0
  21. package/agents/omg-critic.md +16 -0
  22. package/agents/omg-database-engineer.md +43 -0
  23. package/agents/omg-escalation-router.md +17 -0
  24. package/agents/omg-executor.md +12 -0
  25. package/agents/omg-frontend-designer.md +42 -0
  26. package/agents/omg-implement-mode.md +50 -0
  27. package/agents/omg-infra-engineer.md +43 -0
  28. package/agents/omg-qa-tester.md +16 -0
  29. package/agents/omg-research-mode.md +43 -0
  30. package/agents/omg-security-auditor.md +43 -0
  31. package/agents/omg-testing-engineer.md +43 -0
  32. package/agents/plan.md +80 -0
  33. package/agents/quick_task.md +64 -0
  34. package/agents/reviewer.md +83 -0
  35. package/agents/task.md +71 -0
  36. package/commands/OMG:ccg.md +22 -0
  37. package/commands/OMG:compat.md +57 -0
  38. package/commands/OMG:crazy.md +125 -0
  39. package/commands/OMG:domain-init.md +11 -0
  40. package/commands/OMG:escalate.md +52 -0
  41. package/commands/OMG:health-check.md +45 -0
  42. package/commands/OMG:init.md +134 -0
  43. package/commands/OMG:mode.md +44 -0
  44. package/commands/OMG:project-init.md +11 -0
  45. package/commands/OMG:ralph-start.md +43 -0
  46. package/commands/OMG:ralph-stop.md +23 -0
  47. package/commands/OMG:teams.md +39 -0
  48. package/commands/ai-commit.md +113 -0
  49. package/commands/ccg.md +9 -0
  50. package/commands/create-agent.md +183 -0
  51. package/commands/omc-teams.md +9 -0
  52. package/commands/session-branch.md +85 -0
  53. package/commands/session-fork.md +53 -0
  54. package/commands/session-merge.md +134 -0
  55. package/commands/theme.md +44 -0
  56. package/config/lsp_languages.yaml +324 -0
  57. package/config/themes/catppuccin-frappe.yaml +14 -0
  58. package/config/themes/catppuccin-latte.yaml +14 -0
  59. package/config/themes/catppuccin-macchiato.yaml +14 -0
  60. package/config/themes/catppuccin-mocha.yaml +14 -0
  61. package/config/themes/dracula.yaml +14 -0
  62. package/config/themes/gruvbox-dark.yaml +14 -0
  63. package/config/themes/nord.yaml +14 -0
  64. package/config/themes/one-dark.yaml +14 -0
  65. package/config/themes/solarized-dark.yaml +14 -0
  66. package/config/themes/tokyo-night.yaml +14 -0
  67. package/control_plane/__init__.py +2 -0
  68. package/control_plane/openapi.yaml +109 -0
  69. package/control_plane/server.py +107 -0
  70. package/control_plane/service.py +148 -0
  71. package/crates/omg-natives/Cargo.toml +17 -0
  72. package/crates/omg-natives/src/clipboard.rs +5 -0
  73. package/crates/omg-natives/src/glob.rs +15 -0
  74. package/crates/omg-natives/src/grep.rs +15 -0
  75. package/crates/omg-natives/src/highlight.rs +15 -0
  76. package/crates/omg-natives/src/html.rs +14 -0
  77. package/crates/omg-natives/src/image.rs +5 -0
  78. package/crates/omg-natives/src/keys.rs +5 -0
  79. package/crates/omg-natives/src/lib.rs +36 -0
  80. package/crates/omg-natives/src/prof.rs +5 -0
  81. package/crates/omg-natives/src/ps.rs +5 -0
  82. package/crates/omg-natives/src/shell.rs +5 -0
  83. package/crates/omg-natives/src/task.rs +5 -0
  84. package/crates/omg-natives/src/text.rs +14 -0
  85. package/hooks/_agent_registry.py +421 -0
  86. package/hooks/_budget.py +31 -0
  87. package/hooks/_common.py +476 -0
  88. package/hooks/_learnings.py +126 -0
  89. package/hooks/_memory.py +103 -0
  90. package/hooks/circuit-breaker.py +270 -0
  91. package/hooks/config-guard.py +163 -0
  92. package/hooks/context_pressure.py +53 -0
  93. package/hooks/credential_store.py +801 -0
  94. package/hooks/fetch-rate-limits.py +212 -0
  95. package/hooks/firewall.py +48 -0
  96. package/hooks/hashline-formatter-bridge.py +224 -0
  97. package/hooks/hashline-injector.py +273 -0
  98. package/hooks/hashline-validator.py +216 -0
  99. package/hooks/idle-detector.py +95 -0
  100. package/hooks/intentgate-keyword-detector.py +188 -0
  101. package/hooks/magic-keyword-router.py +195 -0
  102. package/hooks/policy_engine.py +310 -0
  103. package/hooks/post-tool-failure.py +19 -0
  104. package/hooks/post-write.py +199 -0
  105. package/hooks/pre-compact.py +204 -0
  106. package/hooks/pre-tool-inject.py +98 -0
  107. package/hooks/prompt-enhancer.py +672 -0
  108. package/hooks/quality-runner.py +191 -0
  109. package/hooks/secret-guard.py +47 -0
  110. package/hooks/session-end-capture.py +137 -0
  111. package/hooks/session-start.py +275 -0
  112. package/hooks/shadow_manager.py +297 -0
  113. package/hooks/state_migration.py +209 -0
  114. package/hooks/stop-gate.py +7 -0
  115. package/hooks/stop_dispatcher.py +929 -0
  116. package/hooks/test-validator.py +138 -0
  117. package/hooks/todo-state-tracker.py +114 -0
  118. package/hooks/tool-ledger.py +126 -0
  119. package/hooks/trust_review.py +524 -0
  120. package/install.sh +9 -0
  121. package/omg_natives/__init__.py +186 -0
  122. package/omg_natives/_bindings.py +165 -0
  123. package/omg_natives/clipboard.py +36 -0
  124. package/omg_natives/glob.py +42 -0
  125. package/omg_natives/grep.py +61 -0
  126. package/omg_natives/highlight.py +54 -0
  127. package/omg_natives/html.py +157 -0
  128. package/omg_natives/image.py +51 -0
  129. package/omg_natives/keys.py +46 -0
  130. package/omg_natives/prof.py +39 -0
  131. package/omg_natives/ps.py +93 -0
  132. package/omg_natives/shell.py +58 -0
  133. package/omg_natives/task.py +41 -0
  134. package/omg_natives/text.py +50 -0
  135. package/package.json +26 -0
  136. package/plugins/README.md +82 -0
  137. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  138. package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
  139. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  140. package/plugins/advanced/commands/OMG:learn.md +110 -0
  141. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  142. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  143. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  144. package/plugins/advanced/commands/OMG:security-review.md +119 -0
  145. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  146. package/plugins/advanced/commands/OMG:ship.md +46 -0
  147. package/plugins/advanced/plugin.json +96 -0
  148. package/plugins/core/plugin.json +82 -0
  149. package/pytest.ini +5 -0
  150. package/registry/__init__.py +1 -0
  151. package/registry/verify_artifact.py +90 -0
  152. package/rules/contextual/architect-mode.md +9 -0
  153. package/rules/contextual/big-picture.md +20 -0
  154. package/rules/contextual/code-hygiene.md +26 -0
  155. package/rules/contextual/context-management.md +19 -0
  156. package/rules/contextual/context-minimization.md +32 -0
  157. package/rules/contextual/ddd-sdd.md +28 -0
  158. package/rules/contextual/dependency-safety.md +16 -0
  159. package/rules/contextual/doc-check.md +13 -0
  160. package/rules/contextual/implement-mode.md +9 -0
  161. package/rules/contextual/infra-safety.md +14 -0
  162. package/rules/contextual/outside-in.md +13 -0
  163. package/rules/contextual/persistent-mode.md +24 -0
  164. package/rules/contextual/research-mode.md +9 -0
  165. package/rules/contextual/security-domains.md +25 -0
  166. package/rules/contextual/vision-detection.md +27 -0
  167. package/rules/contextual/web-search.md +25 -0
  168. package/rules/contextual/write-verify.md +23 -0
  169. package/rules/core/00-truth.md +20 -0
  170. package/rules/core/01-surgical.md +19 -0
  171. package/rules/core/02-circuit-breaker.md +22 -0
  172. package/rules/core/03-ensemble.md +28 -0
  173. package/rules/core/04-testing.md +30 -0
  174. package/runtime/__init__.py +32 -0
  175. package/runtime/adapters/__init__.py +13 -0
  176. package/runtime/adapters/claude.py +60 -0
  177. package/runtime/adapters/gpt.py +53 -0
  178. package/runtime/adapters/local.py +53 -0
  179. package/runtime/business_workflow.py +220 -0
  180. package/runtime/compat.py +1299 -0
  181. package/runtime/custom_agent_loader.py +366 -0
  182. package/runtime/dispatcher.py +47 -0
  183. package/runtime/ecosystem.py +371 -0
  184. package/runtime/legacy_compat.py +7 -0
  185. package/runtime/omc_compat.py +7 -0
  186. package/runtime/omc_contract_snapshot.json +916 -0
  187. package/runtime/omg_compat_contract_snapshot.json +916 -0
  188. package/runtime/subagent_dispatcher.py +362 -0
  189. package/runtime/team_router.py +838 -0
  190. package/scripts/check-omc-contract-snapshot.py +12 -0
  191. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  192. package/scripts/check-omg-standalone-clean.py +102 -0
  193. package/scripts/legacy_to_omg_migrate.py +29 -0
  194. package/scripts/migrate-omc.py +464 -0
  195. package/scripts/omc_to_omg_migrate.py +12 -0
  196. package/scripts/omg.py +493 -0
  197. package/scripts/settings-merge.py +224 -0
  198. package/scripts/verify-no-omc.sh +5 -0
  199. package/scripts/verify-standalone.sh +21 -0
  200. package/templates/idea.yml +30 -0
  201. package/templates/policy.yaml +15 -0
  202. package/templates/profile.yaml +25 -0
  203. package/templates/runtime.yaml +12 -0
  204. package/templates/working-memory.md +17 -0
  205. package/tools/__init__.py +2 -0
  206. package/tools/browser_consent.py +289 -0
  207. package/tools/browser_stealth.py +481 -0
  208. package/tools/browser_tool.py +448 -0
  209. package/tools/changelog_generator.py +268 -0
  210. package/tools/commit_splitter.py +361 -0
  211. package/tools/config_discovery.py +151 -0
  212. package/tools/config_merger.py +449 -0
  213. package/tools/git_inspector.py +298 -0
  214. package/tools/lsp_client.py +275 -0
  215. package/tools/lsp_discovery.py +231 -0
  216. package/tools/lsp_operations.py +392 -0
  217. package/tools/python_repl.py +656 -0
  218. package/tools/python_sandbox.py +609 -0
  219. package/tools/search_providers/__init__.py +77 -0
  220. package/tools/search_providers/brave.py +115 -0
  221. package/tools/search_providers/exa.py +116 -0
  222. package/tools/search_providers/jina.py +104 -0
  223. package/tools/search_providers/perplexity.py +139 -0
  224. package/tools/search_providers/synthetic.py +74 -0
  225. package/tools/session_snapshot.py +736 -0
  226. package/tools/ssh_manager.py +912 -0
  227. package/tools/theme_engine.py +294 -0
  228. package/tools/theme_selector.py +137 -0
  229. 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)