@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.
Files changed (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,672 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ UserPromptSubmit Hook — OMG v1
4
+
5
+ Inspired by oh-my-opencode's Sisyphus agent system. Key upgrades:
6
+ 1. Intent classification BEFORE acting (IntentGate)
7
+ 2. Discipline enforcement — never stop halfway
8
+ 3. Agent-aware routing — Codex/Gemini/Claude orchestration
9
+ 4. Anti-hallucination protocol
10
+ 5. Error loop prevention (circuit-breaker awareness)
11
+ 6. Vision/screenshot auto-detection
12
+ 7. DDD/Security domain auto-triggers
13
+ 8. Context budget: MAX 800 chars output
14
+
15
+ No dependency on CLAUDE.md or AGENTS.md.
16
+ """
17
+ import json, sys, os, re, time
18
+ import importlib
19
+
20
+ HOOKS_DIR = os.path.dirname(__file__)
21
+ if HOOKS_DIR not in sys.path:
22
+ sys.path.insert(0, HOOKS_DIR)
23
+
24
+ try:
25
+ from hooks._common import setup_crash_handler, json_input, atomic_json_write, get_feature_flag, _resolve_project_dir
26
+ from hooks.state_migration import resolve_state_dir
27
+ from hooks._budget import BUDGET_PROMPT_TOTAL as budget_prompt_total
28
+ from hooks.context_pressure import estimate_context_pressure
29
+ except ImportError:
30
+ _common = importlib.import_module("_common")
31
+ _state_migration = importlib.import_module("state_migration")
32
+ _budget = importlib.import_module("_budget")
33
+ _context_pressure = importlib.import_module("context_pressure")
34
+ setup_crash_handler = _common.setup_crash_handler
35
+ json_input = _common.json_input
36
+ atomic_json_write = _common.atomic_json_write
37
+ get_feature_flag = _common.get_feature_flag
38
+ _resolve_project_dir = _common._resolve_project_dir
39
+ resolve_state_dir = _state_migration.resolve_state_dir
40
+ budget_prompt_total = _budget.BUDGET_PROMPT_TOTAL
41
+ estimate_context_pressure = _context_pressure.estimate_context_pressure
42
+
43
+ BUDGET_PROMPT_TOTAL = budget_prompt_total
44
+
45
+ setup_crash_handler("prompt-enhancer", fail_closed=False)
46
+
47
+ data = json_input()
48
+
49
+ prompt = data.get("tool_input", {}).get("user_message", "") or data.get("user_message", "")
50
+ if not prompt:
51
+ sys.exit(0)
52
+
53
+ prompt_lower = prompt.lower().strip()
54
+ project_dir = _resolve_project_dir()
55
+ omg_root = os.path.join(project_dir, ".omg")
56
+ state_dir = resolve_state_dir(project_dir, "state", "")
57
+ knowledge_dir = resolve_state_dir(project_dir, "knowledge", "knowledge")
58
+ injections = []
59
+
60
+ # ── Context budget ──
61
+ MAX_CHARS = BUDGET_PROMPT_TOTAL
62
+
63
+ def budget_ok():
64
+ return sum(len(i) for i in injections) < MAX_CHARS
65
+
66
+ def add(text):
67
+ remaining = MAX_CHARS - sum(len(i) for i in injections)
68
+ if remaining <= 20:
69
+ return
70
+ if len(text) > remaining:
71
+ text = text[:remaining - 3] + "..."
72
+ injections.append(text)
73
+
74
+
75
+ def signal_matches_text(signal, text):
76
+ if re.search(r'[\uac00-\ud7a3]', signal):
77
+ return signal in text
78
+ return re.search(r'\b' + re.escape(signal) + r'\b', text, re.IGNORECASE) is not None
79
+
80
+ # ── Zero-injection optimization ──
81
+ # Simple prompts (≤10 words, no coding/mode/routing signals) get zero overhead
82
+ _word_count_early = len(prompt_lower.split())
83
+ _has_any_signal = any([
84
+ any(signal_matches_text(sig, prompt) for sig in ["fix","bug","implement","build","create","refactor",
85
+ "review","auth","css","layout","ui","ux","test",
86
+ "stuck","error","crash","ralph","ulw","crazy",
87
+ "plan","design","search","find","research","explain",
88
+ "codex","gemini","ccg","screenshot","screen",
89
+ "security","warning","hook error","resume","handoff",
90
+ "continue","domain","scaffold","debug","deploy",
91
+ "수정","구현","버그","에러","고쳐","스크린샷","보안"]),
92
+ _word_count_early > 10,
93
+ ])
94
+ if not _has_any_signal:
95
+ sys.exit(0)
96
+ # ═══════════════════════════════════════════════════════════
97
+ # 1. INTENT CLASSIFICATION (IntentGate)
98
+ # ═══════════════════════════════════════════════════════════
99
+ INTENT_MAP = {
100
+ "fix": {
101
+ "signals": ["fix", "bug", "error", "broken", "crash", "not working", "fails",
102
+ "수정", "버그", "에러", "고쳐", "고치", "안돼", "안됨", "깨짐", "오류"],
103
+ "directive": "FIX — Debug root cause, patch source code (NOT tests), verify with evidence"
104
+ },
105
+ "plan": {
106
+ "signals": ["plan", "design", "architect", "strategy",
107
+ "계획", "설계", "아키텍처", "전략"],
108
+ "directive": "PLAN — Ask clarifying questions. Map domain. Plan before code"
109
+ },
110
+ "refactor": {
111
+ "signals": ["refactor", "clean", "optimize", "improve", "simplify",
112
+ "리팩토링", "리팩터", "최적화", "개선", "정리"],
113
+ "directive": "REFACTOR — Preserve behavior. Before AND after tests must pass"
114
+ },
115
+ "review": {
116
+ "signals": ["review", "check", "audit", "inspect", "look at",
117
+ "리뷰", "검토", "확인", "감사", "점검"],
118
+ "directive": "REVIEW — Read ALL code first. Report findings. Don't change unless asked"
119
+ },
120
+ "research": {
121
+ "signals": ["research", "find", "search", "how to", "what is", "explain",
122
+ "검색", "찾아", "어떻게", "설명", "문서"],
123
+ "directive": "RESEARCH — Search, synthesize, report. Use web_search if needed"
124
+ },
125
+ "implement": {
126
+ "signals": ["implement", "build", "create", "add", "make", "feature", "new",
127
+ "구현", "빌드", "생성", "만들", "추가", "기능", "개발"],
128
+ "directive": "IMPLEMENT — Plan → code → test → verify. Follow existing patterns"
129
+ },
130
+ }
131
+
132
+ detected_intent = None
133
+ for intent_key, intent_data in INTENT_MAP.items():
134
+ if any(signal_matches_text(sig, prompt) for sig in intent_data["signals"]):
135
+ detected_intent = intent_key
136
+ break
137
+
138
+ # ═══════════════════════════════════════════════════════════
139
+ # 2. DISCIPLINE SYSTEM (Sisyphus-grade)
140
+ # ═══════════════════════════════════════════════════════════
141
+ parts = []
142
+
143
+ if detected_intent:
144
+ parts.append(f"@intent: {INTENT_MAP[detected_intent]['directive']}")
145
+
146
+ parts.append(
147
+ "@discipline: Senior-eng mode. Clean minimal code. "
148
+ "VERIFY changes. NEVER claim done unverified. "
149
+ "NEVER modify tests as fix. No noise comments, "
150
+ "no generic names (data/result/temp/val). FULL file reads."
151
+ )
152
+
153
+ if detected_intent in ("fix", "implement", "refactor"):
154
+ parts.append(
155
+ "@verify: After EVERY change run build/lint/test. Show exit code."
156
+ )
157
+
158
+ if parts and budget_ok():
159
+ add("\n".join(parts))
160
+
161
+ # ═══════════════════════════════════════════════════════════
162
+ # 3. MODE DETECTION (ulw/ralph/crazy)
163
+ # ═══════════════════════════════════════════════════════════
164
+ ULW_SIGNALS = ["ulw", "ultrawork", "ralph", "끝까지", "멈추지마", "계속해",
165
+ "다될때까지", "don't stop", "keep going", "until done",
166
+ "finish everything", "complete all"]
167
+ is_ulw = any(signal_matches_text(sig, prompt) for sig in ULW_SIGNALS)
168
+
169
+ CRAZY_SIGNALS = ["crazy", "all agents", "maximum", "모든 에이전트", "최대", "미친"]
170
+ is_crazy = any(signal_matches_text(sig, prompt) for sig in CRAZY_SIGNALS)
171
+
172
+ if is_crazy and budget_ok():
173
+ add(
174
+ "@mode:CRAZY — All agents active. "
175
+ "Brainstorming is merged in CRAZY (no separate brainstorm step). "
176
+ "Claude=orchestrator, Codex=deep-code+security, Gemini=UI/UX. "
177
+ "Parallel dispatch. Error-loop prevention ON. "
178
+ "After planning, run a Codex validation pass before implementation."
179
+ )
180
+ elif is_ulw and budget_ok():
181
+ add(
182
+ "@mode:PERSISTENT — Do NOT stop until complete. "
183
+ "Work through ALL items. Skip if blocked, continue others. "
184
+ "Escalate to Codex/Gemini as needed. Verify everything."
185
+ )
186
+
187
+ # ── Ralph loop auto-activation on keyword ──
188
+ if is_ulw and get_feature_flag('ralph_loop'):
189
+ ralph_path = os.path.join(project_dir, '.omg', 'state', 'ralph-loop.json')
190
+ if not os.path.exists(ralph_path):
191
+ # Extract the goal from the prompt (everything after the keyword)
192
+ goal = prompt.strip()
193
+ for kw in ('ralph', 'ulw', 'ultrawork'):
194
+ if kw in prompt_lower:
195
+ idx = prompt_lower.find(kw) + len(kw)
196
+ extracted = prompt[idx:].strip()
197
+ if extracted:
198
+ goal = extracted
199
+ break
200
+ from datetime import datetime as _dt, timezone
201
+ state = {
202
+ 'active': True,
203
+ 'iteration': 0,
204
+ 'max_iterations': 50,
205
+ 'original_prompt': goal[:200],
206
+ 'started_at': _dt.now(timezone.utc).isoformat(),
207
+ 'checklist_path': '.omg/state/_checklist.md'
208
+ }
209
+ try:
210
+ os.makedirs(os.path.dirname(ralph_path), exist_ok=True)
211
+ atomic_json_write(ralph_path, state)
212
+ except Exception:
213
+ pass
214
+
215
+ # ═══════════════════════════════════════════════════════════
216
+ # 3b. AUTO-COMPLEXITY DETECTION (auto-trigger modes for complex tasks)
217
+ # ═══════════════════════════════════════════════════════════
218
+ # If user didn't explicitly request crazy/ulw, detect complexity and auto-suggest
219
+ if not is_crazy and not is_ulw and budget_ok():
220
+ complexity_score = 0
221
+
222
+ # Multi-step connectors (+1 each)
223
+ MULTI_STEP = ["and then", "after that", "followed by", "next", "also",
224
+ "그리고", "다음에", "이후에", "또한", "하고", "그다음"]
225
+ complexity_score += sum(1 for s in MULTI_STEP if signal_matches_text(s, prompt))
226
+
227
+ # Multiple action verbs in same prompt (+1 per verb beyond first, max +3)
228
+ ACTION_VERBS = ["fix", "implement", "build", "create", "add", "update",
229
+ "refactor", "migrate", "deploy", "rewrite", "redesign",
230
+ "수정", "구현", "만들", "추가", "수정", "리팩토링", "배포"]
231
+ verb_count = sum(1 for v in ACTION_VERBS if signal_matches_text(v, prompt))
232
+ complexity_score += min(max(verb_count - 1, 0), 3)
233
+
234
+ # Multi-file/component signals (+2 each)
235
+ MULTI_COMPONENT = ["entire", "all files", "whole project", "full stack",
236
+ "frontend and backend", "client and server", "end to end",
237
+ "every", "all the", "across",
238
+ "전체", "모든 파일", "풀스택", "모두", "전부",
239
+ "처음부터 끝까지"]
240
+ complexity_score += sum(2 for s in MULTI_COMPONENT if signal_matches_text(s, prompt))
241
+
242
+ # Architecture signals (+2 each)
243
+ ARCH_SIGNALS = ["architect", "redesign", "migration", "microservice",
244
+ "monorepo", "restructure", "overhaul", "rewrite from scratch",
245
+ "아키텍처", "마이그레이션", "재설계", "전면 수정", "처음부터 다시"]
246
+ complexity_score += sum(2 for s in ARCH_SIGNALS if signal_matches_text(s, prompt))
247
+
248
+ # Enumeration signals (numbered/bullet lists)
249
+ numbered_items = len(re.findall(r'(?:^|\n)\s*[\d]+[.)]\s', prompt_lower))
250
+ bullet_items = len(re.findall(r'(?:^|\n)\s*[-*]\s', prompt_lower))
251
+ complexity_score += min(numbered_items + bullet_items, 5)
252
+
253
+ # Word count signal
254
+ word_count = len(prompt_lower.split())
255
+ if word_count > 80:
256
+ complexity_score += 2
257
+ elif word_count > 40:
258
+ complexity_score += 1
259
+
260
+ # HIGH complexity (≥4): auto-trigger CRAZY
261
+ if complexity_score >= 4:
262
+ add(
263
+ "@mode:CRAZY(auto) — Complex task detected (multi-step/multi-component). "
264
+ "All agents active: Claude=orchestrator, Codex=deep-code, Gemini=UI/UX. "
265
+ "Work through all items systematically. Verify each step."
266
+ )
267
+ # MEDIUM complexity (≥2): auto-trigger PERSISTENT
268
+ elif complexity_score >= 2:
269
+ add(
270
+ "@mode:PERSISTENT(auto) — Multi-step task detected. "
271
+ "Work through ALL items. Skip if blocked, continue others. "
272
+ "Don't stop until checklist complete."
273
+ )
274
+
275
+ # ═══════════════════════════════════════════════════════════
276
+ # 3c. COGNITIVE MODE (from .omg/state/mode.txt)
277
+ # ═══════════════════════════════════════════════════════════
278
+ _mode_path = os.path.join(state_dir, 'mode.txt')
279
+ if os.path.exists(_mode_path) and budget_ok():
280
+ try:
281
+ with open(_mode_path, 'r', encoding='utf-8') as _mf:
282
+ _mode = _mf.read().strip().lower()
283
+ if _mode in ('research', 'architect', 'implement'):
284
+ _mode_hints = {
285
+ 'research': 'RESEARCH — Read/search/synthesize. No code changes unless asked.',
286
+ 'architect': 'ARCHITECT — Map system first. Specs and interfaces only, no implementation.',
287
+ 'implement': 'IMPLEMENT — TDD. Verify every change. Follow existing patterns.',
288
+ }
289
+ add(f'@mode:{_mode_hints[_mode]}')
290
+ except Exception:
291
+ pass
292
+
293
+
294
+ # ═══════════════════════════════════════════════════════════
295
+ # 4. SPECIALIST ROUTING (registry-based)
296
+ # ═══════════════════════════════════════════════════════════
297
+ CCG_SIGNALS = [
298
+ "ccg", "full stack", "full-stack", "frontend and backend", "backend and frontend",
299
+ "architecture review", "review everything", "cross-functional", "end-to-end", "e2e",
300
+ "풀스택", "아키텍처 리뷰", "전체 리뷰",
301
+ ]
302
+ DEEP_PLAN_SIGNALS = ["deep-plan", "deep plan", "/omg:deep-plan"]
303
+ EXPLICIT_GEMINI = ["gemini", "제미니"]
304
+ EXPLICIT_CODEX = ["codex", "코덱스"]
305
+
306
+ # Keyword-first model routing. If an explicit keyword exists, force OMG route first.
307
+ has_ccg_signal = any(signal_matches_text(sig, prompt) for sig in CCG_SIGNALS)
308
+ has_deep_plan_signal = any(signal_matches_text(sig, prompt) for sig in DEEP_PLAN_SIGNALS)
309
+ has_gemini_signal = any(signal_matches_text(sig, prompt) for sig in EXPLICIT_GEMINI)
310
+ has_codex_signal = any(signal_matches_text(sig, prompt) for sig in EXPLICIT_CODEX)
311
+
312
+ route_lock = ""
313
+ if has_deep_plan_signal:
314
+ route_lock = "deep-plan"
315
+ elif has_ccg_signal or (has_gemini_signal and has_codex_signal):
316
+ route_lock = "ccg"
317
+ elif has_gemini_signal:
318
+ route_lock = "gemini"
319
+ elif has_codex_signal:
320
+ route_lock = "codex"
321
+
322
+ if route_lock and budget_ok():
323
+ if route_lock == "deep-plan":
324
+ add(
325
+ '@route-lock: Explicit keyword route=deep-plan. Execute /OMG:deep-plan "[goal]" FIRST. '
326
+ "Do NOT call plugin/Skill routes (omg-teams/frontend-design/etc) before this OMG route."
327
+ )
328
+ elif route_lock == "ccg":
329
+ add(
330
+ '@route-lock: Explicit keyword route=ccg. Execute /OMG:ccg "[problem]" FIRST. '
331
+ "Do NOT call plugin/Skill routes (omg-teams/frontend-design/etc) before this OMG route."
332
+ )
333
+ elif route_lock == "gemini":
334
+ add(
335
+ '@route-lock: Explicit keyword route=gemini. Execute /OMG:escalate gemini "[problem]" FIRST. '
336
+ "Do NOT call plugin/Skill routes (omg-teams/frontend-design/etc) before this OMG route."
337
+ )
338
+ else:
339
+ add(
340
+ '@route-lock: Explicit keyword route=codex. Execute /OMG:escalate codex "[problem]" FIRST. '
341
+ "Do NOT call plugin/Skill routes (omg-teams/frontend-design/etc) before this OMG route."
342
+ )
343
+
344
+ if not route_lock and get_feature_flag('agent_registry') and budget_ok():
345
+ try:
346
+ try:
347
+ from hooks._agent_registry import resolve_agent, detect_available_models
348
+ except ImportError:
349
+ _agent_registry = importlib.import_module("_agent_registry")
350
+ resolve_agent = _agent_registry.resolve_agent
351
+ detect_available_models = _agent_registry.detect_available_models
352
+ _maybe_kws = locals().get("kws")
353
+ _routing_kws = _maybe_kws if _maybe_kws else set(re.findall(r'\b[a-zA-Z]{3,}\b', prompt_lower))
354
+ matched_agent = resolve_agent(_routing_kws)
355
+ if isinstance(matched_agent, dict):
356
+ _agent_name = matched_agent.get('name', '')
357
+ _preferred = matched_agent.get('preferred_model', 'claude')
358
+ if _preferred == 'gemini-cli':
359
+ add(f'@agent: {_agent_name} → /OMG:escalate gemini "[task]" (visual/frontend domain)')
360
+ elif _preferred == 'codex-cli':
361
+ add(f'@agent: {_agent_name} → /OMG:escalate codex "[task]" (backend/security domain)')
362
+ elif _preferred in ('claude', 'domain-dependent'):
363
+ _desc = str(matched_agent.get('description', ''))[:80]
364
+ if _desc:
365
+ add(f'@agent: {_agent_name} — {_desc}')
366
+ except Exception:
367
+ pass
368
+
369
+ SEQUENTIAL_THINKING_SIGNALS = [
370
+ "sequential thinking",
371
+ "sequential-thinking",
372
+ "chain of thought",
373
+ "step by step reasoning",
374
+ "단계적 사고",
375
+ ]
376
+ if any(signal_matches_text(sig, prompt) for sig in SEQUENTIAL_THINKING_SIGNALS) and budget_ok():
377
+ add("@reasoning: Use /OMG:sequential-thinking for structured hypothesis and verification flow.")
378
+
379
+ # Security domain warning (keep this — it's additive, not routing)
380
+ SECURITY_SIGNALS = [
381
+ "auth", "login", "signup", "session", "token", "password", "jwt", "oauth",
382
+ "payment", "billing", "checkout", "stripe", "card",
383
+ "database", "migration", "schema", "sql", "query",
384
+ "encrypt", "decrypt", "cors",
385
+ "인증", "로그인", "세션", "토큰", "비밀번호", "결제", "데이터베이스", "보안",
386
+ ]
387
+ if not route_lock and any(signal_matches_text(sig, prompt) for sig in SECURITY_SIGNALS) and budget_ok():
388
+ if detected_intent in ("fix", "implement", "refactor"):
389
+ add("@security: CRITICAL DOMAIN — No hardcoded secrets. Run /OMG:security-review after.")
390
+
391
+ # ═══════════════════════════════════════════════════════════
392
+ # 5. VISION DETECTION
393
+ # ═══════════════════════════════════════════════════════════
394
+ VISION_SIGNALS = [
395
+ "screenshot", "screen", "look at this", "see this", "attached image",
396
+ "this image", "the picture", "visual bug", "looks wrong", "looks broken",
397
+ "스크린샷", "화면 캡처", "이미지", "사진", "보여", "이렇게 보여",
398
+ ]
399
+ if any(signal_matches_text(sig, prompt) for sig in VISION_SIGNALS) and budget_ok():
400
+ add(
401
+ "@vision: Visual context detected. Use screenshot tools if available. "
402
+ "/OMG:escalate gemini for visual analysis."
403
+ )
404
+
405
+ # ═══════════════════════════════════════════════════════════
406
+ # 6. RESUME / HANDOFF
407
+ # ═══════════════════════════════════════════════════════════
408
+ RESUME_SIGNALS = [
409
+ "continue where", "pick up where", "left off", "resume", "handoff",
410
+ "what was i working on", "previous session",
411
+ "이어서", "계속해", "이전 세션", "하던 거", "핸드오프",
412
+ "session handoff", "## what was done",
413
+ ]
414
+ if any(signal_matches_text(sig, prompt) for sig in RESUME_SIGNALS) and budget_ok():
415
+ for hp in [os.path.join(state_dir, "handoff.md"), os.path.join(state_dir, "handoff-portable.md")]:
416
+ if os.path.exists(hp):
417
+ try:
418
+ with open(hp, "r", encoding="utf-8", errors="ignore") as f:
419
+ htext = f.read(1500)
420
+ sections = []
421
+ for s in re.split(r"\n## ", htext):
422
+ h = s.split("\n")[0].lower()
423
+ if any(k in h for k in ("next", "state", "fail")):
424
+ sections.append("## " + s.strip()[:200])
425
+ if sections:
426
+ add("@handoff:" + "\n".join(sections)[:250])
427
+ else:
428
+ add("@handoff: Read .omg/state/handoff.md for context")
429
+ except Exception:
430
+ pass
431
+ break
432
+
433
+ # ═══════════════════════════════════════════════════════════
434
+ # 7. CODING CONTEXT (checklist + DDD + knowledge)
435
+ # ═══════════════════════════════════════════════════════════
436
+ CODE_SIGNALS = [
437
+ "fix", "implement", "refactor", "build", "add", "create", "modify",
438
+ "change", "update", "code", "test", "debug",
439
+ "수정", "구현", "빌드", "추가", "생성", "코드", "테스트", "디버그",
440
+ "고쳐", "개발",
441
+ ]
442
+ is_coding = any(signal_matches_text(sig, prompt) for sig in CODE_SIGNALS)
443
+
444
+ if is_coding and budget_ok():
445
+ cp = os.path.join(state_dir, "_checklist.md")
446
+ if os.path.exists(cp):
447
+ try:
448
+ with open(cp, "r", encoding="utf-8", errors="ignore") as f:
449
+ lines = f.readlines()
450
+ done = sum(1 for l in lines if "[x]" in l.lower())
451
+ total = sum(1 for l in lines if l.strip().startswith(("[", "- [")))
452
+ pending = [l.strip().replace("[ ] ", "").replace("- [ ] ", "")[:50]
453
+ for l in lines if "[ ]" in l][:2]
454
+ if total > 0:
455
+ add(f"@progress: {done}/{total} | next: {' → '.join(pending)}")
456
+ except Exception:
457
+ pass
458
+
459
+ # DDD
460
+ DDD_SIGNALS = ["new domain", "new module", "scaffold", "domain", "새 도메인", "새 모듈"]
461
+ if any(signal_matches_text(sig, prompt) for sig in DDD_SIGNALS) and budget_ok():
462
+ pd = os.path.join(knowledge_dir, "domain-patterns")
463
+ if os.path.isdir(pd):
464
+ pats = [f.replace(".md", "") for f in os.listdir(pd) if f.endswith(".md")]
465
+ if pats:
466
+ add(f"@ddd: Patterns: {', '.join(pats[:3])}. Follow existing.")
467
+ else:
468
+ add("@ddd: No patterns. Use /OMG:domain-init for first reference.")
469
+
470
+ # Knowledge retrieval (top-2, with index cache for performance)
471
+ # §4.5: Instead of os.walk + read every file on every prompt,
472
+ # maintain a lightweight index (.omg/knowledge/.index.json) keyed by mtime.
473
+ kd = knowledge_dir
474
+ # Skip knowledge search for very short prompts with no coding signals (perf optimization)
475
+ _word_count = len(prompt_lower.split())
476
+ _has_code_signal = is_coding or detected_intent is not None
477
+ if os.path.isdir(kd) and budget_ok() and (_word_count >= 15 or _has_code_signal):
478
+ words = set(re.findall(r'\b[a-zA-Z]{3,}\b', prompt_lower))
479
+ words |= set(re.findall(r'[\uac00-\ud7a3]{2,}', prompt))
480
+ stops = {"the","and","for","that","this","with","from","have","will",
481
+ "but","not","are","was","can","could","should","about",
482
+ "just","also","want","need","like","make","please","help","use","try"}
483
+ kws = words - stops
484
+ if kws:
485
+ # Load or rebuild index
486
+ index_path = os.path.join(kd, ".index.json")
487
+ index = {}
488
+ try:
489
+ if os.path.exists(index_path):
490
+ with open(index_path, "r") as f:
491
+ index = json.load(f)
492
+ if not isinstance(index, dict):
493
+ print(f"[OMG] prompt-enhancer: index.json is not a dict ({type(index).__name__}), rebuilding", file=sys.stderr)
494
+ try:
495
+ os.remove(index_path)
496
+ except OSError:
497
+ pass
498
+ index = {}
499
+ except (json.JSONDecodeError, ValueError):
500
+ # Corrupted index — delete and rebuild
501
+ try:
502
+ os.remove(index_path)
503
+ except OSError:
504
+ pass
505
+ index = {}
506
+ except FileNotFoundError:
507
+ index = {}
508
+ except Exception:
509
+ index = {}
510
+
511
+ # Scan files, rebuild stale/missing entries (cap: 30 files)
512
+ rebuild = False
513
+ file_count = 0
514
+ for root, dirs, files in os.walk(kd):
515
+ for fn in files:
516
+ if not fn.endswith(".md") or fn.startswith("."):
517
+ continue
518
+ file_count += 1
519
+ if file_count > 30:
520
+ break
521
+ fp = os.path.join(root, fn)
522
+ try:
523
+ mtime = str(os.path.getmtime(fp))
524
+ cached = index.get(fp, {})
525
+ if cached.get("mtime") == mtime:
526
+ continue # still fresh
527
+ # Read and index (sanitize potential secrets before caching)
528
+ with open(fp, "r", encoding="utf-8", errors="ignore") as f:
529
+ content = f.read(1500).lower()
530
+ # Strip lines that look like secret assignments before caching
531
+ sanitized_lines = []
532
+ for cline in content.split("\n"):
533
+ if re.search(r'(?:key|secret|token|password|credential)\s*[:=]', cline):
534
+ continue
535
+ sanitized_lines.append(cline)
536
+ content = "\n".join(sanitized_lines)
537
+ index[fp] = {"mtime": mtime, "content": content}
538
+ rebuild = True
539
+ except Exception:
540
+ pass
541
+ if file_count > 30:
542
+ break
543
+
544
+ # Cap index at 100 entries, remove oldest by mtime if needed
545
+ if len(index) > 100:
546
+ # Sort by mtime (oldest first) and remove excess
547
+ sorted_entries = sorted(index.items(), key=lambda x: x[1].get("mtime", "0"))
548
+ entries_to_remove = len(index) - 100
549
+ for fp, _ in sorted_entries[:entries_to_remove]:
550
+ del index[fp]
551
+ rebuild = True
552
+
553
+ # Save index if changed using atomic write
554
+ if rebuild:
555
+ atomic_json_write(index_path, index)
556
+
557
+ # Match keywords against cached content
558
+ matches = []
559
+ for fp, data in index.items():
560
+ if not isinstance(data, dict) or "content" not in data:
561
+ continue
562
+ c = data["content"]
563
+ sc = sum(1 for kw in kws if kw in c)
564
+ if sc >= 2:
565
+ matches.append((sc, fp))
566
+ matches.sort(key=lambda x: -x[0])
567
+ for sc, fp in matches[:2]:
568
+ if not budget_ok():
569
+ break
570
+ rel = os.path.relpath(fp, omg_root)
571
+ add(f"@knowledge({rel})")
572
+
573
+ # ═══════════════════════════════════════════════════════════
574
+ # 7b. MEMORY RETRIEVAL (cross-session context)
575
+ # ═══════════════════════════════════════════════════════════
576
+ if get_feature_flag('memory') and budget_ok():
577
+ try:
578
+ try:
579
+ from hooks._memory import search_memories
580
+ except ImportError:
581
+ _memory = importlib.import_module("_memory")
582
+ search_memories = _memory.search_memories
583
+ # Reuse keywords already extracted for knowledge search
584
+ _kws_local = locals().get("kws")
585
+ _mem_kws = list(_kws_local) if _kws_local else []
586
+ if _mem_kws:
587
+ mem_context = search_memories(project_dir, _mem_kws, max_results=3, max_chars=200)
588
+ if mem_context:
589
+ add(f'@memory: {mem_context}')
590
+ except Exception:
591
+ pass
592
+
593
+ # ═══════════════════════════════════════════════════════════
594
+ # 8. ERROR LOOP PREVENTION
595
+ # ═══════════════════════════════════════════════════════════
596
+ STUCK_SIGNALS = ["stuck", "same error", "keep getting", "tried everything",
597
+ "doesn't work", "막혀", "안돼", "실패", "같은에러", "모르겠"]
598
+ if any(signal_matches_text(sig, prompt) for sig in STUCK_SIGNALS) and budget_ok():
599
+ tp = os.path.join(state_dir, "ledger", "failure-tracker.json")
600
+ ctx = ""
601
+ active = {}
602
+ if os.path.exists(tp):
603
+ try:
604
+ with open(tp, "r") as f:
605
+ t = json.load(f)
606
+ active = {k: v.get("count", 0) for k, v in t.items()
607
+ if isinstance(v, dict) and v.get("count", 0) >= 2}
608
+ if active:
609
+ top = sorted(active.items(), key=lambda x: -x[1])[:2]
610
+ ctx = f" ({', '.join(f'{k[:25]}×{c}' for k,c in top)})"
611
+ except Exception:
612
+ pass
613
+ # Only inject if there are ≥2 tracked failures (not just keyword match)
614
+ if active:
615
+ # Dedup: skip if @stuck was injected within last 60 seconds
616
+ ts_path = os.path.join(state_dir, ".last-stuck-ts")
617
+ now = time.time()
618
+ should_inject = True
619
+ try:
620
+ if os.path.exists(ts_path):
621
+ with open(ts_path, "r") as f:
622
+ last_ts = float(f.read().strip())
623
+ if now - last_ts < 60:
624
+ should_inject = False
625
+ except (ValueError, OSError):
626
+ pass # Corrupt file → allow injection
627
+ if should_inject:
628
+ try:
629
+ os.makedirs(os.path.dirname(ts_path) or ".", exist_ok=True)
630
+ with open(ts_path, "w") as f:
631
+ f.write(str(now))
632
+ except OSError:
633
+ pass
634
+ add(f"@stuck{ctx}: STOP retrying. /OMG:escalate codex | different approach | ask user")
635
+
636
+ # ═══════════════════════════════════════════════════════════
637
+ # 9. WRITE/EDIT FAILURE AWARENESS (anti-hallucination)
638
+ # ═══════════════════════════════════════════════════════════
639
+ # When hook errors or write/edit failures occur, Claude often claims success.
640
+ # Detect error patterns and inject a verification requirement.
641
+ WRITE_ERROR_SIGNALS = [
642
+ "hook error", "error editing file", "error writing file",
643
+ "error: pretooluse", "error: posttooluse",
644
+ "security warning", "security_reminder",
645
+ "⚠️", "xss", "innerhtml", "xss vulnerabilit",
646
+ "hook 에러", "파일 수정 에러", "파일 쓰기 에러",
647
+ ]
648
+ if any(signal_matches_text(sig, prompt) for sig in WRITE_ERROR_SIGNALS) and budget_ok():
649
+ add(
650
+ "@write-verify: Hook/Write/Edit error detected in conversation. "
651
+ "BEFORE claiming success: READ the target file to verify changes are present. "
652
+ "If file unchanged → retry with different method (Edit, Bash heredoc, or cat >). "
653
+ "NEVER say 'updated successfully' without reading the file first."
654
+ )
655
+
656
+ # ═══════════════════════════════════════════════════════════
657
+ # OUTPUT
658
+ # ═══════════════════════════════════════════════════════════
659
+ try:
660
+ tool_count, _threshold, is_high_pressure = estimate_context_pressure(project_dir)
661
+ if is_high_pressure:
662
+ add(f"@context-pressure: High context usage detected ({tool_count} tool calls). Auto-saving state...")
663
+ except Exception:
664
+ pass
665
+
666
+ if injections:
667
+ output = "\n".join(injections)
668
+ if len(output) > MAX_CHARS:
669
+ output = output[:MAX_CHARS - 3] + "..."
670
+ json.dump({"contextInjection": output}, sys.stdout)
671
+
672
+ sys.exit(0)