Files
brain/user-level-files/hooks/economy-mode.py
T
Дмитрий ed9bade863 feat: extract brain artifacts from Liderra + ~/.claude/
project-files/:
- CLAUDE.md.template (266 lines)
- docs/Pravila_raboty_Claude.template.md (720 lines)
- docs/Plugin_stack_rules.template.md (916 lines)
- docs/Tooling.template.md (613 lines)
- docs/CHANGELOG_claude_md.template.md
- docs/visualizations/hooks-skills-plugins-map.html (3122 lines)
- .mcp.json.template (universal: playwright/github/semgrep; laravel-boost dropped)

user-level-files/:
- hooks/ (10 Python files: skill-marker, skill-check, economy-* x8)
- settings-fragment.json (enabledPlugins + permissions + hooks only)
- marketplaces.json (3 sources)
- plugins-manifest.json (4 plugins pinned with gitCommitSha)
- mcp-user.template.json (magic with <<MAGIC_API_KEY>> placeholder)

Gitleaks scan: 0 findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:46:51 +03:00

308 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""UserPromptSubmit hook: parses 'экономия N%' from user prompt and
injects behavioral rules for that economy level. Also requires Claude
to announce the level as the first line of the response.
Levels are anchored at 0 / 25 / 50 / 75 / 100. Arbitrary integer N% is
mapped to the nearest anchor. Default (no keyword) is 100%.
v2 robustness fixes (over v1):
- Russian inflection: matches all 6 forms (экономия/и/ю/ей/иями)
- Separators: \\s, :, ,, -, =, (, ), [, ], em-dash, en-dash
- Decimal numbers: 75.5%, 75,5%, 75.0% all parse correctly
- Discussion guard: 'не активируй', 'забудь', 'отбой', 'пример',
'как работает', 'что даст/покрывает/такое' — keyword prefix in 30
chars before match disqualifies that match
- Question guard: prompts ending in '?' = discussion (no activation)
- Multi-match: iterates from LAST to first, returns first non-discussion
match (handles 'не X, а Y' and 'X, потом Y' patterns)"""
import hashlib
import json
import os
import re
import sys
import tempfile
import time
try:
sys.stdin.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# ====================================================================
# Pattern components
# ====================================================================
# Russian inflections: все 6 форм слова «экономия»
_INFLECT = r"эконом(?:ия|ии|ию|ией|иями)"
# Separators between keyword and number: whitespace + common punctuation
# Includes em-dash (—) and en-dash (); hyphen at end of class to avoid
# the need for escaping.
_SEP = r"[\s:,()=\[\]—–-]*"
# Number: optional sign + digits + optional decimal (with . or , as separator)
_NUM = r"([+-]?\d+(?:[.,]\d+)?)"
# Optional whitespace then literal %
_PCT = r"\s*%"
PATTERN = re.compile(
r"\b" + _INFLECT + _SEP + _NUM + _PCT,
re.IGNORECASE,
)
# If any of these (lowercased) keywords appears within 30 chars BEFORE a
# match, that match is treated as discussion context (not activation).
DISCUSSION_PREFIXES = (
"не ", # «не активируй экономия 75%»
"не\t",
"не\n",
"забудь", # «забудь про экономия 75%»
"отключи",
"отбой", # «отбой экономия 75%»
"пример", # «пример: экономия 75%»
"как работает",
"как работают",
"что даст",
"что дают",
"что покрывает",
"что покрывают",
"что такое",
"что значит",
"вместо",
"никогда",
"не используй",
"не применяй",
)
# Clause boundaries — punctuation that separates independent clauses.
# Note: ':' is intentionally NOT included so 'пример: экономия 75%' is
# correctly treated as discussion (the keyword 'пример' precedes the colon).
_CLAUSE_BOUNDARIES = (",", ".", ";", "", "", "?", "!", "\n")
def _is_question(prompt: str) -> bool:
return prompt.rstrip().endswith("?")
def _last_clause(prefix: str) -> str:
"""Return the text after the last clause boundary in `prefix`.
Used to avoid negation in earlier clause leaking into discussion check
of a later match (e.g. 'не X, а Y' — the 'не' belongs to clause 1)."""
last_idx = -1
for sep in _CLAUSE_BOUNDARIES:
idx = prefix.rfind(sep)
if idx > last_idx:
last_idx = idx
if last_idx < 0:
return prefix
return prefix[last_idx + 1 :]
def _has_discussion_prefix(prompt: str, match_start: int) -> bool:
raw_prefix = prompt[max(0, match_start - 30) : match_start].lower()
clause = _last_clause(raw_prefix)
return any(kw in clause for kw in DISCUSSION_PREFIXES)
def parse_level(prompt: str):
"""Return int 0..100 if user explicitly activated a level, else None.
NEW (v3): match must be at end of prompt — only whitespace + light punct
after. Handles user's writing style: directive at end as trailer."""
if not prompt:
return None
matches = list(PATTERN.finditer(prompt))
if not matches:
return None
# Take LAST match (user's directive position at end)
last = matches[-1]
# Check tail after match: only whitespace + light punctuation allowed
tail = prompt[last.end():]
if not re.fullmatch(r"[\s.!?)\]]*", tail):
return None # match not at end → discussion/description
# Backup discussion guard for last match (e.g. "что покрывает экономия 0%" alone)
if _has_discussion_prefix(prompt, last.start()):
return None
try:
num_str = last.group(1).replace(",", ".")
num = float(num_str)
return max(0, min(100, int(round(num))))
except (ValueError, TypeError):
return None
# ====================================================================
# Levels
# ====================================================================
LEVELS = {
100: {
"label": "100%",
"tail": "по умолчанию, все паттерны активны",
"rules": [
"Текущее умолчание поведения. Никаких добавочных требований.",
"Все жёсткие, мета и системные паттерны экономии — активны.",
],
},
75: {
"label": "75%",
"tail": "жёсткие и мета OFF",
"rules": [
"ЖЁСТКИЕ ПАТТЕРНЫ ВЫКЛЮЧЕНЫ на эту задачу:",
"- НЕ заявлять 'passed/готово/работает/прошло' без реального Bash-запуска тестов/линта/команды.",
"- НЕ cherry-pick'ать результаты: формулировка вида '498/500 passed' = выписать оба failure'а явно, не маскировать как 'тесты прошли'.",
"- НЕ anchor'иться на первой гипотезе при debug — сгенерировать минимум 2 альтернативы перед патчем.",
"- НЕ premature closure: claim 'готово' только после evidence (запуск с exit code 0 + проверка output).",
"- НЕ скипать brainstorming на новой фиче, если задача попадает под Pravila §12.2.",
"МЕТА-ПАТТЕРН ВЫКЛЮЧЕН:",
"- Тихая верификация == видимой. То, что не показано пользователю, всё равно должно быть сделано.",
"СИСТЕМНЫЕ паттерны остаются активны: Grep head_limit, Read с offset/limit на больших файлах, subagent summary, доверие memory без re-Read'а.",
],
},
50: {
"label": "50%",
"tail": "жёсткие/мета OFF + критичные системные",
"rules": [
"Все правила уровня 75% +",
"На критичных решениях verify memory (re-Read актуального файла, не доверять stale).",
"На debug всегда минимум 2 гипотезы (фактически = systematic-debugging skill).",
"Тестовый output: показывать full в ответе, не саммари.",
"Subagent: на критичных задачах прочитать raw output вручную, не только summary.",
],
},
25: {
"label": "25%",
"tail": "минимальная экономия, verify по умолчанию",
"rules": [
"Все правила уровня 50% +",
"verification-before-completion skill вызывается на любой задаче в 2 и более шагов (даже без явного 'verify' от пользователя).",
"Read с offset/limit — только на файлах >5000 строк.",
"Grep head_limit поднять до 500 (вместо 250).",
"Subagent — только на гарантированно независимых задачах; в остальных случаях прямой Read.",
],
},
0: {
"label": "0%",
"tail": "максимальное всеобъемлющее качество, без любых скипов",
"rules": [
"ВСЕ паттерны экономии ВЫКЛЮЧЕНЫ. ОБЯЗАТЕЛЬНЫЕ требования на каждое действие в этой задаче:",
"",
"ПРОЦЕСС:",
"- Multi-step задача (≥3 шага): EnterPlanMode/writing-plans skill ПЕРВЫМ, согласовать с пользователем до выполнения.",
"- Любой debug / unexpected behavior: superpowers:systematic-debugging с минимум 3 гипотезами; falsify каждую перед фиксом.",
"- Любая creative задача (фича/компонент/endpoint/нетривиальный refactor): superpowers:brainstorming ПЕРВЫМ.",
"- Перед claim 'готово'/'closed'/'merged'/'passed': обязательно invoke superpowers:verification-before-completion.",
"- TDD на любой код: superpowers:test-driven-development; failing test first, GREEN после.",
"",
"ЧТЕНИЕ И ИССЛЕДОВАНИЕ:",
"- Full file reads без offset/limit на файлах до 5000 строк.",
"- Grep без head_limit (или явно 0 = unlimited) на критичных поисках; default 500.",
"- Memory facts: всегда re-Read актуального файла ПЕРЕД использованием; не доверять stale memory.",
"- Перед задачей касающейся проекта: re-Read CLAUDE.md и Pravila на начало.",
"- Subagent: запрашивать raw output, не summary; решения принимать самому.",
"",
"ВЕРИФИКАЦИЯ:",
"- После КАЖДОГО Edit/Write на code — запуск relevant тестов (Pest/Vitest по контексту).",
"- После КАЖДОГО изменения миграции/схемы — db tests + smoke check.",
"- Перед коммитом — full pre-commit run (lefthook stages включая gitleaks-full-history + lychee + larastan + pint + pest).",
"- Bash output показывать ВСЕГДА в ответе, не только при ошибке.",
"- Full test output, не саммари; failure'ы выписывать явно с file:line.",
"",
"ФОРМУЛИРОВКИ:",
"- Никаких 'should work' / 'looks correct' / 'тесты должны пройти' без реального запуска.",
"- Никакого cherry-picking: 'tests pass' = ровно столько, сколько прошло; остальное — failed с указанием.",
"- Каждое утверждение про код — с file:line как pin'ом, не общей фразой.",
"- Если что-то не проверено — явно 'не верифицировал X' в разделе ограничений.",
"",
"ОТКРЫТЫЕ ВОПРОСЫ И ИНТЕГРАЦИЯ:",
"- Перед закрытием темы из реестра (Б-/CTO-/DO-/Ю-/Диз-/OPEN-) — проверить наличие явного 'закрываем' от заказчика; иначе вопрос остаётся открытым.",
"- Атомарные коммиты: один логический change → один коммит.",
],
},
}
def closest_level(pct: int) -> int:
return min(LEVELS.keys(), key=lambda lv: abs(lv - pct))
def main() -> None:
try:
data = json.load(sys.stdin)
except Exception:
return
prompt = data.get("prompt") or ""
raw_pct = parse_level(prompt)
if raw_pct is not None:
level = closest_level(raw_pct)
explicit = True
else:
level = 100
explicit = False
# NEW (v3): write state file for sibling hooks (state-guard, verifier, postcompact)
sid = data.get("session_id")
if sid:
state_path = os.path.join(tempfile.gettempdir(), f"claude-economy-{sid}.json")
if level == 100 and not explicit:
# Default — remove state to signal no active mode
try:
if os.path.exists(state_path):
os.remove(state_path)
except OSError:
pass
else:
state = {
"session_id": sid,
"level": level,
"label": LEVELS[level]["label"],
"tail": LEVELS[level]["tail"],
"set_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"set_by_prompt_hash": hashlib.sha256(prompt.encode("utf-8")).hexdigest()[:12],
}
try:
# Atomic write via tempfile + replace
tmp = state_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(state, f)
os.replace(tmp, state_path)
except Exception:
pass
spec = LEVELS[level]
rules_block = "\n".join(spec["rules"])
explicit_note = (
"(пользователь указал явно)"
if explicit
else "(default — пользователь не указал уровень)"
)
ctx = (
f"=== ECONOMY MODE: {spec['label']} {explicit_note} ===\n\n"
f"ПЕРВОЙ строкой ответа на эту задачу обязательно написать:\n"
f" `экономия: {spec['label']}{spec['tail']}`\n\n"
f"ИНСТРУКЦИИ для этой turn:\n{rules_block}\n\n"
f"Действует только на текущую задачу — следующий промпт парсится заново. "
f"§12 hard rule из Pravila НЕ override-ится этим режимом — на всех уровнях."
)
out = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": ctx,
}
}
try:
sys.stdout.write(json.dumps(out, ensure_ascii=True))
except Exception:
return
if __name__ == "__main__":
main()