---
project: vibe-coding
type: plan
date: 2026-05-30
status: ready-to-execute
related_spec: "{vibe-coding} {plan} tasks-bot 2.0 ticktick – 2026-05-30.md"
---

# TickTick API Preflight — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Verify that TickTick Open API supports the 7 operations required by the tasks-bot 2.0 spec (folder/list addressing + 6 CRUD ops), and produce a Preflight Results block that either confirms the spec or guides its revision.

**Architecture:** Standalone Python module under `scripts/preflight/` in the existing `tasks-bot` repo. Reuses the existing `.venv`. Two entry points: `oauth_setup.py` (one-shot, manual) and `run_preflight.py` (probe script). A thin HTTP client + 7 check functions + a tiny reporter.

**Tech Stack:** Python 3.13, `requests`, `python-dotenv`. No async needed. No DB. No tests beyond a unit test for the reporter — the checks themselves ARE the test, against a real API.

**Out of scope for this plan:** the full bot rewrite, capture handler, summary scheduler, migration, archive, evals. Those land in separate plans **after** preflight results are recorded in the spec.

---

## File Structure

| File | Responsibility |
|------|----------------|
| `scripts/preflight/__init__.py` | Empty package marker |
| `scripts/preflight/config.py` | Loads env (CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN), constants (BASE_URL, REDIRECT_URI) |
| `scripts/preflight/oauth_setup.py` | One-shot OAuth: opens browser, catches callback on localhost:8765, exchanges code → tokens, prints them for `.env` paste |
| `scripts/preflight/http_client.py` | Thin requests wrapper: `get/post/put/delete` with bearer auth + JSON encoding |
| `scripts/preflight/checks.py` | 7 check functions, each returns `CheckResult(name, status, details)` |
| `scripts/preflight/reporter.py` | Format results to markdown for paste into spec |
| `scripts/preflight/run_preflight.py` | Wires it all: load config → run each check → print and write markdown report |
| `tests/preflight/test_reporter.py` | Unit test for reporter (pure function, no API) |
| `.env.preflight.example` | Template for required env vars |

After the run, results land in `docs/preflight-results-2026-05-30.md` (or whatever date) and are then pasted into the v2 spec as a new section.

---

## Manual prerequisites (do once, before Task 1)

These steps require a browser and are not codeable. Do them before starting tasks.

1. Открыть https://developer.ticktick.com/ → войти под аккаунтом Наташи.
2. Зарегистрировать новое приложение: name `tasks-bot-2.0`, redirect URI `http://localhost:8765/callback`.
3. Скопировать `Client ID` и `Client Secret` — понадобятся в Task 2.
4. Свериться с TickTick Open API docs (`https://developer.ticktick.com/docs`): актуальный auth URL, token URL, scopes, base API URL. Найденные URL положить в Task 2 (config.py) как константы.

Если developer.ticktick.com не доступен или регистрация невозможна — это **блокер всей переделки**, останавливаемся и думаем заново.

---

## Task 1: Создать skeleton директории `scripts/preflight/`

**Files:**
- Create: `scripts/preflight/__init__.py`
- Create: `tests/preflight/__init__.py`
- Create: `.env.preflight.example`

- [ ] **Step 1.1: Создать пакет `scripts/preflight/`**

```bash
mkdir -p scripts/preflight tests/preflight
touch scripts/preflight/__init__.py tests/preflight/__init__.py
```

- [ ] **Step 1.2: Создать `.env.preflight.example`**

```
TICKTICK_CLIENT_ID=
TICKTICK_CLIENT_SECRET=
TICKTICK_ACCESS_TOKEN=
TICKTICK_REFRESH_TOKEN=
```

- [ ] **Step 1.3: Убедиться, что `.env.preflight` (без `.example`) в `.gitignore`**

Run: `grep -q "^\.env\.preflight$" .gitignore || echo ".env.preflight" >> .gitignore`

- [ ] **Step 1.4: Commit**

```bash
git add scripts/preflight/__init__.py tests/preflight/__init__.py .env.preflight.example .gitignore
git commit -m "preflight: skeleton dirs and env template"
```

---

## Task 2: `config.py` — конфигурация и константы

**Files:**
- Create: `scripts/preflight/config.py`

- [ ] **Step 2.1: Написать модуль**

```python
"""Конфигурация для preflight-спайка TickTick API.

Все секреты читаются из .env.preflight. URL-ы — константы (свериться с
developer.ticktick.com/docs перед первым запуском).
"""
import os
from dataclasses import dataclass
from dotenv import load_dotenv

# Грузим .env.preflight рядом с корнем проекта
load_dotenv('.env.preflight')

# URL-ы из официальной документации TickTick Open API
# (https://developer.ticktick.com/docs)
AUTH_URL = "https://ticktick.com/oauth/authorize"
TOKEN_URL = "https://ticktick.com/oauth/token"
BASE_API_URL = "https://api.ticktick.com/open/v1"
REDIRECT_URI = "http://localhost:8765/callback"
SCOPES = "tasks:read tasks:write"


@dataclass
class Config:
    client_id: str
    client_secret: str
    access_token: str | None
    refresh_token: str | None


def load() -> Config:
    return Config(
        client_id=os.environ["TICKTICK_CLIENT_ID"],
        client_secret=os.environ["TICKTICK_CLIENT_SECRET"],
        access_token=os.getenv("TICKTICK_ACCESS_TOKEN"),
        refresh_token=os.getenv("TICKTICK_REFRESH_TOKEN"),
    )
```

- [ ] **Step 2.2: Commit**

```bash
git add scripts/preflight/config.py
git commit -m "preflight: config loader and TickTick URL constants"
```

---

## Task 3: `oauth_setup.py` — one-shot OAuth flow

**Files:**
- Create: `scripts/preflight/oauth_setup.py`

- [ ] **Step 3.1: Написать скрипт**

```python
"""One-shot OAuth для TickTick.

Запускается локально на маке:
    .venv/bin/python -m scripts.preflight.oauth_setup

Открывает браузер, ловит callback на localhost:8765, обменивает code
на access/refresh tokens, печатает их для копи-паста в .env.preflight.
"""
import http.server
import secrets
import urllib.parse
import webbrowser

import requests

from scripts.preflight import config


def main() -> None:
    cfg = config.load()
    state = secrets.token_urlsafe(16)

    auth_url = config.AUTH_URL + "?" + urllib.parse.urlencode({
        "client_id": cfg.client_id,
        "response_type": "code",
        "redirect_uri": config.REDIRECT_URI,
        "scope": config.SCOPES,
        "state": state,
    })

    code_holder: dict[str, str] = {}

    class Handler(http.server.BaseHTTPRequestHandler):
        def do_GET(self):  # noqa: N802 (HTTP handler signature)
            if "?" not in self.path:
                self.send_error(400, "no query")
                return
            qs = urllib.parse.parse_qs(self.path.split("?", 1)[1])
            if qs.get("state", [None])[0] != state:
                self.send_error(400, "state mismatch")
                return
            code_holder["code"] = qs["code"][0]
            self.send_response(200)
            self.send_header("Content-type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write("OK. Можно закрыть вкладку.".encode("utf-8"))

        def log_message(self, *args):  # silence default logging
            return

    print(f"Открываю браузер: {auth_url}")
    webbrowser.open(auth_url)

    with http.server.HTTPServer(("localhost", 8765), Handler) as srv:
        srv.handle_request()  # single callback

    if "code" not in code_holder:
        raise SystemExit("Не получили authorization code")

    resp = requests.post(
        config.TOKEN_URL,
        data={
            "client_id": cfg.client_id,
            "client_secret": cfg.client_secret,
            "code": code_holder["code"],
            "grant_type": "authorization_code",
            "redirect_uri": config.REDIRECT_URI,
        },
        timeout=20,
    )
    resp.raise_for_status()
    tokens = resp.json()

    print()
    print("=== Скопируй в .env.preflight ===")
    print(f"TICKTICK_ACCESS_TOKEN={tokens['access_token']}")
    if "refresh_token" in tokens:
        print(f"TICKTICK_REFRESH_TOKEN={tokens['refresh_token']}")
    print()
    print(f"expires_in: {tokens.get('expires_in')} сек")


if __name__ == "__main__":
    main()
```

- [ ] **Step 3.2: Установить зависимости (если не стоят)**

Run: `.venv/bin/pip install requests python-dotenv`
Expected: пакеты установлены без ошибок.

- [ ] **Step 3.3: Создать `.env.preflight` (НЕ коммитить)**

Скопировать из `.env.preflight.example`, заполнить `TICKTICK_CLIENT_ID` и `TICKTICK_CLIENT_SECRET` из браузера (см. Manual prerequisites).

- [ ] **Step 3.4: Запустить OAuth, получить токены**

Run: `.venv/bin/python -m scripts.preflight.oauth_setup`
Expected: открылся браузер, после логина в TickTick — на терминал напечатаны `TICKTICK_ACCESS_TOKEN=...` и `TICKTICK_REFRESH_TOKEN=...`.

Если в стейте `code` не пришёл, ошибка `state mismatch`, или 4xx от `/oauth/token` — фиксируем причину, проверяем redirect_uri и scopes в developer-консоли TickTick. Не двигаемся дальше пока токен не получен.

- [ ] **Step 3.5: Скопировать токены в `.env.preflight`**

Подставить полученные значения в файл.

- [ ] **Step 3.6: Commit (только код, без `.env.preflight`)**

```bash
git add scripts/preflight/oauth_setup.py
git commit -m "preflight: OAuth one-shot setup script"
```

---

## Task 4: `http_client.py` — тонкая HTTP-обёртка

**Files:**
- Create: `scripts/preflight/http_client.py`

- [ ] **Step 4.1: Написать клиент**

```python
"""Тонкий HTTP-клиент для TickTick API.

Не делаем здесь автоматический refresh — для preflight access token свежий
после Task 3. Если он истечёт во время прогона — перезапустим oauth_setup.
В production-боте будет полноценный lifecycle.
"""
from typing import Any
import requests

from scripts.preflight import config


class TickTickHTTPError(Exception):
    def __init__(self, method: str, path: str, status: int, body: str):
        super().__init__(f"{method} {path} -> {status}: {body[:300]}")
        self.status = status
        self.body = body


def _url(path: str) -> str:
    return config.BASE_API_URL.rstrip("/") + "/" + path.lstrip("/")


def _headers(access_token: str) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
    }


def request(method: str, path: str, access_token: str, json_body: Any = None) -> Any:
    resp = requests.request(
        method=method,
        url=_url(path),
        headers=_headers(access_token),
        json=json_body,
        timeout=20,
    )
    if not resp.ok:
        raise TickTickHTTPError(method, path, resp.status_code, resp.text)
    if resp.status_code == 204 or not resp.text:
        return None
    return resp.json()


def get(path: str, token: str) -> Any:
    return request("GET", path, token)


def post(path: str, token: str, body: Any) -> Any:
    return request("POST", path, token, body)


def put(path: str, token: str, body: Any) -> Any:
    return request("PUT", path, token, body)


def delete(path: str, token: str) -> Any:
    return request("DELETE", path, token)
```

- [ ] **Step 4.2: Commit**

```bash
git add scripts/preflight/http_client.py
git commit -m "preflight: thin HTTP client wrapper"
```

---

## Task 5: `checks.py` — 7 операций как функции

**Files:**
- Create: `scripts/preflight/checks.py`

Каждый check возвращает `CheckResult(name, status, details)`. `status` — один из `"✓"`, `"✗"`, `"⚠"`. `details` — короткая строка (что произошло, ID объекта, тело ошибки).

Все checks бросают `TickTickHTTPError` наверх — wrapper в `run_preflight.py` ловит и превращает в `✗`.

Важно: пути API (`/project`, `/task`, `/task/{id}/complete` и т.п.) — это **гипотезы по типичным REST-конвенциям**. Перед первым запуском **свериться с актуальными TickTick docs** (раздел Manual prerequisites) и при необходимости поправить строки. Это та часть, ради которой spike и пишется.

- [ ] **Step 5.1: Написать модуль**

```python
"""7 preflight-проверок TickTick API.

Каждая функция выполняет одну операцию на тестовых данных и возвращает
CheckResult. Цель — выяснить, что реально поддерживается, не сломав
production-данные.

Пути API ниже — гипотезы. Перед запуском сверь с актуальной документацией
TickTick Open API и поправь, если отличается.
"""
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

from scripts.preflight import http_client as http

YEKB = timezone(timedelta(hours=5))


@dataclass
class CheckResult:
    name: str
    status: str  # "✓" | "✗" | "⚠"
    details: str


# === Check 0: folder/list-модель адресуема по ID ===

def check_folder_list_model(token: str) -> CheckResult:
    """Listing проектов и попытка увидеть структуру папка/список.

    TickTick UI показывает папки (folders) и внутри них списки (lists).
    Open API может называть это иначе. Цель:
      - получить список всех projects через GET /project
      - проверить, есть ли поле parent_id / groupId / folder_id у проекта
      - если да — структура «папка → списки» представима через parent_id
      - если нет — flat-projects модель: 5 папок × N списков = плоские проекты с
        именами вида "{folder} - {list}"
    """
    projects = http.get("project", token)
    if not isinstance(projects, list):
        return CheckResult(
            "0. folder/list addressable by ID",
            "⚠",
            f"GET /project вернул не-список: {type(projects).__name__}",
        )
    sample = projects[0] if projects else {}
    parent_keys = [k for k in ("groupId", "parentId", "parent_id", "folderId") if k in sample]
    if parent_keys:
        return CheckResult(
            "0. folder/list addressable by ID",
            "✓",
            f"projects поддерживают вложенность через {parent_keys[0]} (sample keys: {list(sample.keys())})",
        )
    return CheckResult(
        "0. folder/list addressable by ID",
        "⚠",
        f"projects плоские, parent_id-поля не нашлось. Sample keys: {list(sample.keys())}. "
        "Падать на flat-projects модель.",
    )


# === Helpers для создания тестовой структуры ===

def _create_test_project(token: str, name: str) -> str:
    """Создать project и вернуть его ID."""
    result = http.post("project", token, {"name": name, "color": "#3B82F6"})
    return result["id"]


def _delete_test_project(token: str, project_id: str) -> None:
    http.delete(f"project/{project_id}", token)


def _create_test_task(token: str, project_id: str, title: str, due: datetime | None = None) -> str:
    body: dict = {"projectId": project_id, "title": title}
    if due:
        body["dueDate"] = due.isoformat()
    result = http.post("task", token, body)
    return result["id"]


# === Check 1: create task в указанный проект ===

def check_create_task(token: str, project_id: str) -> CheckResult:
    try:
        task_id = _create_test_task(token, project_id, "preflight: create")
        return CheckResult("1. create task", "✓", f"created task {task_id} in project {project_id}")
    except http.TickTickHTTPError as e:
        return CheckResult("1. create task", "✗", str(e))


# === Check 2: update due ===

def check_update_due(token: str, project_id: str) -> CheckResult:
    try:
        task_id = _create_test_task(token, project_id, "preflight: update due")
        new_due = datetime.now(YEKB) + timedelta(days=3)
        http.put(f"task/{task_id}", token, {"dueDate": new_due.isoformat(), "projectId": project_id})
        return CheckResult("2. update due", "✓", f"updated due on task {task_id}")
    except http.TickTickHTTPError as e:
        return CheckResult("2. update due", "✗", str(e))


# === Check 3: complete ===

def check_complete(token: str, project_id: str) -> CheckResult:
    try:
        task_id = _create_test_task(token, project_id, "preflight: complete")
        http.post(f"project/{project_id}/task/{task_id}/complete", token, None)
        return CheckResult("3. complete", "✓", f"completed task {task_id}")
    except http.TickTickHTTPError as e:
        return CheckResult("3. complete", "✗", str(e))


# === Check 4: delete ===

def check_delete(token: str, project_id: str) -> CheckResult:
    try:
        task_id = _create_test_task(token, project_id, "preflight: delete")
        http.delete(f"project/{project_id}/task/{task_id}", token)
        return CheckResult("4. delete", "✓", f"deleted task {task_id}")
    except http.TickTickHTTPError as e:
        return CheckResult("4. delete", "✗", str(e))


# === Check 5: move project/list ===

def check_move_project(token: str, project_id_a: str, project_id_b: str) -> CheckResult:
    try:
        task_id = _create_test_task(token, project_id_a, "preflight: move")
        http.put(f"task/{task_id}", token, {"projectId": project_id_b})
        return CheckResult(
            "5. move project/list",
            "✓",
            f"moved task {task_id}: {project_id_a} -> {project_id_b}",
        )
    except http.TickTickHTTPError as e:
        return CheckResult("5. move project/list", "✗", str(e))


# === Check 6: fetch completed for period ===

def check_fetch_completed(token: str) -> CheckResult:
    """Достать closed-таски за последние 30 дней.

    Эндпоинт у TickTick может называться `/task/closed`, `/project/{id}/closed`,
    или требовать параметры from/to. Пробуем самый общий вариант, в details
    пишем, что вернулось.
    """
    try:
        from_dt = (datetime.now(YEKB) - timedelta(days=30)).date().isoformat()
        to_dt = datetime.now(YEKB).date().isoformat()
        result = http.get(f"task/closed?from={from_dt}&to={to_dt}", token)
        if result is None:
            return CheckResult("6. fetch completed", "⚠", "200 but empty body")
        count = len(result) if isinstance(result, list) else "non-list"
        return CheckResult("6. fetch completed", "✓", f"got {count} closed tasks in last 30 days")
    except http.TickTickHTTPError as e:
        return CheckResult(
            "6. fetch completed",
            "✗",
            f"{e} — план B (event log) активен",
        )
```

- [ ] **Step 5.2: Commit**

```bash
git add scripts/preflight/checks.py
git commit -m "preflight: 7 API check functions"
```

---

## Task 6: `reporter.py` + unit test

**Files:**
- Create: `scripts/preflight/reporter.py`
- Create: `tests/preflight/test_reporter.py`

- [ ] **Step 6.1: Написать тест первым**

```python
# tests/preflight/test_reporter.py
from scripts.preflight.checks import CheckResult
from scripts.preflight.reporter import format_markdown


def test_format_markdown_renders_all_results():
    results = [
        CheckResult("0. folder/list addressable by ID", "✓", "via groupId"),
        CheckResult("1. create task", "✓", "created task abc"),
        CheckResult("6. fetch completed", "✗", "404 not found"),
    ]
    md = format_markdown(results)

    assert "# Preflight Results" in md
    assert "| 0. folder/list addressable by ID | ✓ | via groupId |" in md
    assert "| 1. create task | ✓ | created task abc |" in md
    assert "| 6. fetch completed | ✗ | 404 not found |" in md


def test_format_markdown_summary_counts():
    results = [
        CheckResult("a", "✓", ""),
        CheckResult("b", "✓", ""),
        CheckResult("c", "✗", ""),
        CheckResult("d", "⚠", ""),
    ]
    md = format_markdown(results)
    assert "Supported: 2 / 4" in md
    assert "Failed: 1" in md
    assert "Partial: 1" in md
```

- [ ] **Step 6.2: Запустить тест — должен упасть**

Run: `.venv/bin/pytest tests/preflight/test_reporter.py -v`
Expected: FAIL — `format_markdown` ещё не существует.

- [ ] **Step 6.3: Написать `reporter.py`**

```python
"""Форматирование CheckResult'ов в markdown для вставки в спек."""
from scripts.preflight.checks import CheckResult


def format_markdown(results: list[CheckResult]) -> str:
    lines: list[str] = []
    lines.append("# Preflight Results")
    lines.append("")
    lines.append("| Operation | Status | Details |")
    lines.append("|-----------|--------|---------|")
    for r in results:
        lines.append(f"| {r.name} | {r.status} | {r.details} |")
    lines.append("")
    ok = sum(1 for r in results if r.status == "✓")
    fail = sum(1 for r in results if r.status == "✗")
    partial = sum(1 for r in results if r.status == "⚠")
    lines.append(f"**Supported: {ok} / {len(results)}** · Failed: {fail} · Partial: {partial}")
    lines.append("")
    return "\n".join(lines)
```

- [ ] **Step 6.4: Прогнать тест — должен пройти**

Run: `.venv/bin/pytest tests/preflight/test_reporter.py -v`
Expected: PASS.

- [ ] **Step 6.5: Commit**

```bash
git add scripts/preflight/reporter.py tests/preflight/test_reporter.py
git commit -m "preflight: reporter + unit test"
```

---

## Task 7: `run_preflight.py` — главный entry point

**Files:**
- Create: `scripts/preflight/run_preflight.py`

- [ ] **Step 7.1: Написать**

```python
"""Прогнать все 7 preflight-проверок против реального TickTick API.

Запуск:
    .venv/bin/python -m scripts.preflight.run_preflight

Создаёт два тестовых проекта (для check_move), прогоняет все проверки,
печатает результаты в stdout И пишет в docs/preflight-results-YYYY-MM-DD.md.
После прогона тестовые проекты удаляются (best-effort).
"""
from datetime import datetime
from pathlib import Path

from scripts.preflight import checks, config, http_client as http
from scripts.preflight.reporter import format_markdown


def main() -> None:
    cfg = config.load()
    if not cfg.access_token:
        raise SystemExit("Нет TICKTICK_ACCESS_TOKEN в .env.preflight. Прогони oauth_setup сначала.")

    token = cfg.access_token

    # Создаём 2 тестовых проекта для check_move
    project_a = checks._create_test_project(token, "preflight-test-A")
    project_b = checks._create_test_project(token, "preflight-test-B")
    print(f"Created test projects: {project_a}, {project_b}")

    results = []
    try:
        results.append(checks.check_folder_list_model(token))
        results.append(checks.check_create_task(token, project_a))
        results.append(checks.check_update_due(token, project_a))
        results.append(checks.check_complete(token, project_a))
        results.append(checks.check_delete(token, project_a))
        results.append(checks.check_move_project(token, project_a, project_b))
        results.append(checks.check_fetch_completed(token))
    finally:
        # Cleanup, best effort
        for pid in (project_a, project_b):
            try:
                checks._delete_test_project(token, pid)
            except http.TickTickHTTPError as e:
                print(f"WARN: cleanup project {pid} failed: {e}")

    md = format_markdown(results)
    print()
    print(md)

    date = datetime.now().strftime("%Y-%m-%d")
    out_path = Path(f"docs/preflight-results-{date}.md")
    out_path.write_text(md, encoding="utf-8")
    print(f"\nWritten to: {out_path}")


if __name__ == "__main__":
    main()
```

- [ ] **Step 7.2: Commit (без запуска)**

```bash
git add scripts/preflight/run_preflight.py
git commit -m "preflight: main runner"
```

---

## Task 8: Прогнать preflight на реальном API

- [ ] **Step 8.1: Запустить**

Run: `.venv/bin/python -m scripts.preflight.run_preflight`
Expected: на stdout — markdown-таблица с 7 строками, в файле `docs/preflight-results-2026-05-30.md` — то же самое. Большинство строк `✓`, возможно 1-2 `⚠` или `✗`.

Если ВСЕ `✗` с 401 — токен истёк/неправильный, перезапустить `oauth_setup`.

Если 1-я строка (folder/list) `⚠ projects плоские` — это важный сигнал, см. Task 9.

Если `create_task` или `delete` падают с 403/404 — пути API отличаются от наших гипотез. Свериться с docs, поправить `checks.py`, прогнать снова. Это нормальный итерационный цикл спайка.

- [ ] **Step 8.2: Зафиксировать результаты в git**

```bash
git add docs/preflight-results-2026-05-30.md
git commit -m "preflight: results from first run"
```

---

## Task 9: Обновить спек разделом «Preflight Results»

**Files:**
- Modify: `docs/{vibe-coding} {plan} tasks-bot 2.0 ticktick – 2026-05-30.md`

- [ ] **Step 9.1: Открыть спек, вставить новый раздел перед «Открытые вопросы»**

Содержание раздела — таблица из `docs/preflight-results-2026-05-30.md` + краткие выводы:
- Если `0. folder/list` = `✓` — структура «папки + списки» работает как есть, ничего не меняем.
- Если `0. folder/list` = `⚠ flat` — пересмотреть раздел «Структура TickTick»: вместо `Mars → To Do/Бэклог/Стратегия` сделать `Mars - To Do`, `Mars - Бэклог`, `Mars - Стратегия` как отдельные projects. Routing-логика остаётся, но строки имён списков меняются.
- Если `6. fetch completed` = `✗` — фиксируем переход на план B монтли-архива (event log на Pi).
- Если что-то из `1`–`5` = `✗` — добавить в раздел список конкретных обходов или поднять блокер.

- [ ] **Step 9.2: Обновить статус спека**

В frontmatter: `status: ready-for-preflight` → `status: ready-to-plan` (или `blocked: <причина>` если что-то критичное упало).

- [ ] **Step 9.3: Commit**

```bash
git add "docs/{vibe-coding} {plan} tasks-bot 2.0 ticktick – 2026-05-30.md"
git commit -m "spec: preflight results + status bump"
```

---

## После плана

После того как спек обновлён результатами:
- Если структура подтвердилась — пишем второй plan, разбивающий остальную реализацию (capture / commands / scheduler / migration / archive / evals) на бите-сайз таски.
- Если структура потребовала flat-projects — перед вторым планом обновляем разделы «Структура TickTick» и «Capture handler» в спеке (поменяются строки имён, не суть архитектуры).
- Если есть блокеры — обсуждаем заново.

## Self-Review

Прошлась по плану свежим взглядом:

1. **Spec coverage:** план покрывает раздел спека «Preflight checks» полностью (7 операций) и одно его последствие (обновление «Preflight results»). Остальные разделы спека — в отдельный plan, как и заявлено в Goal.
2. **Placeholders:** TBD/TODO в плане нет. Гипотетические API-пути в `checks.py` отмечены явно, с инструкцией «свериться с docs перед запуском».
3. **Type consistency:** `CheckResult` определён в `checks.py`, импортируется в `reporter.py` и `tests/preflight/test_reporter.py`. `TickTickHTTPError` определён в `http_client.py`, импортируется в `checks.py` и `run_preflight.py` через алиас `http`. Совпадает.
4. **Bite-sized:** каждая задача — один цельный модуль с тестом или прогоном. Шаги внутри — мелкие (написать функцию, прогнать тест, коммит).

Готов к выполнению.
