@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,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
IntentGate Keyword Detection Hook — OMG v1.2
|
|
4
|
+
|
|
5
|
+
UserPromptSubmit hook that detects magic keywords in user prompts,
|
|
6
|
+
maps them to intents with confidence scoring, and injects a LEADER_HINT
|
|
7
|
+
into the hook output for downstream routing. Detection only — no execution.
|
|
8
|
+
|
|
9
|
+
Classification v1.2:
|
|
10
|
+
- Each detected intent includes a confidence score (0.0–1.0)
|
|
11
|
+
- Compound intent parsing: multiple keywords → multiple intents
|
|
12
|
+
|
|
13
|
+
Confidence rules:
|
|
14
|
+
- Exact keyword match (standalone): 0.95
|
|
15
|
+
- Keyword embedded in context: 0.90
|
|
16
|
+
- Keyword in compound phrase (multi-intent): 0.85
|
|
17
|
+
- Multiple occurrences of same keyword: 0.98 (cap)
|
|
18
|
+
|
|
19
|
+
Magic keywords:
|
|
20
|
+
- ultrawork, autopilot, ralph → execution modes
|
|
21
|
+
- plan this, tdd, search → task types
|
|
22
|
+
- stop, crazy → special directives
|
|
23
|
+
"""
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
HOOKS_DIR = os.path.dirname(__file__)
|
|
31
|
+
if HOOKS_DIR not in sys.path:
|
|
32
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from hooks._common import (
|
|
36
|
+
setup_crash_handler,
|
|
37
|
+
json_input,
|
|
38
|
+
get_feature_flag,
|
|
39
|
+
_resolve_project_dir,
|
|
40
|
+
check_performance_budget,
|
|
41
|
+
PRE_TOOL_INJECT_MAX_MS,
|
|
42
|
+
)
|
|
43
|
+
except ImportError:
|
|
44
|
+
import importlib
|
|
45
|
+
_common = importlib.import_module("_common")
|
|
46
|
+
setup_crash_handler = _common.setup_crash_handler
|
|
47
|
+
json_input = _common.json_input
|
|
48
|
+
get_feature_flag = _common.get_feature_flag
|
|
49
|
+
_resolve_project_dir = _common._resolve_project_dir
|
|
50
|
+
check_performance_budget = _common.check_performance_budget
|
|
51
|
+
PRE_TOOL_INJECT_MAX_MS = _common.PRE_TOOL_INJECT_MAX_MS
|
|
52
|
+
|
|
53
|
+
setup_crash_handler("intentgate-keyword-detector", fail_closed=False)
|
|
54
|
+
|
|
55
|
+
# ═══════════════════════════════════════════════════════════
|
|
56
|
+
# KEYWORD → INTENT MAPPING
|
|
57
|
+
# ═══════════════════════════════════════════════════════════
|
|
58
|
+
KEYWORD_INTENT_MAP = {
|
|
59
|
+
"ultrawork": "INTENT_MAX_EFFORT",
|
|
60
|
+
"autopilot": "INTENT_AUTONOMOUS",
|
|
61
|
+
"ralph": "INTENT_LOOP",
|
|
62
|
+
"plan this": "INTENT_PLAN",
|
|
63
|
+
"tdd": "INTENT_TEST_DRIVEN",
|
|
64
|
+
"search": "INTENT_SEARCH",
|
|
65
|
+
"stop": "INTENT_STOP",
|
|
66
|
+
"crazy": "INTENT_CRAZY",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ═══════════════════════════════════════════════════════════
|
|
71
|
+
# CONFIDENCE SCORING ENGINE
|
|
72
|
+
# ═══════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
def _count_keyword_occurrences(keyword, prompt_lower):
|
|
75
|
+
"""Count occurrences of keyword in prompt (respecting matching rules)."""
|
|
76
|
+
if " " in keyword:
|
|
77
|
+
# Multi-word: non-overlapping substring count
|
|
78
|
+
count = 0
|
|
79
|
+
start = 0
|
|
80
|
+
while True:
|
|
81
|
+
idx = prompt_lower.find(keyword, start)
|
|
82
|
+
if idx == -1:
|
|
83
|
+
break
|
|
84
|
+
count += 1
|
|
85
|
+
start = idx + len(keyword)
|
|
86
|
+
return count
|
|
87
|
+
else:
|
|
88
|
+
# Single-word: word boundary count
|
|
89
|
+
pattern = r'\b' + re.escape(keyword) + r'\b'
|
|
90
|
+
return len(re.findall(pattern, prompt_lower))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _compute_confidence(keyword, prompt_lower, occurrence_count, total_intents):
|
|
94
|
+
"""Compute confidence score for a detected intent.
|
|
95
|
+
|
|
96
|
+
Rules (applied in priority order):
|
|
97
|
+
- Multiple occurrences of same keyword: 0.98 (capped)
|
|
98
|
+
- Keyword in compound phrase (multiple intents): 0.85
|
|
99
|
+
- Exact keyword match (standalone prompt): 0.95
|
|
100
|
+
- Keyword embedded in context signals: 0.90
|
|
101
|
+
"""
|
|
102
|
+
# Multiple occurrences of same keyword → highest confidence (cap)
|
|
103
|
+
if occurrence_count > 1:
|
|
104
|
+
return 0.98
|
|
105
|
+
|
|
106
|
+
# Compound intent (multiple different keywords detected)
|
|
107
|
+
if total_intents > 1:
|
|
108
|
+
return 0.85
|
|
109
|
+
|
|
110
|
+
# Single intent, single occurrence
|
|
111
|
+
stripped = prompt_lower.strip()
|
|
112
|
+
|
|
113
|
+
# Exact/standalone match → high confidence
|
|
114
|
+
if stripped == keyword:
|
|
115
|
+
return 0.95
|
|
116
|
+
|
|
117
|
+
# Keyword embedded in surrounding context → slightly lower
|
|
118
|
+
return 0.90
|
|
119
|
+
# ═══════════════════════════════════════════════════════════
|
|
120
|
+
# FEATURE FLAG CHECK
|
|
121
|
+
# ═══════════════════════════════════════════════════════════
|
|
122
|
+
start_time = time.time()
|
|
123
|
+
|
|
124
|
+
if not get_feature_flag("INTENTGATE", default=False):
|
|
125
|
+
# Feature disabled — return no-op JSON
|
|
126
|
+
json.dump({}, sys.stdout)
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
# ═══════════════════════════════════════════════════════════
|
|
130
|
+
# INPUT PARSING
|
|
131
|
+
# ═══════════════════════════════════════════════════════════
|
|
132
|
+
data = json_input()
|
|
133
|
+
|
|
134
|
+
prompt = data.get("tool_input", {}).get("user_message", "") or data.get("user_message", "")
|
|
135
|
+
if not prompt:
|
|
136
|
+
json.dump({}, sys.stdout)
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
|
|
139
|
+
prompt_lower = prompt.lower().strip()
|
|
140
|
+
|
|
141
|
+
# ═══════════════════════════════════════════════════════════
|
|
142
|
+
# KEYWORD DETECTION (case-insensitive, multi-keyword, confidence scoring)
|
|
143
|
+
# ═══════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
# First pass: detect keywords with occurrence counts
|
|
146
|
+
detected_raw = []
|
|
147
|
+
|
|
148
|
+
for keyword, intent in KEYWORD_INTENT_MAP.items():
|
|
149
|
+
occurrences = _count_keyword_occurrences(keyword, prompt_lower)
|
|
150
|
+
if occurrences > 0:
|
|
151
|
+
detected_raw.append((keyword, intent, occurrences))
|
|
152
|
+
|
|
153
|
+
# Second pass: compute confidence scores
|
|
154
|
+
num_intents = len(detected_raw)
|
|
155
|
+
detected_intents = []
|
|
156
|
+
|
|
157
|
+
for keyword, intent, occurrences in detected_raw:
|
|
158
|
+
confidence = _compute_confidence(keyword, prompt_lower, occurrences, num_intents)
|
|
159
|
+
detected_intents.append({
|
|
160
|
+
"intent": intent,
|
|
161
|
+
"confidence": confidence,
|
|
162
|
+
"keyword": keyword,
|
|
163
|
+
})
|
|
164
|
+
# ═══════════════════════════════════════════════════════════
|
|
165
|
+
# OUTPUT CONSTRUCTION
|
|
166
|
+
# ═══════════════════════════════════════════════════════════
|
|
167
|
+
output = {}
|
|
168
|
+
|
|
169
|
+
if detected_intents:
|
|
170
|
+
# Inject LEADER_HINT with detected intents and confidence scores
|
|
171
|
+
output["LEADER_HINT"] = {
|
|
172
|
+
"detected_intents": detected_intents,
|
|
173
|
+
"keyword_count": len(detected_intents),
|
|
174
|
+
"routing_enabled": True,
|
|
175
|
+
"classification_version": "1.2",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ═══════════════════════════════════════════════════════════
|
|
179
|
+
# PERFORMANCE BUDGET CHECK
|
|
180
|
+
# ═══════════════════════════════════════════════════════════
|
|
181
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
182
|
+
check_performance_budget("intentgate-keyword-detector", elapsed_ms, PRE_TOOL_INJECT_MAX_MS)
|
|
183
|
+
|
|
184
|
+
# ═══════════════════════════════════════════════════════════
|
|
185
|
+
# OUTPUT
|
|
186
|
+
# ═══════════════════════════════════════════════════════════
|
|
187
|
+
json.dump(output, sys.stdout)
|
|
188
|
+
sys.exit(0)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Magic Keyword Router — OMG v1.2
|
|
4
|
+
|
|
5
|
+
PostToolUse hook that reads the LEADER_HINT produced by
|
|
6
|
+
intentgate-keyword-detector, selects the appropriate agent based on intent,
|
|
7
|
+
and writes a routing result to `.omg/state/routing_result.json`.
|
|
8
|
+
|
|
9
|
+
Decision only — no subprocess calls or agent execution.
|
|
10
|
+
|
|
11
|
+
Input sources (checked in priority order):
|
|
12
|
+
1. LEADER_HINT in stdin JSON (from hook pipeline)
|
|
13
|
+
2. `.omg/state/leader_hint.json` (persisted by future integrations)
|
|
14
|
+
|
|
15
|
+
Feature flag: OMG_MAGIC_ROUTER_ENABLED (default off)
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
HOOKS_DIR = os.path.dirname(__file__)
|
|
23
|
+
if HOOKS_DIR not in sys.path:
|
|
24
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from hooks._common import (
|
|
28
|
+
setup_crash_handler,
|
|
29
|
+
json_input,
|
|
30
|
+
get_feature_flag,
|
|
31
|
+
atomic_json_write,
|
|
32
|
+
get_project_dir,
|
|
33
|
+
check_performance_budget,
|
|
34
|
+
PRE_TOOL_INJECT_MAX_MS,
|
|
35
|
+
)
|
|
36
|
+
except ImportError:
|
|
37
|
+
import importlib
|
|
38
|
+
_common = importlib.import_module("_common")
|
|
39
|
+
setup_crash_handler = _common.setup_crash_handler
|
|
40
|
+
json_input = _common.json_input
|
|
41
|
+
get_feature_flag = _common.get_feature_flag
|
|
42
|
+
atomic_json_write = _common.atomic_json_write
|
|
43
|
+
get_project_dir = _common.get_project_dir
|
|
44
|
+
check_performance_budget = _common.check_performance_budget
|
|
45
|
+
PRE_TOOL_INJECT_MAX_MS = _common.PRE_TOOL_INJECT_MAX_MS
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from hooks._agent_registry import INTENT_ROUTING
|
|
49
|
+
except ImportError:
|
|
50
|
+
_registry = importlib.import_module("_agent_registry")
|
|
51
|
+
INTENT_ROUTING = _registry.INTENT_ROUTING
|
|
52
|
+
|
|
53
|
+
setup_crash_handler("magic-keyword-router", fail_closed=False)
|
|
54
|
+
|
|
55
|
+
# ═══════════════════════════════════════════════════════════
|
|
56
|
+
# CONSTANTS
|
|
57
|
+
# ═══════════════════════════════════════════════════════════
|
|
58
|
+
ROUTING_RESULT_PATH = ".omg/state/routing_result.json"
|
|
59
|
+
LEADER_HINT_PATH = ".omg/state/leader_hint.json"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ═══════════════════════════════════════════════════════════
|
|
63
|
+
# FEATURE FLAG CHECK
|
|
64
|
+
# ═══════════════════════════════════════════════════════════
|
|
65
|
+
start_time = time.time()
|
|
66
|
+
|
|
67
|
+
if not get_feature_flag("MAGIC_ROUTER", default=False):
|
|
68
|
+
# Feature disabled — no-op
|
|
69
|
+
json.dump({}, sys.stdout)
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
|
|
72
|
+
# ═══════════════════════════════════════════════════════════
|
|
73
|
+
# INPUT PARSING
|
|
74
|
+
# ═══════════════════════════════════════════════════════════
|
|
75
|
+
data = json_input()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _extract_leader_hint(hook_data):
|
|
79
|
+
"""Extract LEADER_HINT from hook stdin data.
|
|
80
|
+
|
|
81
|
+
Checks multiple locations where the hint may appear:
|
|
82
|
+
- Top-level "LEADER_HINT" key
|
|
83
|
+
- Nested under "tool_output"
|
|
84
|
+
- Nested under "hookSpecificOutput"
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(hook_data, dict):
|
|
87
|
+
return None
|
|
88
|
+
# Direct top-level
|
|
89
|
+
hint = hook_data.get("LEADER_HINT")
|
|
90
|
+
if hint:
|
|
91
|
+
return hint
|
|
92
|
+
# Nested in tool_output
|
|
93
|
+
tool_output = hook_data.get("tool_output", {})
|
|
94
|
+
if isinstance(tool_output, dict):
|
|
95
|
+
hint = tool_output.get("LEADER_HINT")
|
|
96
|
+
if hint:
|
|
97
|
+
return hint
|
|
98
|
+
# Nested in hookSpecificOutput
|
|
99
|
+
hso = hook_data.get("hookSpecificOutput", {})
|
|
100
|
+
if isinstance(hso, dict):
|
|
101
|
+
hint = hso.get("LEADER_HINT")
|
|
102
|
+
if hint:
|
|
103
|
+
return hint
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _read_leader_hint_file(project_dir):
|
|
108
|
+
"""Read LEADER_HINT from persisted file (secondary source)."""
|
|
109
|
+
path = os.path.join(project_dir, LEADER_HINT_PATH)
|
|
110
|
+
if not os.path.exists(path):
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
114
|
+
data = json.load(f)
|
|
115
|
+
# File may contain {"LEADER_HINT": {...}} or the hint directly
|
|
116
|
+
if isinstance(data, dict):
|
|
117
|
+
return data.get("LEADER_HINT", data) if "detected_intents" not in data else data
|
|
118
|
+
return None
|
|
119
|
+
except (json.JSONDecodeError, OSError):
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _resolve_routing(leader_hint):
|
|
124
|
+
"""Given a LEADER_HINT dict, resolve the target agent.
|
|
125
|
+
|
|
126
|
+
Returns (target_agent, intent, confidence) tuple.
|
|
127
|
+
target_agent may be None (e.g., INTENT_STOP → halt).
|
|
128
|
+
"""
|
|
129
|
+
detected = leader_hint.get("detected_intents", [])
|
|
130
|
+
if not detected:
|
|
131
|
+
return None, None, 0.0
|
|
132
|
+
|
|
133
|
+
# Pick first non-None routable intent (highest priority = first in list)
|
|
134
|
+
for intent_entry in detected:
|
|
135
|
+
intent_name = intent_entry.get("intent", "")
|
|
136
|
+
confidence = intent_entry.get("confidence", 0.0)
|
|
137
|
+
target = INTENT_ROUTING.get(intent_name)
|
|
138
|
+
|
|
139
|
+
# INTENT_STOP maps to None explicitly — that IS a valid routing result
|
|
140
|
+
if intent_name in INTENT_ROUTING:
|
|
141
|
+
return target, intent_name, confidence
|
|
142
|
+
|
|
143
|
+
# No known intent found
|
|
144
|
+
return None, None, 0.0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ═══════════════════════════════════════════════════════════
|
|
148
|
+
# LEADER_HINT RESOLUTION (stdin preferred, file fallback)
|
|
149
|
+
# ═══════════════════════════════════════════════════════════
|
|
150
|
+
project_dir = get_project_dir()
|
|
151
|
+
leader_hint = _extract_leader_hint(data)
|
|
152
|
+
|
|
153
|
+
if leader_hint is None:
|
|
154
|
+
leader_hint = _read_leader_hint_file(project_dir)
|
|
155
|
+
|
|
156
|
+
# ═══════════════════════════════════════════════════════════
|
|
157
|
+
# ROUTING DECISION
|
|
158
|
+
# ═══════════════════════════════════════════════════════════
|
|
159
|
+
from datetime import datetime, timezone
|
|
160
|
+
|
|
161
|
+
routing_result_path = os.path.join(project_dir, ROUTING_RESULT_PATH)
|
|
162
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
163
|
+
|
|
164
|
+
if leader_hint and leader_hint.get("detected_intents"):
|
|
165
|
+
target_agent, intent, confidence = _resolve_routing(leader_hint)
|
|
166
|
+
routing_result = {
|
|
167
|
+
"target_agent": target_agent,
|
|
168
|
+
"intent": intent,
|
|
169
|
+
"confidence": confidence,
|
|
170
|
+
"fallback": False,
|
|
171
|
+
"timestamp": ts,
|
|
172
|
+
}
|
|
173
|
+
else:
|
|
174
|
+
# No LEADER_HINT → fallback
|
|
175
|
+
routing_result = {
|
|
176
|
+
"target_agent": None,
|
|
177
|
+
"intent": None,
|
|
178
|
+
"confidence": 0.0,
|
|
179
|
+
"fallback": True,
|
|
180
|
+
"timestamp": ts,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
atomic_json_write(routing_result_path, routing_result)
|
|
184
|
+
|
|
185
|
+
# ═══════════════════════════════════════════════════════════
|
|
186
|
+
# PERFORMANCE BUDGET CHECK
|
|
187
|
+
# ═══════════════════════════════════════════════════════════
|
|
188
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
189
|
+
check_performance_budget("magic-keyword-router", elapsed_ms, PRE_TOOL_INJECT_MAX_MS)
|
|
190
|
+
|
|
191
|
+
# ═══════════════════════════════════════════════════════════
|
|
192
|
+
# OUTPUT (no-op for PostToolUse — just exit clean)
|
|
193
|
+
# ═══════════════════════════════════════════════════════════
|
|
194
|
+
json.dump({}, sys.stdout)
|
|
195
|
+
sys.exit(0)
|