@trac3er/oh-my-god 2.0.0 → 2.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 +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -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 +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -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/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- 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/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -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/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- 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 +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +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
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tiered token estimation helpers for OMG hooks."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import importlib
|
|
8
|
+
import urllib.request
|
|
9
|
+
from collections.abc import Iterable
|
|
10
|
+
|
|
11
|
+
API_URL = "https://api.anthropic.com/v1/messages/count_tokens"
|
|
12
|
+
API_MODEL = "claude-3-5-haiku-20241022"
|
|
13
|
+
|
|
14
|
+
_FEATURE_UI_DISPLAY = "ui_display"
|
|
15
|
+
_FEATURE_BUDGET_ENFORCEMENT = "budget_enforcement"
|
|
16
|
+
_FEATURE_PREFLIGHT = "preflight"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _safe_int(value: float) -> int:
|
|
20
|
+
if value <= 0:
|
|
21
|
+
return 0
|
|
22
|
+
return int(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _extract_features(text: str) -> tuple[int, int, int]:
|
|
26
|
+
encoded = text.encode("utf-8")
|
|
27
|
+
byte_count = len(encoded)
|
|
28
|
+
word_count = len(text.split())
|
|
29
|
+
line_count = text.count("\n") + (1 if text else 0)
|
|
30
|
+
return byte_count, word_count, line_count
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_coefficients() -> tuple[float, float, float, float]:
|
|
34
|
+
return (1.0, 0.19, 0.75, 1.1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _gaussian_solve(matrix: list[list[float]], vector: list[float]) -> list[float] | None:
|
|
38
|
+
n = len(vector)
|
|
39
|
+
if n == 0:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
for i in range(n):
|
|
43
|
+
pivot = i
|
|
44
|
+
for r in range(i + 1, n):
|
|
45
|
+
if abs(matrix[r][i]) > abs(matrix[pivot][i]):
|
|
46
|
+
pivot = r
|
|
47
|
+
if abs(matrix[pivot][i]) < 1e-12:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
if pivot != i:
|
|
51
|
+
matrix[i], matrix[pivot] = matrix[pivot], matrix[i]
|
|
52
|
+
vector[i], vector[pivot] = vector[pivot], vector[i]
|
|
53
|
+
|
|
54
|
+
pivot_val = matrix[i][i]
|
|
55
|
+
for c in range(i, n):
|
|
56
|
+
matrix[i][c] /= pivot_val
|
|
57
|
+
vector[i] /= pivot_val
|
|
58
|
+
|
|
59
|
+
for r in range(n):
|
|
60
|
+
if r == i:
|
|
61
|
+
continue
|
|
62
|
+
factor = matrix[r][i]
|
|
63
|
+
if factor == 0:
|
|
64
|
+
continue
|
|
65
|
+
for c in range(i, n):
|
|
66
|
+
matrix[r][c] -= factor * matrix[i][c]
|
|
67
|
+
vector[r] -= factor * vector[i]
|
|
68
|
+
return vector
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _fit_linear_coefficients(samples: Iterable[tuple[str, int]]) -> tuple[float, float, float, float]:
|
|
74
|
+
rows: list[list[float]] = []
|
|
75
|
+
targets: list[float] = []
|
|
76
|
+
for text, tokens in samples:
|
|
77
|
+
bcount, wcount, lcount = _extract_features(text)
|
|
78
|
+
rows.append([1.0, float(bcount), float(wcount), float(lcount)])
|
|
79
|
+
targets.append(float(tokens))
|
|
80
|
+
|
|
81
|
+
dim = 4
|
|
82
|
+
if len(rows) < dim:
|
|
83
|
+
return _default_coefficients()
|
|
84
|
+
|
|
85
|
+
xtx = [[0.0 for _ in range(dim)] for _ in range(dim)]
|
|
86
|
+
xty = [0.0 for _ in range(dim)]
|
|
87
|
+
for row, target in zip(rows, targets):
|
|
88
|
+
for i in range(dim):
|
|
89
|
+
xty[i] += row[i] * target
|
|
90
|
+
for j in range(dim):
|
|
91
|
+
xtx[i][j] += row[i] * row[j]
|
|
92
|
+
|
|
93
|
+
solved = _gaussian_solve(xtx, xty)
|
|
94
|
+
if solved is None:
|
|
95
|
+
return _default_coefficients()
|
|
96
|
+
|
|
97
|
+
return (float(solved[0]), float(solved[1]), float(solved[2]), float(solved[3]))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_CALIBRATION_SAMPLES: tuple[tuple[str, int], ...] = (
|
|
101
|
+
("ls", 5),
|
|
102
|
+
("git status", 7),
|
|
103
|
+
("echo 'hello world'", 10),
|
|
104
|
+
("python3 -m pytest tests/hooks/test_feature_flags_v2.py -q", 18),
|
|
105
|
+
("def hello():\n return 'world'\n", 24),
|
|
106
|
+
(
|
|
107
|
+
"""from pathlib import Path\nfor path in Path('hooks').glob('*.py'):\n print(path.name)\n""",
|
|
108
|
+
44,
|
|
109
|
+
),
|
|
110
|
+
(
|
|
111
|
+
"""def estimate_tokens(text: str, tier: int = 1) -> int:\n if tier == 1:\n return max(1, int(len(text) / 3.5))\n return 0\n""",
|
|
112
|
+
80,
|
|
113
|
+
),
|
|
114
|
+
(
|
|
115
|
+
"\n".join(["line with common code and comments" for _ in range(80)]),
|
|
116
|
+
720,
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"\n".join(["longer source line with punctuation () {} [] == != <= >=" for _ in range(250)]),
|
|
120
|
+
1800,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
_COEFFICIENTS = _fit_linear_coefficients(_CALIBRATION_SAMPLES)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _estimate_tier1(text: str) -> int:
|
|
128
|
+
if not text:
|
|
129
|
+
return 0
|
|
130
|
+
return max(1, int(len(text) / 3.5))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _estimate_tier2(text: str) -> int:
|
|
134
|
+
if not text:
|
|
135
|
+
return 0
|
|
136
|
+
bias, w_bytes, w_words, w_lines = _COEFFICIENTS
|
|
137
|
+
bcount, wcount, lcount = _extract_features(text)
|
|
138
|
+
prediction = bias + (w_bytes * bcount) + (w_words * wcount) + (w_lines * lcount)
|
|
139
|
+
return max(1, _safe_int(prediction))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_anthropic_api_key() -> str | None:
|
|
143
|
+
try:
|
|
144
|
+
store_mod = importlib.import_module("credential_store")
|
|
145
|
+
key = store_mod.get_active_key("anthropic")
|
|
146
|
+
if key:
|
|
147
|
+
return key
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
return os.environ.get("ANTHROPIC_API_KEY")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _estimate_tier3(text: str) -> int:
|
|
154
|
+
if not text:
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
api_key = _get_anthropic_api_key()
|
|
158
|
+
if not api_key:
|
|
159
|
+
return _estimate_tier2(text)
|
|
160
|
+
|
|
161
|
+
payload = {
|
|
162
|
+
"model": API_MODEL,
|
|
163
|
+
"messages": [{"role": "user", "content": text}],
|
|
164
|
+
}
|
|
165
|
+
body = json.dumps(payload).encode("utf-8")
|
|
166
|
+
request = urllib.request.Request(
|
|
167
|
+
API_URL,
|
|
168
|
+
data=body,
|
|
169
|
+
method="POST",
|
|
170
|
+
headers={
|
|
171
|
+
"content-type": "application/json",
|
|
172
|
+
"x-api-key": api_key,
|
|
173
|
+
"anthropic-version": "2023-06-01",
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
with urllib.request.urlopen(request, timeout=8) as response:
|
|
179
|
+
raw = response.read().decode("utf-8")
|
|
180
|
+
parsed = json.loads(raw)
|
|
181
|
+
token_value = parsed.get("input_tokens")
|
|
182
|
+
if isinstance(token_value, int) and token_value >= 0:
|
|
183
|
+
return token_value
|
|
184
|
+
except Exception:
|
|
185
|
+
return _estimate_tier2(text)
|
|
186
|
+
|
|
187
|
+
return _estimate_tier2(text)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def auto_select_tier(operation: str, text: str = "") -> int:
|
|
191
|
+
normalized = (operation or "").strip().lower()
|
|
192
|
+
if normalized == _FEATURE_UI_DISPLAY:
|
|
193
|
+
return 1
|
|
194
|
+
if normalized == _FEATURE_BUDGET_ENFORCEMENT:
|
|
195
|
+
return 2
|
|
196
|
+
if normalized == _FEATURE_PREFLIGHT:
|
|
197
|
+
if len(text) >= 8000:
|
|
198
|
+
return 3
|
|
199
|
+
if _estimate_tier2(text) >= 1000:
|
|
200
|
+
return 3
|
|
201
|
+
return 2
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def estimate_tokens(text: str, tier: int = 1) -> int:
|
|
206
|
+
"""Estimate token count with 3 reliability/cost tiers.
|
|
207
|
+
|
|
208
|
+
Tier 1: fast heuristic (`len(text)/3.5`).
|
|
209
|
+
Tier 2: calibrated linear model using bytes, words, lines.
|
|
210
|
+
Tier 3: Anthropic count_tokens API with graceful fallback to tier 2.
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
if tier == 1:
|
|
214
|
+
return _estimate_tier1(text)
|
|
215
|
+
if tier == 2:
|
|
216
|
+
return _estimate_tier2(text)
|
|
217
|
+
if tier == 3:
|
|
218
|
+
return _estimate_tier3(text)
|
|
219
|
+
return _estimate_tier1(text)
|
|
220
|
+
except Exception:
|
|
221
|
+
return _estimate_tier1(text)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart Hook — Smart Branch Manager.
|
|
3
|
+
|
|
4
|
+
Auto-creates a feature branch when on main/master/develop.
|
|
5
|
+
Extracts task description from OMG state files for branch naming.
|
|
6
|
+
|
|
7
|
+
Feature-gated: OMG_GIT_WORKFLOW_ENABLED (uses get_feature_flag('GIT_WORKFLOW'))
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime
|
|
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 setup_crash_handler, json_input, get_feature_flag
|
|
21
|
+
|
|
22
|
+
setup_crash_handler("branch-manager", fail_closed=False)
|
|
23
|
+
|
|
24
|
+
# Default branches that trigger feature branch creation
|
|
25
|
+
DEFAULT_BRANCHES = frozenset({"main", "master", "develop"})
|
|
26
|
+
|
|
27
|
+
# Max length for the descriptive part of branch name
|
|
28
|
+
MAX_BRANCH_NAME_LEN = 50
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_project_dir() -> str:
|
|
32
|
+
"""Get project directory from env or cwd."""
|
|
33
|
+
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _has_git(project_dir: str) -> bool:
|
|
37
|
+
"""Check if project_dir is inside a git repo."""
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
["git", "-C", project_dir, "rev-parse", "--git-dir"],
|
|
41
|
+
capture_output=True, text=True, timeout=5,
|
|
42
|
+
)
|
|
43
|
+
return result.returncode == 0
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _current_branch(project_dir: str) -> str | None:
|
|
49
|
+
"""Get current branch name. Returns None on failure."""
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "-C", project_dir, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
53
|
+
capture_output=True, text=True, timeout=5,
|
|
54
|
+
)
|
|
55
|
+
if result.returncode == 0:
|
|
56
|
+
return result.stdout.strip()
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sanitize_branch_name(description: str) -> str:
|
|
63
|
+
"""Sanitize a description into a valid git branch name segment.
|
|
64
|
+
|
|
65
|
+
Rules:
|
|
66
|
+
- Lowercase
|
|
67
|
+
- Replace spaces/underscores with hyphens
|
|
68
|
+
- Strip special characters (keep alphanumeric and hyphens)
|
|
69
|
+
- Collapse consecutive hyphens
|
|
70
|
+
- Strip leading/trailing hyphens
|
|
71
|
+
- Max MAX_BRANCH_NAME_LEN chars
|
|
72
|
+
"""
|
|
73
|
+
name = description.lower().strip()
|
|
74
|
+
# Replace spaces and underscores with hyphens
|
|
75
|
+
name = re.sub(r"[\s_]+", "-", name)
|
|
76
|
+
# Remove everything except alphanumeric and hyphens
|
|
77
|
+
name = re.sub(r"[^a-z0-9-]", "", name)
|
|
78
|
+
# Collapse consecutive hyphens
|
|
79
|
+
name = re.sub(r"-{2,}", "-", name)
|
|
80
|
+
# Strip leading/trailing hyphens
|
|
81
|
+
name = name.strip("-")
|
|
82
|
+
# Truncate to max length, but don't cut mid-word if possible
|
|
83
|
+
if len(name) > MAX_BRANCH_NAME_LEN:
|
|
84
|
+
truncated = name[:MAX_BRANCH_NAME_LEN]
|
|
85
|
+
# Try to cut at last hyphen to avoid mid-word truncation
|
|
86
|
+
last_hyphen = truncated.rfind("-")
|
|
87
|
+
if last_hyphen > 20:
|
|
88
|
+
truncated = truncated[:last_hyphen]
|
|
89
|
+
name = truncated.rstrip("-")
|
|
90
|
+
return name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _extract_task_description(project_dir: str) -> str | None:
|
|
94
|
+
"""Extract task description from OMG state files.
|
|
95
|
+
|
|
96
|
+
Priority order:
|
|
97
|
+
(a) .omg/state/_plan.md title (first # heading)
|
|
98
|
+
(b) .omg/state/_checklist.md first item
|
|
99
|
+
(c) .omg/state/working-memory.md last entry
|
|
100
|
+
(d) fallback: None (caller uses session-{timestamp})
|
|
101
|
+
"""
|
|
102
|
+
state_dir = os.path.join(project_dir, ".omg", "state")
|
|
103
|
+
|
|
104
|
+
# (a) Plan title
|
|
105
|
+
plan_path = os.path.join(state_dir, "_plan.md")
|
|
106
|
+
if os.path.isfile(plan_path):
|
|
107
|
+
try:
|
|
108
|
+
with open(plan_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
109
|
+
for line in f:
|
|
110
|
+
line = line.strip()
|
|
111
|
+
if line.startswith("# "):
|
|
112
|
+
title = line[2:].strip()
|
|
113
|
+
if title:
|
|
114
|
+
return title
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# (b) Checklist first item
|
|
119
|
+
checklist_path = os.path.join(state_dir, "_checklist.md")
|
|
120
|
+
if os.path.isfile(checklist_path):
|
|
121
|
+
try:
|
|
122
|
+
with open(checklist_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
123
|
+
for line in f:
|
|
124
|
+
line = line.strip()
|
|
125
|
+
# Match markdown checkbox items: - [ ] or - [x]
|
|
126
|
+
m = re.match(r"^-\s*\[.\]\s*(.+)$", line)
|
|
127
|
+
if m:
|
|
128
|
+
item = m.group(1).strip()
|
|
129
|
+
if item:
|
|
130
|
+
return item
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
# (c) Working memory last entry
|
|
135
|
+
wm_path = os.path.join(state_dir, "working-memory.md")
|
|
136
|
+
if os.path.isfile(wm_path):
|
|
137
|
+
try:
|
|
138
|
+
with open(wm_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
139
|
+
content = f.read()
|
|
140
|
+
# Split by ## headings, take last entry's content
|
|
141
|
+
sections = re.split(r"\n## ", content)
|
|
142
|
+
if len(sections) > 1:
|
|
143
|
+
# Last section: first line is heading, rest is content
|
|
144
|
+
last_lines = sections[-1].split("\n")
|
|
145
|
+
# Find first non-empty content line after heading
|
|
146
|
+
for line in last_lines[1:]:
|
|
147
|
+
line = line.strip()
|
|
148
|
+
if line:
|
|
149
|
+
return line
|
|
150
|
+
# Fallback to heading if no content
|
|
151
|
+
heading = last_lines[0].strip()
|
|
152
|
+
if heading:
|
|
153
|
+
return heading
|
|
154
|
+
elif sections:
|
|
155
|
+
# Single section — try first non-empty non-heading line
|
|
156
|
+
for line in sections[0].split("\n"):
|
|
157
|
+
line = line.strip()
|
|
158
|
+
if line and not line.startswith("#"):
|
|
159
|
+
return line
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
# (d) No state files found
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _create_branch(project_dir: str, branch_name: str) -> bool:
|
|
168
|
+
"""Create and checkout a new branch. Returns True on success."""
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
["git", "-C", project_dir, "checkout", "-b", branch_name],
|
|
172
|
+
capture_output=True, text=True, timeout=10,
|
|
173
|
+
)
|
|
174
|
+
return result.returncode == 0
|
|
175
|
+
except Exception:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main() -> None:
|
|
180
|
+
"""Main hook entry point."""
|
|
181
|
+
data = json_input()
|
|
182
|
+
|
|
183
|
+
# Feature gate: exit silently if disabled
|
|
184
|
+
if not get_feature_flag("GIT_WORKFLOW", default=False):
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
|
|
187
|
+
project_dir = _get_project_dir()
|
|
188
|
+
|
|
189
|
+
# No-op if not a git repo
|
|
190
|
+
if not _has_git(project_dir):
|
|
191
|
+
sys.exit(0)
|
|
192
|
+
|
|
193
|
+
# Get current branch
|
|
194
|
+
branch = _current_branch(project_dir)
|
|
195
|
+
if branch is None:
|
|
196
|
+
sys.exit(0)
|
|
197
|
+
|
|
198
|
+
# No-op if already on a non-default branch (feature branch, etc.)
|
|
199
|
+
if branch not in DEFAULT_BRANCHES:
|
|
200
|
+
sys.exit(0)
|
|
201
|
+
|
|
202
|
+
# Extract task description and build branch name
|
|
203
|
+
description = _extract_task_description(project_dir)
|
|
204
|
+
if description:
|
|
205
|
+
sanitized = _sanitize_branch_name(description)
|
|
206
|
+
else:
|
|
207
|
+
sanitized = ""
|
|
208
|
+
|
|
209
|
+
if not sanitized:
|
|
210
|
+
# Fallback: session-{timestamp}
|
|
211
|
+
sanitized = f"session-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
212
|
+
|
|
213
|
+
target_branch = f"feature/{sanitized}"
|
|
214
|
+
|
|
215
|
+
# Dry-run mode: output what would happen without executing
|
|
216
|
+
dry_run = os.environ.get("OMG_GIT_WORKFLOW_DRY_RUN", "").lower() in ("1", "true", "yes")
|
|
217
|
+
if dry_run:
|
|
218
|
+
print(
|
|
219
|
+
f"[OMG branch-manager] DRY-RUN: Would create branch '{target_branch}' from '{branch}'",
|
|
220
|
+
file=sys.stderr,
|
|
221
|
+
)
|
|
222
|
+
sys.exit(0)
|
|
223
|
+
|
|
224
|
+
# Create the feature branch
|
|
225
|
+
success = _create_branch(project_dir, target_branch)
|
|
226
|
+
if success:
|
|
227
|
+
print(
|
|
228
|
+
f"[OMG branch-manager] Created branch '{target_branch}' from '{branch}'",
|
|
229
|
+
file=sys.stderr,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
sys.exit(0)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse budget governor (BATS-style additionalContext injection)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
HOOKS_DIR = os.path.dirname(__file__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_module(module_name: str, filename: str):
|
|
15
|
+
path = os.path.join(HOOKS_DIR, filename)
|
|
16
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
17
|
+
if spec is None or spec.loader is None:
|
|
18
|
+
raise RuntimeError(f"Unable to load module: {filename}")
|
|
19
|
+
module = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(module)
|
|
21
|
+
return module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_common = _load_module("_common", "_common.py")
|
|
25
|
+
_cost_ledger = _load_module("_cost_ledger", "_cost_ledger.py")
|
|
26
|
+
_token_counter = _load_module("_token_counter", "_token_counter.py")
|
|
27
|
+
|
|
28
|
+
get_feature_flag = _common.get_feature_flag
|
|
29
|
+
get_project_dir = _common.get_project_dir
|
|
30
|
+
json_input = _common.json_input
|
|
31
|
+
setup_crash_handler = _common.setup_crash_handler
|
|
32
|
+
read_cost_summary = _cost_ledger.read_cost_summary
|
|
33
|
+
estimate_tokens = _token_counter.estimate_tokens
|
|
34
|
+
|
|
35
|
+
DEFAULT_SESSION_LIMIT_USD = 5.0
|
|
36
|
+
DEFAULT_INPUT_PER_MTOK = 3.0
|
|
37
|
+
DEFAULT_OUTPUT_PER_MTOK = 15.0
|
|
38
|
+
DEFAULT_PROJECTED_TOOL_CALLS = 50
|
|
39
|
+
DEFAULT_THRESHOLDS = [50, 80, 95]
|
|
40
|
+
THRESHOLD_STATE_FILE = ".omg/state/.cost-threshold-state.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _safe_float(value, default: float) -> float:
|
|
44
|
+
try:
|
|
45
|
+
parsed = float(value)
|
|
46
|
+
if parsed <= 0:
|
|
47
|
+
return default
|
|
48
|
+
return parsed
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
return default
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_budget_config(project_dir: str) -> tuple[float, float, float]:
|
|
54
|
+
session_limit = DEFAULT_SESSION_LIMIT_USD
|
|
55
|
+
input_per_mtok = DEFAULT_INPUT_PER_MTOK
|
|
56
|
+
output_per_mtok = DEFAULT_OUTPUT_PER_MTOK
|
|
57
|
+
|
|
58
|
+
settings_path = os.path.join(project_dir, "settings.json")
|
|
59
|
+
try:
|
|
60
|
+
with open(settings_path, "r", encoding="utf-8") as f:
|
|
61
|
+
settings = json.load(f)
|
|
62
|
+
budget_cfg = settings.get("_omg", {}).get("cost_budget", {})
|
|
63
|
+
pricing = budget_cfg.get("pricing", {})
|
|
64
|
+
session_limit = _safe_float(budget_cfg.get("session_limit_usd"), DEFAULT_SESSION_LIMIT_USD)
|
|
65
|
+
input_per_mtok = _safe_float(pricing.get("input_per_mtok"), DEFAULT_INPUT_PER_MTOK)
|
|
66
|
+
output_per_mtok = _safe_float(pricing.get("output_per_mtok"), DEFAULT_OUTPUT_PER_MTOK)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return session_limit, input_per_mtok, output_per_mtok
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _to_text(value) -> str:
|
|
74
|
+
if value is None:
|
|
75
|
+
return ""
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
return value
|
|
78
|
+
try:
|
|
79
|
+
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
80
|
+
except Exception:
|
|
81
|
+
return str(value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _estimate_call_cost(tool_input, tool_response, input_per_mtok: float, output_per_mtok: float) -> float:
|
|
85
|
+
input_text = _to_text(tool_input)
|
|
86
|
+
output_text = _to_text(tool_response)
|
|
87
|
+
|
|
88
|
+
tokens_in = estimate_tokens(input_text, tier=2)
|
|
89
|
+
tokens_out = estimate_tokens(output_text, tier=2)
|
|
90
|
+
|
|
91
|
+
cost_in = (tokens_in / 1_000_000.0) * input_per_mtok
|
|
92
|
+
cost_out = (tokens_out / 1_000_000.0) * output_per_mtok
|
|
93
|
+
return max(0.0, cost_in + cost_out)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _project_total_calls(used_cost_usd: float, used_calls: int, session_limit_usd: float) -> int:
|
|
97
|
+
if used_calls <= 0 or used_cost_usd <= 0:
|
|
98
|
+
return DEFAULT_PROJECTED_TOOL_CALLS
|
|
99
|
+
avg_cost = used_cost_usd / float(used_calls)
|
|
100
|
+
if avg_cost <= 0:
|
|
101
|
+
return DEFAULT_PROJECTED_TOOL_CALLS
|
|
102
|
+
projected = max(used_calls, int(round(session_limit_usd / avg_cost)))
|
|
103
|
+
if projected > (DEFAULT_PROJECTED_TOOL_CALLS * 10):
|
|
104
|
+
return DEFAULT_PROJECTED_TOOL_CALLS
|
|
105
|
+
rounded = int(round(projected / 10.0) * 10)
|
|
106
|
+
return max(10, rounded)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _build_context(used_cost_usd: float, session_limit_usd: float, used_calls: int, projected_calls: int) -> str:
|
|
110
|
+
remaining_ratio = 1.0 - (used_cost_usd / session_limit_usd)
|
|
111
|
+
remaining_pct = int(round(max(0.0, min(1.0, remaining_ratio)) * 100))
|
|
112
|
+
return (
|
|
113
|
+
f"Budget: {remaining_pct}% remaining | "
|
|
114
|
+
f"${used_cost_usd:.2f} of ${session_limit_usd:.2f} used | "
|
|
115
|
+
f"{used_calls} tool calls of ~{projected_calls}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_threshold_message(pct: int) -> str:
|
|
120
|
+
if pct >= 95:
|
|
121
|
+
return (
|
|
122
|
+
f"@cost-limit: {pct}% budget used. "
|
|
123
|
+
"Complete current task and stop. Do NOT start new tasks."
|
|
124
|
+
)
|
|
125
|
+
if pct >= 80:
|
|
126
|
+
return (
|
|
127
|
+
f"@cost-critical: {pct}% budget used. "
|
|
128
|
+
"Be efficient \u2014 minimize unnecessary tool calls, "
|
|
129
|
+
"batch operations where possible."
|
|
130
|
+
)
|
|
131
|
+
return f"@cost-warning: {pct}% budget used"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _read_thresholds_config(project_dir: str) -> list[int]:
|
|
135
|
+
try:
|
|
136
|
+
settings_path = os.path.join(project_dir, "settings.json")
|
|
137
|
+
with open(settings_path, "r", encoding="utf-8") as f:
|
|
138
|
+
settings = json.load(f)
|
|
139
|
+
raw = settings.get("_omg", {}).get("cost_budget", {}).get("thresholds")
|
|
140
|
+
if isinstance(raw, list) and all(isinstance(t, (int, float)) for t in raw):
|
|
141
|
+
return sorted(int(t) for t in raw)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
return list(DEFAULT_THRESHOLDS)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _read_threshold_state(project_dir: str) -> dict[str, Any]:
|
|
148
|
+
path = os.path.join(project_dir, THRESHOLD_STATE_FILE)
|
|
149
|
+
try:
|
|
150
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
151
|
+
return json.load(f)
|
|
152
|
+
except Exception:
|
|
153
|
+
return {"session_id": "", "fired": []}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _write_threshold_state(project_dir: str, state: dict[str, Any]) -> None:
|
|
157
|
+
path = os.path.join(project_dir, THRESHOLD_STATE_FILE)
|
|
158
|
+
try:
|
|
159
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
160
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
161
|
+
json.dump(state, f, separators=(",", ":"))
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _check_thresholds(
|
|
167
|
+
used_pct: float, project_dir: str, session_id: str
|
|
168
|
+
) -> list[str]:
|
|
169
|
+
thresholds = _read_thresholds_config(project_dir)
|
|
170
|
+
state = _read_threshold_state(project_dir)
|
|
171
|
+
|
|
172
|
+
if state.get("session_id", "") != session_id:
|
|
173
|
+
state = {"session_id": session_id, "fired": []}
|
|
174
|
+
|
|
175
|
+
already_fired = set(state.get("fired", []))
|
|
176
|
+
new_messages: list[str] = []
|
|
177
|
+
new_fired: list[int] = []
|
|
178
|
+
|
|
179
|
+
for threshold in thresholds:
|
|
180
|
+
if used_pct >= threshold and threshold not in already_fired:
|
|
181
|
+
new_messages.append(_get_threshold_message(threshold))
|
|
182
|
+
new_fired.append(threshold)
|
|
183
|
+
|
|
184
|
+
if new_fired:
|
|
185
|
+
state["fired"] = sorted(list(already_fired | set(new_fired)))
|
|
186
|
+
state["session_id"] = session_id
|
|
187
|
+
_write_threshold_state(project_dir, state)
|
|
188
|
+
|
|
189
|
+
return new_messages
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main() -> None:
|
|
193
|
+
setup_crash_handler("budget-governor", fail_closed=False)
|
|
194
|
+
|
|
195
|
+
payload = json_input()
|
|
196
|
+
if not get_feature_flag("COST_TRACKING", default=False):
|
|
197
|
+
sys.exit(0)
|
|
198
|
+
|
|
199
|
+
project_dir = get_project_dir()
|
|
200
|
+
session_limit_usd, input_per_mtok, output_per_mtok = _read_budget_config(project_dir)
|
|
201
|
+
summary = read_cost_summary(project_dir)
|
|
202
|
+
|
|
203
|
+
estimated_current_cost = _estimate_call_cost(
|
|
204
|
+
payload.get("tool_input", {}),
|
|
205
|
+
payload.get("tool_response", {}),
|
|
206
|
+
input_per_mtok,
|
|
207
|
+
output_per_mtok,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
used_cost_usd = float(summary.get("total_cost_usd", 0.0)) + estimated_current_cost
|
|
211
|
+
used_calls = int(summary.get("entry_count", 0)) + 1
|
|
212
|
+
projected_calls = _project_total_calls(used_cost_usd, used_calls, session_limit_usd)
|
|
213
|
+
|
|
214
|
+
context = _build_context(
|
|
215
|
+
used_cost_usd=used_cost_usd,
|
|
216
|
+
session_limit_usd=session_limit_usd,
|
|
217
|
+
used_calls=used_calls,
|
|
218
|
+
projected_calls=projected_calls,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
used_pct = (used_cost_usd / session_limit_usd * 100) if session_limit_usd > 0 else 0.0
|
|
222
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID", "")
|
|
223
|
+
threshold_alerts = _check_thresholds(used_pct, project_dir, session_id)
|
|
224
|
+
if threshold_alerts:
|
|
225
|
+
context += "\n" + "\n".join(threshold_alerts)
|
|
226
|
+
|
|
227
|
+
json.dump({"additionalContext": context}, sys.stdout)
|
|
228
|
+
sys.exit(0)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
main()
|