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