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