---
project: vibe-coding
type: plan
date: 2026-05-30
status: ready-to-plan
preflight_run: 2026-05-31
supersedes:
  - "{vibe-coding} {plan} ticktick-bot design – 2026-04-10.md"
  - "{vibe-coding} {plan} tasks-bot obsidian – 2026-04-29.md"
---

# tasks-bot 2.0 — переезд в TickTick

## Контекст и почему переделка

Текущий бот (`tasks-bot` v1) хранит задачи в Obsidian-вальте через промежуточную SQLite + GitHub-bridge. Работает, но:

- **Bulk-операции и переносы неудобные.** Чтобы перепланировать неделю или переместить несколько задач между проектами, надо много команд в чате. В TickTick это делается мышкой за секунды.
- **Идеи для постов теряются.** Нет места для коротких инсайтов «надо написать про X». В Obsidian-файлах с задачами они смотрятся не на своём месте.
- **Разговорный режим не сработал.** LLM плохо понимает свободные команды («перенеси X на завтра», «закрой Y»). Часть проблемы — слишком широкая зона ответственности LLM.

**Решение:** база правды переезжает в TickTick. Там удобный мобильный UI для bulk-операций, поддержка проектов и описаний, дедлайны, нативные напоминания. Telegram-бот остаётся как **быстрая клавиатура в TickTick** — это всё ещё самый быстрый способ записать мысль (чат всегда открыт).

## Цели

1. Бот пишет задачи **напрямую в TickTick** через API, маршрутизируя в нужный проект+список по детерминированным правилам.
2. **Команды** (`/done`, `/defer`, `/text`, `/date`, `/proj`) делают то же, что и сейчас, но пишут в TickTick.
3. **Сводки** в Telegram (утро 9:00, вечер 21:00, воскресенье 18:00 TZ Екатеринбург) остаются, но читают из TickTick.
4. LLM работает в **одном узком месте** — извлекает из сообщения содержание (project, content, due, platform_hint, strategic_flag). В какой список класть — простой `if` поверх извлечённого. Если LLM не уверена в проекте — спрашивает inline-кнопками.
5. Сохранение завершённых задач в архив Obsidian раз в месяц.

## Не-цели (v2.0)

- Голосовой ввод (отложен на v2.1).
- Разговорный режим в чате (выпилен полностью).
- Управление структурой TickTick из бота (создание проектов, списков, тегов).
- Двусторонний реал-тайм sync (изменения в TickTick UI не отражаются в Telegram до следующей сводки — это ок).

## Архитектура

```
┌──────────────────────────────────┐
│  Telegram                        │
│  • текст (захват)                │
│  • inline-кнопки (уточнение)     │
│  • сводки 9 / 21 / вс            │
│  • /done /defer /text /date /proj│
└──────────────┬───────────────────┘
               │
               ▼
┌──────────────────────────────────┐
│  Raspberry Pi: tasks-bot 2.0     │
│                                  │
│  • capture handler:              │
│    - LLM извлекает project,      │
│      content, due, platform_hint,│
│      strategic_flag, confidence  │
│    - routing в список: if-rules  │
│    - confidence ≥ порог → write  │
│    - confidence < порог → ask    │
│    - таймаут на ответ → fallback │
│      в TickTick inbox            │
│                                  │
│  • command handler:              │
│    /done /defer /text /date      │
│    /proj → прямые вызовы API     │
│                                  │
│  • summary scheduler:            │
│    cron → читает TickTick →      │
│    форматирует → Telegram        │
└──────────────┬───────────────────┘
               │ TickTick API (OAuth2)
               ▼
┌──────────────────────────────────┐
│  TickTick — база правды          │
│  Папки: Mars, Channel,           │
│  Consulting, Life, Vibe Coding   │
│  + системный Inbox (fallback)    │
│  Внутри — списки (см. раздел     │
│  «Структура TickTick»)           │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│  Mac: monthly-archive.py         │
│  (cron, 1-го числа каждого мес.) │
│  • читает TickTick: закрытое за  │
│    прошлый месяц                 │
│  • пишет markdown по проектам в  │
│    Obsidian-вальт                │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│  Одноразовый migration.py        │
│  • читает текущий tasks.json     │
│  • создаёт открытые задачи в     │
│    TickTick по проектам          │
│  • помечает старые как мигрир.   │
└──────────────────────────────────┘
```

## Компоненты

### Capture handler

**Назначение:** обработать входящее текстовое сообщение → создать задачу в TickTick в нужном проекте/списке.

**Что LLM извлекает:**
- `project`: `mars` | `channel` | `consulting` | `life` | `vibe-coding` | `inbox`
- `content`: чистый текст задачи (без меток проекта/срока)
- `due`: ISO дата или null (TZ Екатеринбург, см. «Время и часовые пояса»)
- `platform_hint`: `threads` | `instagram` | `telegram` | null (только если `project=channel`)
- `strategic_flag`: bool (только если `project in {mars, vibe-coding}`)
- `project_confidence`: 0..1

**Поведение:**
1. LLM извлекает поля.
2. По правилам routing (см. «Структура TickTick») детерминированно выбирается **список** внутри проекта.
3. Если `project_confidence ≥ 0.7`:
   - вызывает TickTick API `create_task` в выбранном проекте/списке
   - отвечает: `✓ создано в {project} / {list}{, срок YYYY-MM-DD}`
4. Если `project_confidence < 0.7`:
   - сохраняет «pending capture» в локальном кэше (TTL 5 минут)
   - отправляет inline-кнопки выбора проекта: `[mars] [channel] [consulting] [life] [vibe-coding] [📥 inbox]`
   - после нажатия — применяет routing-правила с уже извлечёнными `due`, `platform_hint`, `strategic_flag`, создаёт задачу
   - на таймаут — создаёт в TickTick Inbox, отвечает: `↳ положила в inbox, не успела уточнить`

**Про confidence для платформы и стратегии.** Отдельной кнопочной развилки для `platform_hint` и `strategic_flag` нет. Если LLM не уверена — флаг `false` / hint `null`, задача падает по правилу в `Бэклог` или `To Do`. Проще руками перетащить в `Threads` в TickTick, чем подтверждать каждый раз. Эти поля — best effort, не гарантия.

**Зависимости:** OpenRouter (LLM), TickTick API, Telegram Bot API.
**Интерфейс:** обработчик `MessageHandler(filters.TEXT)`.

### Command handler

**Назначение:** структурированные команды для работы с TickTick из чата.

**Команды (минимальный набор):**

| Команда | Что делает |
|---------|------------|
| `/today` | Запрашивает on-demand утреннюю сводку (тот же набор, что у 09:00 scheduled — без Inbox) |
| `/inbox` | Read-only показывает TickTick Inbox (задачи без проекта) |
| `/backlog [project]` | Read-only показывает `Бэклог` и `Стратегия` (по проекту или по всем). Не входит в scheduled-сводки |
| `/done N` | Закрывает задачу N из последней свежей сводки |
| `/defer N <when>` | Переносит срок задачи N. `when`: `today`, `week`, `none`, `YYYY-MM-DD` |
| `/defer all` | Переносит все открытые сегодняшние задачи на завтра (кнопка в вечерней сводке) |
| `/text N <new>` | Меняет текст задачи N |
| `/proj N <project>` | Переносит задачу N в другой проект |
| `/date N <when>` | То же, что `/defer N` |
| `/del N` | Помечает как удалённую (TickTick: delete task) |

Номера `N` — из последней присланной сводки. Если последняя сводка устарела (новый день / `/today` не дёргался) — отвечает «Номера устарели, пришли /today».

**Маппинг N → TickTick task_id** хранится в кэше бота (in-memory + sqlite на случай рестарта Pi).

**Re-check перед мутацией.** Каждая мутационная команда (`/done`, `/defer`, `/text`, `/proj`, `/date`, `/del`) перед записью **сначала читает задачу из TickTick по `task_id`**, проверяет:
- задача существует (не удалена в UI);
- статус всё ещё «open» (для `/done` — иначе «уже закрыта»);
- для `/proj N <project>` — целевой проект существует.

Если проверка не прошла — отвечает понятным сообщением («задача уже закрыта в TickTick», «задача удалена», «проект не найден») и не делает запись. Это закрывает race между моментом отправки сводки и моментом исполнения команды (ты могла закрыть задачу мышкой в TickTick между ними).

**Routing при `/proj N <project>`.** После смены проекта **список пересчитывается по тем же routing-правилам**, что и в capture. Без `platform_hint` и `strategic_flag` (которые при `/proj` не указываются), правило сводится к: `due` есть → `To Do`, нет → `Бэклог`. Например, `/proj 3 mars` для задачи без срока положит её в `Mars / Бэклог`. Если хочется сразу в `Стратегия` — после `/proj` руками подвинуть в TickTick UI, либо ввести расширенный синтаксис `/proj 3 mars/Стратегия` позже (не в MVP).

### Summary scheduler

**Назначение:** по расписанию читать TickTick и присылать сводку.

**Расписание (TZ Asia/Yekaterinburg):**
- 09:00 — задачи из `To Do` с `due ≤ end_of_week` + платформенные с `due ≤ today`, с разметкой статуса
- 21:00 — что не закрыто из сегодняшней утренней сводки + кнопка «Перенести всё»
- Воскресенье 18:00 — **celebration**: что закрыто за неделю, с похвалой

**Утренняя сводка (09:00).** Включает:
- задачи из списков `To Do` (по всем проектам) с `due ≤ end_of_week` (конец текущей недели, воскресенье 23:59 Екб)
- задачи из платформенных списков Channel (`Threads`, `Instagram`, `Канал`) с `due ≤ today` (просроченные и сегодняшние scheduled-посты)

Задачи в `To Do` со сроком **дальше этой недели** (например, на месяц вперёд) в утреннюю сводку **не попадают** — иначе будущие задачи лезут в утро и заваливают. Их видно через `/today week+N` (опционально, если нужно — на этапе UI) или просто открыв TickTick.

Внутри проекта группируются по статусу:
- 🔴 overdue (`due < today`)
- 🔥 today (`due == today`)
- 📅 ahead (`due > today` ∧ `due ≤ end_of_week`)

**Что НЕ входит в утреннюю сводку:**
- `Бэклог` и `Стратегия` (no-due по определению) — смотришь через `/backlog [project]`
- Платформенные списки Channel **без срока** или с `due > today` — смотришь руками в TickTick
- Системный TickTick Inbox — смотришь через `/inbox` по запросу

**Вечерняя сводка (21:00).** Только незакрытые из утренней сводки этого дня (overdue/today). Кнопка «Перенести всё» переносит open today-задачи на завтра.

**Воскресная сводка (18:00) — celebration.** Не read-only обзор недели, а формат «что я закрыла»:

```
🎉 Неделя закрыта!

📁 mars (5):
  ✅ позвонить педагогу
  ✅ согласовать афишу
  ...

📁 channel (3):
  ✅ пост про хор (Канал)
  ...

📁 life (4):
  ...

Итого 12 задач за неделю. Молодец!
```

Источник: задачи со статусом completed за прошедшую неделю (Пн–Вс TZ Екб). Без сравнения с прошлой неделей, без нагрузки на планирование — просто радостный итог.

**Формат утренней сводки** — группировка по проектам, статусы эмодзи:
```
mars
  🔥 1. позвонить педагогу
  🔥 2. согласовать афишу
  🔴 3. сдать отчёт педагогам (был на вторник)

channel
  🔥 4. написать пост про хор (Threads)
  📅 5. редактура поста про лето (Канал, пт)

consulting
  🔥 6. созвон с клиентом

life
  📅 7. налоги (пт)
```

Группировка по проектам в фиксированном порядке: mars → channel → consulting → life → vibe-coding. Системный TickTick Inbox в scheduled-сводках не показываем (доступен через `/inbox`). В скобках рядом с задачей показываем список, если это не дефолтный `To Do` (`Threads`, `Канал` и т.п.) — чтобы было видно, в каком контексте задача.

### Migration (одноразовый)

**Скрипт:** `scripts/migrate_to_ticktick.py`, запускается вручную один раз.

**Шаги:**
1. Читает текущий `tasks.json` из data-репо.
2. Для каждой открытой задачи (status ≠ DONE/CANCELLED):
   - применяет routing-правила (project + due → list):
     - есть `due` → создаётся в `To Do`
     - нет `due` → создаётся в `Бэклог` (для Mars/Consulting/Life/Vibe Coding) или в `To Do` (для Channel, если в Channel нет Бэклог-эквивалента в исторических задачах)
   - создаёт через TickTick API в соответствующем проекте+списке
   - помечает оригинал как `MIGRATED`
3. Печатает отчёт: «создано N задач в TickTick по проектам/спискам {…}».
4. После проверки — оператор (Наташа) запускает остановку v1-сервиса и деплой v2.

**После миграции** старые Obsidian-файлы с задачами (`{mars} {plan} задачи – 2026-05.md` и т.п.) остаются как архив; новых записей в них не идёт. Mac-launchd-сервис v1 (`ru.dudina.tasks-bot.mac-sync.plist`) выключается.

### Monthly archive

**Скрипт:** `scripts/monthly_archive.py` на маке, запускается через launchd 1-го числа каждого месяца в 10:00 (TZ Екатеринбург).

**Риск:** этот компонент зависит от того, отдаёт ли TickTick Open API closed/completed задачи за период (особенно те, что закрыты руками в UI). Если **отдаёт** — используем напрямую (план А). Если **нет или с ограничениями** — fallback (план B).

**План А (если API поддерживает):**
1. Через TickTick API забирает все задачи, **закрытые** за прошлый календарный месяц (по TZ Екатеринбург).
2. Группирует по проектам, внутри проекта — по списку (`To Do`, `Бэклог`, `Стратегия`, `Threads`, `Instagram`, `Канал`).
3. На каждый проект пишет файл:

```
projects/mars/{mars} {source} архив задач – 2026-05.md
projects/channel/{channel} {source} архив задач – 2026-05.md
projects/consulting/{consulting} {source} архив задач – 2026-05.md
projects/life/{life} {source} архив задач – 2026-05.md
projects/vibe-coding/{vibe-coding} {source} архив задач – 2026-05.md
```

**Внутренний формат:**

```markdown
---
project: mars
type: source
date: 2026-05
---

# Архив задач — май 2026

## To Do

- ✅ позвонить педагогу — 2026-05-03
  > Описание из TickTick если было: «уточнить расписание июньских уроков»
- ✅ согласовать афишу — 2026-05-12

## Бэклог

- ✅ разобрать конкурсные заявки — 2026-05-20

## Стратегия

- ✅ план развития выпускников — 2026-05-15
  > Тезисы из description
```

Файл `channel` содержит секции по платформам (`Threads`, `Instagram`, `Канал`) и `Бэклог`. Файлы `consulting`, `life` — только `To Do` и `Бэклог`. Файлы `mars`, `vibe-coding` — все три (`To Do`, `Бэклог`, `Стратегия`). Если в каком-то списке за месяц ничего не закрыто — секция опускается.

**План B (fallback, если API не отдаёт closed tasks):**

Бот ведёт собственный **event log** — при каждой мутации (`create`, `complete`, `delete`, `update`) пишет запись в локальный `events.jsonl` на Pi:

```json
{"ts": "2026-05-23T14:32:10+05:00", "op": "complete", "task_id": "ttk_abc", "title": "налоги", "project": "life", "list": "To Do"}
```

**Где живёт архиватор в плане B.** Чтобы не возникала дырка «лог на Pi, скрипт на маке», в плане B архиватор тоже **живёт на Pi**:
- `scripts/monthly_archive.py` запускается на Pi через systemd-timer 1-го числа каждого месяца в 10:00 (TZ Екб).
- Скрипт читает `events.jsonl` за прошлый месяц, фильтрует по `op=complete`, группирует по `project + list`, генерирует markdown-файлы.
- Пушит результирующие md-файлы в data-репо `tasks_bot` (тот, что в плане А не нужен; в плане Б он остаётся живым ради этой задачи).
- На маке отдельный launchd-сервис пуллит data-репо в `_inbox/archive-staging/` Obsidian-вальта, оттуда ты переносишь файлы в `projects/{project}/` сама (или мы добавляем второй шаг — автокопирование в правильные папки).

Минус плана B остаётся: задачи, закрытые руками в TickTick UI (минуя бота), в архив **не попадут**. Это осознанный компромисс — если хочется чтобы что-то попало в архив, закрывай через `/done` в Telegram.

**Решение между А и Б** принимается по результатам preflight-spike (раздел «Preflight checks»).

### Эвалы

**Цель:** регрессионная защита LLM-классификатора в capture.

**Структура:**
- Файл `tests/evals/capture_cases.yaml` — список реальных примеров.
- Каждый пример: `{input: "...", expected: {project, due_kind, platform_hint?, strategic_flag?}}`, где `due_kind` ∈ `{none, today, tomorrow, week, specific}`, остальные опциональные (если применимы для проекта).
- `tests/test_capture_evals.py` — pytest-тест, помеченный `@live`, прогоняет каждый пример через реальный LLM-вызов и проверяет:
  - `project` совпал
  - `due_kind` (категория, выведенная из ISO-даты) совпал
  - `platform_hint` совпал (если ожидается)
  - `strategic_flag` совпал (если ожидается)
- Метрика: accuracy по каждому полю отдельно.

**Bootstrap:** при работе бота каждое классифицированное сообщение пишется в `logs/capture.jsonl` со структурой `{input, llm_output, confidence, user_accepted: bool}`. Раз в неделю Наташа просматривает 20–30 свежих записей в Claude Code и помечает «верно/неверно». Помеченные «неверно» — становятся новыми eval-кейсами.

**Когда прогонять:** перед каждым изменением промпта или модели LLM в capture. Цель — не допускать регрессий < 90% accuracy на `project` (главное поле).

**Confidence threshold:** стартуем с 0.7. Если eval-set покажет, что в 0.5–0.7 диапазоне модель почти всегда правa — поднимаем порог fallback вверх (меньше вопросов в чате). Если ниже 0.7 модель часто ошибается — понижаем порог (больше fallback в inbox / больше вопросов).

## Что остаётся от v1 (переиспользуем)

- **Pi-инфра:** Raspberry Pi, proxychains4 + Mihomo, systemd-юнит, SSH-ключи, passwordless sudo для рестарта.
- **GitHub-репо кода:** `tasks-bot-pi`, тот же. История коммитов сохраняется.
- **OpenRouter ключ:** тот же, модель `anthropic/claude-haiku-4.5` (можно перевыбрать после первых эвалов).
- **Telegram bot token:** тот же (плановая перевыдача отдельно — старый был засвечен в логах).
- **Структура кода:** пакетный layout `src/tasks_bot/...`, pytest, тесты.
- **Формат сводок и команд:** UX тот же, что в v1.

## Что выпиливается из v1

- **SQLite + `tasks.json`** — больше не нужны, база правды в TickTick.
- **GitHub data-репо `tasks_bot`** — в плане А **не используется** (всё через TickTick API). В плане Б monthly archive **остаётся живым только для доставки архивных markdown-файлов с Pi на Mac** (см. Monthly archive план B).
- **Mac launchd-сервис `ru.dudina.tasks-bot.mac-sync.plist`** — выключается, рендер markdown в вальт больше не нужен.
- **Conversational mode (`src/tasks_bot/conversational/`)** — удаляется. Свободный текст идёт в новый capture-handler, команды отдельно.
- **Автоклассификация на capture v1** — заменяется новой (с confidence + детерминированным routing-ом в список).
- **`/inbox` команда v1** (показывала задачи без проекта из локальной БД) — переосмысляется: в v2 `/inbox` — read-only показ TickTick Inbox.

## Время и часовые пояса

**Единственный TZ для всей системы — `Asia/Yekaterinburg` (UTC+5).**

- Все scheduled-сводки (09:00, 21:00, Вс 18:00) — в TZ Екатеринбург.
- LLM при парсинге дат («завтра», «пятница», «через неделю») интерпретирует относительно **текущего времени Екатеринбурга** (передаётся в промпт как контекст).
- При записи в TickTick `due` указывается в TZ Екатеринбург. Бот явно выставляет `timezone: "Asia/Yekaterinburg"` в запросах к TickTick API (если API поддерживает), иначе конвертирует в UTC корректно.
- При чтении из TickTick `due` интерпретируется в TZ Екатеринбург для категоризации (today / tomorrow / overdue).
- Архив за месяц — границы месяца тоже по TZ Екатеринбург.

В коде — один helper `now_yekb()` и `to_yekb(dt)`, используется везде, где нужно время. Никаких голых `datetime.now()` / `UTC` в бизнес-логике.

## Структура TickTick (locked)

Папки (folders) в TickTick = проекты бота. Внутри каждой папки — несколько списков с фиксированной семантикой.

```
📁 Mars        → To Do | Бэклог | Стратегия
📁 Channel     → To Do | Threads | Instagram | Канал | Бэклог
📁 Consulting  → To Do | Бэклог
📁 Life        → To Do | Бэклог
📁 Vibe Coding → To Do | Бэклог | Стратегия
📥 Inbox        (системный TickTick Inbox — fallback для непонятного)
```

### Семантика списков

| Список | Что в нём |
|---|---|
| **To Do** | Задачи в работе **со сроком** |
| **Бэклог** | Всё **без срока**, незрелое, «потом» |
| **Стратегия** | Идеи по стратегии (Mars и Vibe Coding) — без сроков, длинные |
| **Threads / Instagram / Канал** | Контент, привязанный к конкретной платформе (Channel) |

### Routing — детерминированный

LLM при capture извлекает только содержание (см. Capture handler); в какой список класть — простые правила:

```
if project == channel:
    if platform_hint in {threads, instagram, telegram}:
        list = соответствующий список (Threads / Instagram / Канал)
    elif due:
        list = "To Do"
    else:
        list = "Бэклог"

elif project in {mars, vibe-coding}:
    if strategic_flag:
        list = "Стратегия"
    elif due:
        list = "To Do"
    else:
        list = "Бэклог"

elif project in {consulting, life}:
    if due:
        list = "To Do"
    else:
        list = "Бэклог"

elif project == inbox:
    list = None  # системный TickTick Inbox без списка
```

Это значит: LLM **не угадывает «какой список»**. Она только извлекает содержание (`project`, `due`, `platform_hint`, `strategic_flag`), маршрутизация в список — детерминированная. Резко упрощает эвалы — проверяем только извлечение полей.

## Preflight checks (обязательно перед имплементацией)

Перед началом кодинга — короткий API-spike. Цель: убедиться, что TickTick Open API покрывает все нужные операции.

**Проверяемые операции (7):**

| № | Операция | Зачем |
|---|----------|-------|
| 0 | **folder/list-модель адресуема по ID** — listing папок (folders) и списков (lists) внутри них, получение их ID, создание задачи с привязкой к конкретному list_id | вся архитектура UX |
| 1 | `create` task в указанный список (project + list) | capture |
| 2 | `update due` (изменить дедлайн) | `/defer`, `/date` |
| 3 | `complete` (закрыть) | `/done` |
| 4 | `delete` | `/del` |
| 5 | `move project/list` (переместить между списками/папками) | `/proj`, ручные перемещения |
| 6 | `fetch completed for period` (получить закрытые за интервал) | monthly archive план A |

**Про №0 (важно):** в Open API термины могут отличаться от UI. То, что в UI называется «папка» — в API может быть `project`. То, что в UI «список» — может быть `project` с `parent_id`, или `list` внутри project, или совсем не существовать как адресуемая сущность (если списки — фронтенд-концепт). Если выяснится, что API оперирует **только плоскими `projects`**, наша структура «папка → 4 списка» эквивалентна 5 плоским projects (`Mars`, `Mars - To Do`, `Mars - Бэклог`, ...) или вложение через `parent_id`. Это **меняет именование и API-вызовы**, но не ломает UX. Лучше поймать до миграции.

> **Результат (2026-05-31, после probe_groups):** в API термины — `project/group` (UI «папка») и `project` (UI «список»). Связь через `groupId` на проекте. Endpoint `GET/POST /project/group` поддерживается. Структура «5 групп + 15 проектов» работает. См. раздел «Preflight Results» ниже.

**Формат spike:** один Python-скрипт `scripts/preflight_ticktick_api.py`:
- проходит OAuth, получает access token;
- по очереди выполняет каждую из 7 операций на тестовой структуре (создаёт тест-папку с 2 тест-списками, создаёт/двигает/закрывает задачу);
- печатает отчёт: `✓ supported` / `✗ not supported` / `⚠ partial: <детали>`.

**Решения, блокирующие имплементацию до спайка:**
- Если №0 показывает «папок нет, только flat projects» → структура переписывается на flat (5 проектов × несколько списков = ~15 plain projects, с naming-конвенцией `{folder} - {list}`). Спек обновляется до старта кода. *(Не сработало: после повторной проверки папки в API оказались (группы, `groupId`), см. Preflight Results.)*
- Если №6 не работает → переходим на event-log fallback для архива (план B в Monthly archive).
- Если №2, №3, №4, или №5 не работают — блокер, ищем обход (например, delete = move в «архив»-список). Возможен пересмотр архитектуры.

Спайк делается **первой задачей** в имплементационном плане; его результаты фиксируются прямо в этом спеке (раздел становится «Preflight results» после прогона).

## Новые зависимости и сетап

- **TickTick API доступ:** регистрация OAuth-приложения на developer.ticktick.com, получение `client_id` / `client_secret`. Refresh token получается на этапе OAuth-сетапа (см. ниже). Всё хранится в `.env` на Pi.
- **OAuth setup script:** `scripts/ticktick_oauth_setup.py` — отдельный one-shot скрипт. Запускается локально на маке (нужен локальный HTTP-callback). Открывает auth URL в браузере, ловит callback с `code`, обменивает на `access_token` + `refresh_token`, печатает значения для копи-паста в `.env` на Pi.
- **OAuth token lifecycle:** TickTick-клиент в боте автоматически следит за валидностью `access_token`. При получении 401 или истечении `expires_at` — вызывает `/oauth/token` с `refresh_token`, обновляет `access_token` (и `refresh_token` если он ротирован), **перезаписывает обновлённые значения в файл секретов** (например, `~/tasks_bot/.secrets/ticktick.json`, отдельно от `.env` чтобы не плодить git-конфликтов на статичный `.env`). Без этого бот тихо помрёт через срок жизни access-токена.
- **TickTick клиент:** Python-обёртка над REST API (можно с нуля — API простой, ~200 строк, или использовать существующий `ticktick-py`, проверить актуальность на момент имплементации).
- **Структура TickTick:** см. раздел «Структура TickTick». Папки и списки создаются один раз руками в UI перед миграцией (5 минут), их ID кешируются ботом в локальном json и подтягиваются при старте. Если в TickTick UI создаётся новый список или меняется ID — рестартнуть бота (`sudo systemctl restart tasks-bot`), кэш обновится.

## Preflight Results (прогнано 2026-05-31, обновлено после probe_groups)

| Operation | Status | Details |
|-----------|--------|---------|
| 0. folder/list addressable by ID | ✓ | Папки = TickTick **groups**, endpoint `GET/POST /project/group`. Поле `groupId` на проекте связывает с папкой. (При первом прогоне промахнулась — пробила тест-проекты без groupId, отсюда был ложный ⚠.) |
| 1. create task | ✓ | POST `/task` с `projectId` в теле |
| 2. update due | ✓ | POST `/task/{id}` (не PUT — TickTick Open API использует POST для update) |
| 3. complete | ✓ | POST `/project/{projectId}/task/{taskId}/complete` |
| 4. delete | ✓ | DELETE `/project/{projectId}/task/{taskId}` |
| 5. move project/list | ✓ | POST `/task/{id}` с новым `projectId` (через update) |
| 6. fetch completed for period | ✗ | `/project/{id}/data` возвращает **только открытые задачи** (statuses=[0], нет поля `completedTasks`). **План B (event log) обязателен для monthly archive.** |

**6 supported · 1 failed**

### Что меняется в спеке по результатам

**1. Структура TickTick = 5 групп (папок) + 15 проектов с `groupId`.**

В TickTick Open API есть две сущности:
- **`project`** — то, что в UI выглядит как «список задач» (его имя видно в сайдбаре). Адресуется по `id`. Имеет поле `groupId` для привязки к папке (опционально).
- **`project/group`** — то, что в UI выглядит как «папка», в которую можно складывать проекты. Адресуется по `id`. Endpoint: `GET/POST /project/group`.

Структура такая:

```
📁 Mars         (group)
   ├─ Mars - To Do      (project, groupId=<Mars>)
   ├─ Mars - Бэклог
   └─ Mars - Стратегия

📁 Channel      (group)
   ├─ Channel - To Do
   ├─ Channel - Threads
   ├─ Channel - Instagram
   ├─ Channel - Канал
   └─ Channel - Бэклог

📁 Consulting   (group)
   ├─ Consulting - To Do
   └─ Consulting - Бэклог

📁 Life         (group)
   ├─ Life - To Do
   └─ Life - Бэклог

📁 Vibe Coding  (group)
   ├─ Vibe Coding - To Do
   ├─ Vibe Coding - Бэклог
   └─ Vibe Coding - Стратегия

+ системный TickTick Inbox (fallback для непонятного)
```

Naming convention имени проекта: `{Brand} - {List}` (разделитель ` - `, пробел-дефис-пробел). Бренд (имя группы) и список (имя проекта без префикса) — две координаты, по которым работает routing бота.

**Текущее состояние:** 5 групп и 15 проектов уже созданы в TickTick (`scripts/setup_ticktick_structure.py` + `scripts/setup_ticktick_groups.py`, 2026-05-31). Маппинг `name → {id, groupId}` лежит в `scripts/preflight/projects_mapping.json` — это bootstrap-config для бота. Группа «Mars» названа `Mars (new)` (так как у Наташи уже была папка `Mars` от старого использования TickTick); после миграции/чистки она переименует в `Mars`.

**Routing бота** — не меняется. LLM извлекает `project` (бренд: mars/channel/...) + `list` (To Do/Бэклог/...). Routing-таблица (см. раздел «Routing — детерминированный») мапит пару `(brand, list)` в `project_id` через `projects_mapping.json`. На UX это не влияет.

**Сводки и команды** — работают с `project_id`. Группировка по бренду делается на стороне бота через mapping. Пользователь видит сводки, сгруппированные по mars/channel/... как и описано в спеке.

**2. Monthly archive — план B активен (event log на Pi).**

`GET /project/{id}/data` отдаёт только открытые задачи. Через Open API получить closed-таски за период нельзя. Поэтому:
- Бот ведёт `events.jsonl` на Pi, пишет туда каждую мутацию (`create`, `complete`, `delete`, `update`).
- Скрипт архива на Pi читает event log за прошлый месяц, фильтрует `op=complete`, группирует по project/list, генерирует md-файлы, пушит в data-репо `tasks_bot`.
- Mac-launchd сервис пуллит data-репо в Obsidian `_inbox/archive-staging/`.
- Известное ограничение: задачи, закрытые мышкой в TickTick UI (минуя бота), в архив не попадут.

**3. HTTP-методы для TickTick API — POST вместо PUT для update.**

TickTick Open API не принимает PUT (отвечает 500 «Request method 'PUT' not supported»). Update задачи делается через `POST /task/{id}` с обязательными полями `id` и `projectId` в теле + те, что меняем. Move project (`/proj`) — частный случай update, тоже POST. Update проекта (например, проставить ему `groupId`) — `POST /project/{id}` с полями `id`, `name`, `color`, плюс изменения.

### Что НЕ меняется

- UX в Telegram: capture, команды, сводки — всё как было в спеке.
- Семантика списков (To Do = со сроком, Бэклог = без, Стратегия = стратегические идеи) — сохраняется.
- Confidence-пороги, evals, OAuth lifecycle — без изменений.
- Routing-правила (детерминированные if-rules) — те же.

### Что нужно решить отдельно

- **Refresh token не вернулся** от TickTick при OAuth. `expires_in: 15551999 сек` (~180 дней) — access token долгий, но не вечный. Перед прогоном продакшен-бота — выяснить, есть ли у TickTick refresh-flow (может требоваться отдельный scope) или access нужно будет обновлять руками через `oauth_setup.py` раз в 180 дней.
- **Группа `Mars (new)`** — временное имя. После чистки старого TickTick-наследия Наташа переименует в `Mars` в UI; бот её узнает по `groupId` (не по имени), переименование безопасно.

## Открытые вопросы

1. **Rate limits TickTick API.** В preflight не зафиксированы. Нагрузка низкая (~20–50 операций в день), вряд ли упрёмся, но в production-боте логировать 429-ответы на всякий случай.
2. **Описание задачи (description) для идей.** Если идея набирается с тезисом — куда класть тезис? В описание задачи в TickTick. Это юзаж-вопрос, не блокер для спека.
3. **Inline vs reply keyboard.** Идём с inline-кнопками — удобнее (не засоряют клавиатуру), требуют callback handler, что не проблема.
4. **Confidence-пороги и TTL pending-capture** — в `.env`, чтобы крутить без редеплоя.
5. **Refresh token из TickTick OAuth не вернулся** (только access на ~180 дней). Решить: положиться на 180-дневный access + ручной перезапуск `oauth_setup.py` раз в полгода, или копать в сторону отдельного scope/flow для refresh-token. Не блокер для MVP — на первом раунде хватит ручного обновления.

## Метрики успеха

После 2 недель работы v2.0:
- Сколько capture-сообщений было обработано без вопросов (idealy ≥ 75%).
- Сколько ушло в TickTick Inbox по таймауту (должно быть < 10%).
- Eval-accuracy на собранном наборе по project (цель ≥ 90%).
- Субъективное: ощущение «удобнее, чем v1» — фиксируем после первой недели.
