Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .claude/agents/max-transcriber.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ memory: project

Ты — агент транскрипции голосовых из мессенджера Max (web.max.ru). Язык — русский.

## Chrome CDP — ОБЯЗАТЕЛЬНЫЙ FLOW
## Сначала — нативная кнопка `→T`

В UI Max возле голосового есть нативная кнопка транскрибации (`→T`). **Если она доступна — используй её, она работает быстрее CDP+whisper pipeline на порядки**. Через Playwright: `browser_snapshot` → найди кнопку у нужного аудио → `browser_click`. Текст появится в чате.

Только если нативная кнопка не сработала или недоступна — переходи к Chrome CDP flow ниже.

## Chrome CDP — ОБЯЗАТЕЛЬНЫЙ FLOW (fallback)

1. **Проверь CDP:**
```powershell
Expand Down
11 changes: 8 additions & 3 deletions .claude/commands/diary.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
```bash
DIARY_DIR=$(py -3.12 scripts/per_user_dir.py --sub memory/diary)
py -3.12 -c "
import os, pathlib, datetime, sys
import os, pathlib, datetime, sys, re
d = pathlib.Path(sys.argv[1])
d.mkdir(parents=True, exist_ok=True)
date = datetime.datetime.now().strftime('%Y-%m-%d')
existing = sorted(int(f.name.split('_')[0]) for f in d.glob('*_*.md') if f.name[:3].isdigit())
start = (existing[-1] + 1) if existing else 1
# Parse leading digits only — tolerant to suffixes like '001b_*.md' if any.
nums = []
for f in d.glob('*_*.md'):
m = re.match(r'^(\d+)', f.name)
if m: nums.append(int(m.group(1)))
nums.sort()
start = (nums[-1] + 1) if nums else 1
for n in range(start, start + 1000):
p = d / f'{n:03d}_{date}.md'
try:
Expand Down
9 changes: 9 additions & 0 deletions docs/learned-the-hard-way.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ bash scripts/ops.sh test # должно показать 116 passed (104 + 12)
scripts/.claude/
```
И запускать `claude` только из root репо.

### Удалил файл хука — фоновый спам «File not found» до конца сессии
**Симптом:** после `rm .claude/hooks/<name>.py` каждое следующее действие генерирует фоновую ошибку `File not found` или подобную, даже если хук не нужен по логике задачи.
**Причина:** Claude Code кеширует `settings.json` на время жизни сессии. Удалённый файл всё ещё ссылается из кеша.
**Фикс (выбрать один):**
- **Stub-вариант:** заменить файл на no-op (`#!/usr/bin/env python3\nimport sys; sys.exit(0)`), удалить позже после `/clear`
- **Clean-вариант:** убрать запись из `settings.json` → удалить файл → `/clear` → новая сессия
**Урок:** связка `settings.json` + удаление файла хука требует `/clear`, не на лету.
**Источник:** diary 028, dreaming-2026-05-08.
**Источник:** diary 020.

### Playwright MCP дампит скрины и кеш в cwd
Expand Down
42 changes: 39 additions & 3 deletions scripts/atomic_context_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

Expand All @@ -30,6 +31,36 @@
import fcntl


def _atomic_write(target: Path, content: str) -> None:
"""Write content to target atomically: temp file in same dir + os.replace.

Why: ``Path.write_text`` opens the target in ``w`` mode which truncates it
BEFORE writing. If anything fails between truncate and write (process
crash, OS kill, disk full, even Antivirus scan delay) the target is left
at 0 bytes. ``os.replace`` swaps inode atomically — observers either see
the old content or the new, never empty.
"""
target.parent.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(target.suffix + f".tmp.{os.getpid()}")
try:
# Use binary write + explicit utf-8 + fsync so the bytes are durable
# before we replace. Without fsync, a power loss right after replace
# could leave us with a renamed-but-empty file on some filesystems.
with open(tmp, "wb") as fp:
fp.write(content.encode("utf-8"))
fp.flush()
os.fsync(fp.fileno())
os.replace(tmp, target) # atomic on POSIX; atomic on NTFS since Vista
except Exception:
# Best-effort cleanup of orphan temp on failure
try:
if tmp.exists():
tmp.unlink()
except OSError:
pass
raise


def with_lock_append(target: Path, new_content: str) -> None:
"""Атомарно прочитать target, append new_content, записать обратно."""
target.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -38,7 +69,7 @@ def with_lock_append(target: Path, new_content: str) -> None:
_acquire(lock_fp)
try:
existing = target.read_text(encoding="utf-8") if target.exists() else ""
target.write_text(existing + new_content, encoding="utf-8")
_atomic_write(target, existing + new_content)
finally:
_release(lock_fp)

Expand All @@ -50,7 +81,7 @@ def with_lock_replace(target: Path, new_content: str) -> None:
with open(lock_path, "w", encoding="utf-8") as lock_fp:
_acquire(lock_fp)
try:
target.write_text(new_content, encoding="utf-8")
_atomic_write(target, new_content)
finally:
_release(lock_fp)

Expand Down Expand Up @@ -95,7 +126,12 @@ def main() -> int:
args = parser.parse_args()

target = Path(args.path)
new_content = sys.stdin.read()
# Read stdin as bytes and decode UTF-8 explicitly. Without this, Python on
# Windows decodes stdin via the console codepage (cp1251 on RU locale),
# which mangles UTF-8 input into surrogate pseudo-chars that then fail to
# re-encode on `write_text(encoding="utf-8")`. Doing this here means callers
# don't need to set PYTHONIOENCODING=utf-8 every time.
new_content = sys.stdin.buffer.read().decode("utf-8")

if args.mode == "append":
with_lock_append(target, new_content)
Expand Down
137 changes: 137 additions & 0 deletions tools/dreaming/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Cortex Dreaming

> Weekly meta-analysis: что **сам Claude Code** и его **субагенты** делали за неделю. Шлёт корпус в Gemini 3.1 Pro, получает паттерны и предложения правил для `CLAUDE.md`, `~/.claude/CLAUDE.md`, `.claude/agents/*.md`, skills и hooks. Пишет в `runtime/dreaming/YYYY-MM-DD.md`. **Dry-run only — никогда не патчит код.** Ты читаешь и решаешь сам.

## Что делает

Собирает 3 типа источников:

1. **Subagent runs** — `.claude/agent-memory/<agent>/{lessons.md, MEMORY.md}` (что делали субагенты, auto от `subagent-stop.py` хука + ручные заметки)
2. **Session diary** — `<USER_PROJECT_DIR>/memory/diary/*.md` за окно (что делал сам Claude Code в сессиях; per-user файлы, не в git)
3. **Existing reflections** — `<USER_PROJECT_DIR>/memory/reflections/*.md` все (для anti-duplication, чтобы не предлагать то что уже синтезировано)

Шлёт корпус в Gemini, получает структурированный отчёт:
- **Findings** — повторяющиеся ошибки, слепые пятна, незаписанные знания, workflow-improvements, дрейф правил
- **Факты** — стабильные знания для записи в memory
- **Кандидаты на дискуссию** — наблюдения с trade-off, нужно обсудить
- **Скип** — если данных мало

## Запуск вручную

```bash
cd tools/dreaming
uv sync # первый раз
uv run python dreaming.py # окно 7 дней
uv run python dreaming.py --days 30 # окно 30 дней
uv run python dreaming.py --dry # без API-вызова, печатает промпт
```

## Что нужно

- `GOOGLE_API_KEY` (или `GEMINI_API_KEY`) в `.env` репо. Скрипт находит `.env` walk-up из любого worktree.
- Хотя бы один источник данных:
- `.claude/agent-memory/<agent>/{lessons.md,MEMORY.md}`, ИЛИ
- `<USER_PROJECT_DIR>/memory/diary/*.md` (создаётся через `/diary` или `/handoff`), ИЛИ
- `<USER_PROJECT_DIR>/memory/reflections/*.md` (создаётся через `/reflect`)
- Если все три пустые — скрипт скажет «skipping» и выйдет с кодом 0.

## Резолвинг путей

Скрипт ищет project root в таком порядке:

1. `CLAUDE_PROJECT_DIR` env var (Claude Code устанавливает автоматически)
2. Parent директория ближайшего `.env` (walk-up из `tools/dreaming/`)
3. `tools/dreaming/../..` (последний fallback)

Это значит: запускай из любого worktree — отчёт всё равно ляжет в `runtime/dreaming/` главного репо. Per-user диру (`~/.claude/projects/<id>/memory/`) скрипт вычисляет сам по project root.

## Расписание (раз в неделю, воскресенье 22:00 МСК)

Вариант 1 — **Windows Task Scheduler** (рекомендуется, не зависит от Claude Code):

```powershell
# Запусти от имени твоего юзера (не админа)
$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-NoProfile -WindowStyle Hidden -Command `"cd 'D:\code\2026\2\cortex\tools\dreaming'; uv run python dreaming.py 2>&1 | Out-File -Append -Encoding utf8 'D:\code\2026\2\cortex\runtime\dreaming\_run.log'`""

$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 22:00

$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-DontStopIfGoingOnBatteries `
-AllowStartIfOnBatteries

Register-ScheduledTask `
-TaskName "Cortex Dreaming Weekly" `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Description "Weekly meta-analysis of subagent runs"
```

Проверка:

```powershell
Get-ScheduledTask -TaskName "Cortex Dreaming Weekly"
Start-ScheduledTask -TaskName "Cortex Dreaming Weekly" # запуск вручную для теста
```

Удалить:

```powershell
Unregister-ScheduledTask -TaskName "Cortex Dreaming Weekly" -Confirm:$false
```

`-StartWhenAvailable` нужен чтобы если комп был выключен в воскресенье 22:00 — задача запустилась когда включишь.

## Что в отчёте

```markdown
## Findings

### 1. [короткое название паттерна, 5-8 слов]
- **Тип**: ошибка | слепое пятно | незаписанное знание | workflow-improvement | дрейф правил
- **Где**: agent/file ИЛИ diary/NNN ИЛИ конкретный путь
- **Сколько раз**: N (или диапазон)
- **Что**: 2-3 предложения
- **Предлагаемое правило**: 1-2 строки
- **Куда положить**: точный путь (CLAUDE.md / ~/.claude/CLAUDE.md / .claude/agents/<name>.md / .claude/skills/<skill>/SKILL.md / .claude/hooks/<name>.py / <USER_PROJECT_DIR>/memory/feedback_*.md / новый файл)
- **Уверенность**: высокая / средняя / низкая

## Факты для записи (без правила, просто знание)
- ...

## Кандидаты на дискуссию (не делать сразу, обсудить)
- ...

## Скип
- (если данных мало — почему пропускаем)
```

Утром в понедельник открыл, прочитал, либо применил руками, либо нет.

## Когда переключать на auto-apply

Первые 2-4 недели — **только dry-run**. Если за этот период:

- ≥80% предложений ты принимаешь
- Нет случаев «предложение всё сломало бы»
- Чувствуешь что лень вручную применять

→ можно расширить скрипт: автоматически открывать PR с diff'ом против `CLAUDE.md` / соответствующего файла. Никогда не коммитить в main без ревью.

## Стоимость

Gemini 3.1 Pro preview: $1.25 / 1M input tokens, $10 / 1M output. Типичный корпус 100KB (~25K input + ~1500 output) → один прогон ≈ $0.05. Год еженедельно ≈ $2.6.

## Ограничения

- **Зависит от компа**: запускается через Task Scheduler, нужно чтобы комп был включён в окно. `-StartWhenAvailable` догоняет пропущенные запуски при ближайшем включении.
- **Качество данных**: текущий `subagent-stop.py` хук пишет в `_unknown/` потому что не определяет `subagent_type`. Это влияет на качество per-agent анализа, но `_unknown` плюс diary дают паттерны. Чинить хук — отдельная задача.
- **Дедупликация имени файла**: если запустишь дважды за день — второй прогон перезатрёт первый (имя файла = дата). Если хочешь два отчёта за день — переименуй первый.
- **Per-user folder**: diary и reflections живут в `~/.claude/projects/<id>/memory/` (per-user, не в git). Для open-source форка — это значит что новый клонировавший репо не получит истории Никиты, но сможет наблюдать паттерны со своих сессий с момента активации.

## Почему Gemini, а не Claude API

`ANTHROPIC_API_KEY` не настроен в `.env`, а `GOOGLE_API_KEY` уже используется в `tools/tg-monitor/`. Gemini 3.1 Pro даёт сравнимое качество анализа на этой задаче и в 8x дешевле. Если в будущем понадобится Claude — поменять `GEMINI_MODEL` на anthropic call в `dreaming.py` (~10 строк).
Loading