@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,138 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stop Hook: Test Validator (v4) — User-Journey Focused
4
+ Catches not just fake tests, but MEANINGLESS tests.
5
+
6
+ v4 additions:
7
+ - Detects "boilerplate-only" test files (only testing existence/types)
8
+ - Checks if tests align with user stories / working-memory goals
9
+ - Warns when tests only cover happy path
10
+
11
+ Callable API:
12
+ check_test_quality(data, project_dir) -> list[str]
13
+ Returns list of block reasons (empty = pass).
14
+ """
15
+ import json, sys, os, re
16
+
17
+ HOOKS_DIR = os.path.dirname(__file__)
18
+ if HOOKS_DIR not in sys.path:
19
+ sys.path.insert(0, HOOKS_DIR)
20
+
21
+ from _common import _resolve_project_dir, should_skip_stop_hooks
22
+
23
+
24
+ def check_test_quality(data, project_dir):
25
+ """Core test-quality validation. Returns list of block-reason strings."""
26
+ import subprocess
27
+
28
+ # Find recently modified test files
29
+ test_files = []
30
+ try:
31
+ result = subprocess.run(
32
+ ["git", "diff", "--name-only", "--diff-filter=AM"],
33
+ capture_output=True, text=True, timeout=10, cwd=project_dir
34
+ )
35
+ for f in result.stdout.strip().split("\n"):
36
+ if f and any(p in f.lower() for p in
37
+ [".test.", ".spec.", "_test.", "test_", "__tests__", ".tests."]):
38
+ full = os.path.join(project_dir, f)
39
+ if os.path.exists(full):
40
+ test_files.append(full)
41
+ except Exception:
42
+ pass
43
+
44
+ if not test_files:
45
+ return []
46
+
47
+ warnings = []
48
+
49
+ for tf in test_files:
50
+ try:
51
+ with open(tf, "r", encoding="utf-8", errors="ignore") as f:
52
+ content = f.read()
53
+ except Exception:
54
+ continue
55
+
56
+ filename = os.path.basename(tf)
57
+ issues = []
58
+
59
+ # === FAKE TEST PATTERNS (from v3, kept) ===
60
+ fake_patterns = [
61
+ (r"expect\s*\(\s*true\s*\)\s*\.to(Be|Equal)\s*\(\s*true\s*\)", "assert true === true"),
62
+ (r"expect\s*\(\s*1\s*\)\s*\.toBe\s*\(\s*1\s*\)", "assert 1 === 1"),
63
+ (r"assert\s+True\b", "assert True (Python)"),
64
+ (r"assert\s+1\s*==\s*1", "assert 1 == 1"),
65
+ ]
66
+ for pat, label in fake_patterns:
67
+ if re.search(pat, content):
68
+ issues.append(f"FAKE: {label}")
69
+
70
+ # === BOILERPLATE-ONLY (v4 new) ===
71
+ # Tests that only check typeof/instanceof/existence
72
+ type_checks = len(re.findall(
73
+ r"(typeof\s+\w+|instanceof\s+\w+|toBeDefined|toBeInstanceOf|\.type\b)", content))
74
+ behavior_checks = len(re.findall(
75
+ r"(toEqual|toContain|toMatch|toThrow|rejects|resolves|toHaveBeenCalledWith|"
76
+ r"toHaveProperty|toHaveLength|toBeGreaterThan|toBeLessThan|assert.*==|"
77
+ r"assertEqual|assertIn|assertRaises|assert_called_with)", content))
78
+
79
+ if type_checks > 3 and behavior_checks == 0:
80
+ issues.append("BOILERPLATE: Only checks types/existence, never tests actual behavior")
81
+
82
+ # === HAPPY PATH ONLY (v4 new) ===
83
+ # Check for error/edge case testing
84
+ has_error_tests = bool(re.search(
85
+ r"(toThrow|rejects|assertRaises|error|invalid|empty|null|undefined|"
86
+ r"edge.case|boundary|overflow|timeout|unauthorized|forbidden|not.found|"
87
+ r"bad.request|missing|malformed)", content, re.IGNORECASE))
88
+ test_count = len(re.findall(r"(test|it|describe)\s*\(", content))
89
+
90
+ if test_count >= 3 and not has_error_tests:
91
+ issues.append("HAPPY PATH ONLY: No error/edge case tests. "
92
+ "What happens with bad input? Unauthorized? Empty data?")
93
+
94
+ # === NO ASSERTIONS (v3 kept) ===
95
+ test_bodies = re.findall(
96
+ r"(?:test|it)\s*\([^)]+,\s*(?:async\s*)?\(\)\s*=>\s*\{([^}]*)\}",
97
+ content, re.DOTALL)
98
+ for body in test_bodies:
99
+ if body.strip() and not re.search(
100
+ r"(expect|assert|should|verify|check|toBe|toEqual|toThrow|toHave)",
101
+ body, re.IGNORECASE):
102
+ issues.append("EMPTY: Test body has no assertions")
103
+ break
104
+
105
+ # === MOCK EVERYTHING (v3 kept, improved) ===
106
+ mock_count = len(re.findall(r"(jest\.mock|mock\(|patch\(|MagicMock|stub\(|sinon\.stub)", content))
107
+ if mock_count > 5 and behavior_checks <= 1:
108
+ issues.append("OVER-MOCKED: Heavy mocking but barely tests real behavior")
109
+
110
+ if issues:
111
+ warnings.append(f"{filename}: " + "; ".join(issues))
112
+
113
+ if warnings:
114
+ msg = "TEST QUALITY ISSUES:\n" + "\n".join(f" {w}" for w in warnings)
115
+ msg += ("\n\nTests should verify what USERS need, not just that code exists.\n"
116
+ "Ask: 'What does the user expect to happen? What could go wrong?'\n"
117
+ "Write tests for those scenarios.")
118
+ return [msg]
119
+
120
+ return []
121
+
122
+
123
+ # Standalone execution (backward compat: invoked directly by hook runner)
124
+ if __name__ == "__main__":
125
+ try:
126
+ data = json.load(sys.stdin)
127
+ except (json.JSONDecodeError, EOFError):
128
+ sys.exit(0)
129
+
130
+ if should_skip_stop_hooks(data):
131
+ sys.exit(0)
132
+
133
+
134
+ project_dir = _resolve_project_dir()
135
+ blocks = check_test_quality(data, project_dir)
136
+ if blocks:
137
+ json.dump({"decision": "block", "reason": blocks[0]}, sys.stdout)
138
+ sys.exit(0)
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostToolUse Hook: Todo State Tracker (v1)
4
+
5
+ Parses todo lists from agent responses and tracks completion status.
6
+ Persists state to .omg/state/todo_progress.json for cross-turn tracking.
7
+
8
+ Feature flag: OMG_TODO_TRACKING_ENABLED (default: False)
9
+ """
10
+ import json
11
+ import sys
12
+ import os
13
+ import re
14
+ from datetime import datetime, timezone
15
+
16
+ HOOKS_DIR = os.path.dirname(__file__)
17
+ if HOOKS_DIR not in sys.path:
18
+ sys.path.insert(0, HOOKS_DIR)
19
+
20
+ from _common import (
21
+ setup_crash_handler,
22
+ json_input,
23
+ get_project_dir,
24
+ get_feature_flag,
25
+ atomic_json_write,
26
+ )
27
+
28
+ setup_crash_handler("todo-state-tracker", fail_closed=False)
29
+
30
+ # Feature flag check
31
+ if not get_feature_flag("TODO_TRACKING", default=False):
32
+ sys.exit(0)
33
+
34
+ data = json_input()
35
+
36
+ # Extract response text from various possible fields
37
+ response_text = ""
38
+ if isinstance(data, dict):
39
+ # PostToolUse hook may have response in different fields
40
+ response_text = (
41
+ data.get("response", "")
42
+ or data.get("tool_response", "")
43
+ or data.get("message", "")
44
+ or ""
45
+ )
46
+ if isinstance(response_text, dict):
47
+ response_text = response_text.get("content", "")
48
+
49
+ if not isinstance(response_text, str):
50
+ response_text = str(response_text) if response_text else ""
51
+
52
+ # Parse todo items: regex pattern for markdown todo format
53
+ # Matches: - [ ] task text or - [x] task text
54
+ TODO_PATTERN = r'- \[([ x])\] (.+)'
55
+ matches = re.findall(TODO_PATTERN, response_text, re.IGNORECASE)
56
+
57
+ if not matches:
58
+ # No todos found, exit cleanly
59
+ sys.exit(0)
60
+
61
+ # Separate incomplete and complete items
62
+ incomplete_items = []
63
+ complete_items = []
64
+
65
+ for status, task_text in matches:
66
+ task_text = task_text.strip()
67
+ if status.lower() == 'x':
68
+ complete_items.append(task_text)
69
+ else:
70
+ incomplete_items.append(task_text)
71
+
72
+ # Load existing state
73
+ project_dir = get_project_dir()
74
+ state_path = os.path.join(project_dir, ".omg", "state", "todo_progress.json")
75
+
76
+ existing_state = {}
77
+ if os.path.exists(state_path):
78
+ try:
79
+ with open(state_path, "r", encoding="utf-8") as f:
80
+ existing_state = json.load(f)
81
+ except Exception:
82
+ existing_state = {}
83
+
84
+ # Ensure existing_state is a dict
85
+ if not isinstance(existing_state, dict):
86
+ existing_state = {}
87
+
88
+ # Cross-turn merge strategy:
89
+ # - Keep existing complete items (don't regress)
90
+ # - Add new complete items
91
+ # - Update incomplete items (replace with current turn's findings)
92
+ # - Preserve session_id if available
93
+
94
+ merged_complete = list(set(existing_state.get("complete", []) + complete_items))
95
+ merged_incomplete = incomplete_items # Replace with current turn's findings
96
+
97
+ # Build new state
98
+ new_state = {
99
+ "incomplete": merged_incomplete,
100
+ "complete": merged_complete,
101
+ "total": len(merged_incomplete) + len(merged_complete),
102
+ "last_updated": datetime.now(timezone.utc).isoformat(),
103
+ }
104
+
105
+ # Preserve session_id if available
106
+ if "session_id" in existing_state:
107
+ new_state["session_id"] = existing_state["session_id"]
108
+ elif "session_id" in data:
109
+ new_state["session_id"] = data.get("session_id")
110
+
111
+ # Atomically write state
112
+ atomic_json_write(state_path, new_state)
113
+
114
+ sys.exit(0)
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostToolUse Hook (*): Tool Execution Ledger (Enterprise)
4
+ Logs every tool execution to .omg/state/ledger/tool-ledger.jsonl.
5
+ Evidence trail for stop-gate.py and claim verification.
6
+ Includes log rotation to prevent unbounded growth.
7
+ """
8
+ import json, sys, os, re, shutil
9
+ from datetime import datetime, timezone
10
+
11
+ HOOKS_DIR = os.path.dirname(__file__)
12
+ if HOOKS_DIR not in sys.path:
13
+ sys.path.insert(0, HOOKS_DIR)
14
+
15
+ from _common import setup_crash_handler, json_input, get_project_dir
16
+ from state_migration import resolve_state_dir
17
+
18
+ setup_crash_handler("tool-ledger", fail_closed=False)
19
+
20
+ data = json_input()
21
+
22
+ project_dir = get_project_dir()
23
+ ledger_dir = resolve_state_dir(project_dir, "state/ledger", "ledger")
24
+ os.makedirs(ledger_dir, exist_ok=True)
25
+
26
+ ledger_path = os.path.join(ledger_dir, "tool-ledger.jsonl")
27
+
28
+ # ── Log rotation: size-only heuristic (avoids O(n) line-count scan) ──
29
+ MAX_BYTES = 5 * 1024 * 1024 # 5MB
30
+ try:
31
+ if os.path.exists(ledger_path):
32
+ size = os.path.getsize(ledger_path)
33
+ needs_rotation = size > MAX_BYTES
34
+
35
+ if needs_rotation:
36
+ archive = ledger_path + ".1"
37
+ # Keep only one archive
38
+ if os.path.exists(archive):
39
+ try:
40
+ os.remove(archive)
41
+ except OSError:
42
+ pass
43
+ shutil.move(ledger_path, archive)
44
+ except Exception:
45
+ pass
46
+
47
+ tool_name = data.get("tool_name", "")
48
+ tool_input = data.get("tool_input", {})
49
+ tool_response = data.get("tool_response", {})
50
+
51
+ entry = {
52
+ "ts": datetime.now(timezone.utc).isoformat(),
53
+ "pid": os.getpid(),
54
+ "tool": tool_name,
55
+ }
56
+
57
+ # Link ledger entries to OMG v1 run/evidence artifacts when available.
58
+ run_id = os.environ.get("OMG_RUN_ID")
59
+ if not run_id:
60
+ active_run = os.path.join(project_dir, ".omg", "shadow", "active-run")
61
+ if os.path.exists(active_run):
62
+ try:
63
+ with open(active_run, "r", encoding="utf-8") as f:
64
+ run_id = f.read().strip()
65
+ except Exception:
66
+ run_id = None
67
+ if run_id:
68
+ entry["run_id"] = run_id
69
+
70
+ if tool_name == "Bash":
71
+ entry["command"] = tool_input.get("command", "")[:500]
72
+ if isinstance(tool_response, dict):
73
+ entry["exit_code"] = tool_response.get("exitCode", tool_response.get("exit_code"))
74
+ snippet = str(tool_response.get("stdout", ""))[:200]
75
+ # Mask potential secrets in stdout before logging
76
+ # Aligned with post-write.py SECRET_PATTERNS for consistent coverage
77
+ SECRET_PATTERNS = [
78
+ (r'(?i)(api[_-]?key|token|secret|password|passwd|credential|auth)[=:]\s*\S+', r'\1=***'),
79
+ (r'AKIA[0-9A-Z]{16}', '***AWS_KEY***'),
80
+ (r'(?:aws_secret_access_key|AWS_SECRET)\s*[:=]\s*[\'"]?[A-Za-z0-9/+=]{40}', '***AWS_SECRET***'),
81
+ (r'-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----', '***PRIVATE_KEY***'),
82
+ (r'sk-[a-zA-Z0-9]{20,}', '***API_KEY***'),
83
+ (r'gh[ps]_[A-Za-z0-9_]{36,}', '***GH_TOKEN***'),
84
+ (r'github_pat_[A-Za-z0-9_]{22,}', '***GH_PAT***'),
85
+ (r'xox[bp]-[0-9]{10,}-[A-Za-z0-9]{20,}', '***SLACK_TOKEN***'),
86
+ (r'sk_live_[A-Za-z0-9]{20,}', '***STRIPE_KEY***'),
87
+ (r'rk_live_[A-Za-z0-9]{20,}', '***STRIPE_KEY***'),
88
+ (r'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{20,}', '***SERVICE_KEY***'),
89
+ (r'AIza[A-Za-z0-9_-]{35}', '***GOOGLE_KEY***'),
90
+ (r'SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}', '***SENDGRID_KEY***'),
91
+ (r'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}', '***JWT***'),
92
+ (r'(?:postgres|mysql|mongodb|redis)://[^:]+:[^@]+@', '***DB_URL***'),
93
+ (r'https?://[^:]+:[^@]+@', '***URL_CREDS***'),
94
+ ]
95
+ for pattern, replacement in SECRET_PATTERNS:
96
+ snippet = re.sub(pattern, replacement, snippet)
97
+ entry["stdout_snippet"] = snippet
98
+ elif tool_name in ("Write", "Edit", "MultiEdit"):
99
+ entry["file"] = tool_input.get("file_path", "")
100
+ entry["success"] = tool_response.get("success") if isinstance(tool_response, dict) else None
101
+ elif tool_name == "Read":
102
+ entry["file"] = tool_input.get("file_path", "")
103
+
104
+ # Attach the latest evidence file path if one exists for this run.
105
+ if run_id:
106
+ ev_path = os.path.join(project_dir, ".omg", "evidence", f"{run_id}.json")
107
+ if os.path.exists(ev_path):
108
+ entry["evidence_path"] = os.path.relpath(ev_path, project_dir)
109
+
110
+ try:
111
+ import fcntl
112
+ fd = open(ledger_path, "a")
113
+ fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
114
+ fd.write(json.dumps(entry, separators=(",", ":")) + "\n")
115
+ fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
116
+ fd.close()
117
+ except (ImportError, BlockingIOError):
118
+ try:
119
+ with open(ledger_path, "a") as f:
120
+ f.write(json.dumps(entry, separators=(",", ":")) + "\n")
121
+ except Exception:
122
+ pass
123
+ except Exception:
124
+ pass # Non-blocking: don't fail the tool call
125
+
126
+ sys.exit(0)