PRONA BoardО продукте

Агент отвечает на стикеры: переписка с Claude Code прямо на доске

Инструкция5 минут чтенияОбновлено 7 октября 2026 г.

Claude Code может отвечать вам прямо на доске: вы пишете стикер, ждёте 10 секунд — и рядом со стикером появляется ответ агента. Для этого рядом с проектом лежит небольшой скрипт-наблюдатель, а Claude Code запускает его в фоне и просыпается на каждый ваш стикер.

Как это работает

  1. Наблюдатель раз в 4 секунды спрашивает у доски, что изменилось.
  2. Новый или изменённый стикер (или текст) он запоминает и ждёт: если 10 секунд его никто не трогает, стикер считается дописанным.
  3. Наблюдатель проверяет, что стикер поставили вы, а не агент, и выводит одну строку: id стикера и его текст.
  4. Claude Code получает эту строку как событие, читает стикер и то, что вокруг него, делает то, о чём вы просите, и отвечает примечанием, прикреплённым к стикеру. Примечание едет вместе со стикером, если его передвинуть.

Что понадобится

  • Claude Code, подключённый к доске. Если ещё не подключали — инструкция «Подключить агента по MCP».
  • Python 3. На macOS проверьте командой python3 --version; если система предложит установить инструменты разработчика — согласитесь. На Windows установите Python с python.org и используйте python вместо python3.
  • Ссылка на доску. Откройте доску и скопируйте адрес из строки браузера.

Как включить

  1. Сохраните наблюдатель в папку проекта. Создайте файл prona-watch.py в той же папке, где запускаете Claude Code, и вставьте в него код ниже. Ничего менять не нужно: токен скрипт возьмёт из настроек Claude Code.

    """PRONA Board: стикер человека, который 10 с не правят, -> одна строка в stdout (событие для Claude Code)."""
    import json, os, sys, time, urllib.request
    
    URL = 'https://board-mcp.prona.space/mcp'
    QUIET_SEC, POLL_SEC = 10, 4
    TEXT_TYPES = {'note', 'text'}
    
    
    def find_token():
        """Токен из PRONA_BOARD_TOKEN или из настроек Claude Code (~/.claude.json). Не печатается."""
        if os.environ.get('PRONA_BOARD_TOKEN'):
            return 'Bearer ' + os.environ['PRONA_BOARD_TOKEN']
        cfg = json.load(open(os.path.expanduser('~/.claude.json'), encoding='utf-8'))
        scopes = [cfg.get('mcpServers', {})] + [p.get('mcpServers', {}) for p in cfg.get('projects', {}).values()]
        for servers in scopes:
            auth = servers.get('prona-board', {}).get('headers', {}).get('Authorization')
            if auth:
                return auth
        sys.exit('Не нашёл токен prona-board: подключите доску к Claude Code или задайте PRONA_BOARD_TOKEN')
    
    
    class Board:
        def __init__(self):
            self.auth, self.sid, self.n = find_token(), None, 0
            self.rpc('initialize', {'protocolVersion': '2025-03-26', 'capabilities': {},
                                    'clientInfo': {'name': 'prona-watch', 'version': '1'}})
            self.post({'jsonrpc': '2.0', 'method': 'notifications/initialized'})
    
        def post(self, body):
            headers = {'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream',
                       'Authorization': self.auth}
            if self.sid:
                headers['Mcp-Session-Id'] = self.sid
            req = urllib.request.Request(URL, data=json.dumps(body).encode(), headers=headers, method='POST')
            with urllib.request.urlopen(req, timeout=30) as resp:
                self.sid = resp.headers.get('Mcp-Session-Id') or self.sid
                raw = resp.read().decode()
            if not raw.strip():
                return None
            if raw.lstrip().startswith('{'):
                return json.loads(raw)
            data = [line[5:].strip() for line in raw.splitlines() if line.startswith('data:')]
            return json.loads(data[-1]) if data else None
    
        def rpc(self, method, params):
            self.n += 1
            r = self.post({'jsonrpc': '2.0', 'id': self.n, 'method': method, 'params': params})
            if r and 'error' in r:
                raise RuntimeError(r['error'])
            return r['result'] if r else None
    
        def read(self, request):
            res = self.rpc('tools/call', {'name': 'board_read', 'arguments': {'request': request}})
            text = ''.join(c.get('text', '') for c in res.get('content', []))
            if res.get('isError'):  # истёкший токен, чужая доска — сервер объясняет словами
                raise RuntimeError(text)
            return json.loads(text)
    
    
    if len(sys.argv) < 2:
        sys.exit('Использование: python3 -u prona-watch.py <ссылка на доску или её id>')
    BOARD = sys.argv[1].rstrip('/').split('/')[-1].split('?')[0]
    here = os.path.dirname(os.path.abspath(__file__))
    CURSOR_FILE = os.path.join(here, f'.prona-watch-{BOARD[:8]}.cursor')
    SEEN_FILE = os.path.join(here, f'.prona-watch-{BOARD[:8]}.seen.json')
    
    try:
        board = Board()
        cursor = open(CURSOR_FILE).read().strip() if os.path.exists(CURSOR_FILE) else None
        if not cursor:
            cursor = board.read({'op': 'changes', 'boardId': BOARD})['cursor']
    except Exception as err:
        sys.exit(f'Доска не ответила: {err}')
    seen = json.load(open(SEEN_FILE, encoding='utf-8')) if os.path.exists(SEEN_FILE) else {}
    pending, fails = {}, 0
    
    while True:
        try:
            ch = board.read({'op': 'changes', 'boardId': BOARD, 'since': cursor})
            cursor = ch.get('cursor', cursor)
            open(CURSOR_FILE, 'w').write(cursor)
            now = time.time()
            for e in ch.get('changed', []):
                if e.get('type') in TEXT_TYPES and e.get('change') in ('created', 'updated'):
                    pending[e['id']] = {'text': e.get('text', ''), 'last': now}
            ready = [i for i, p in pending.items() if now - p['last'] >= QUIET_SEC]
            if ready:
                human = {h.get('id') for h in board.read({'op': 'diff', 'boardId': BOARD}).get('human', [])}
                for i in ready:
                    p = pending.pop(i)
                    if i in human and p['text'].strip() and seen.get(i) != p['text']:
                        seen[i] = p['text']
                        json.dump(seen, open(SEEN_FILE, 'w', encoding='utf-8'), ensure_ascii=False)
                        print(json.dumps({'event': 'sticker', 'id': i, 'text': p['text']}, ensure_ascii=False), flush=True)
            fails = 0
        except Exception as err:  # сеть или сервер: не роняем наблюдение с первого сбоя
            fails += 1
            if fails in (5, 50):
                print(json.dumps({'event': 'error', 'fails': fails, 'error': str(err)[:200]}, ensure_ascii=False), flush=True)
            if fails >= 200:
                sys.exit(1)
        time.sleep(POLL_SEC)
    
  2. Дайте Claude Code задачу следить за доской. Откройте Claude Code в этой папке и отправьте сообщение ниже, заменив ссылку на свою. Агент запустит наблюдатель в фоне и будет отвечать на каждый ваш стикер.

    Следи за доской https://prona.space/board/ВАШ_ID и отвечай мне на ней.
    Запусти через Monitor команду `python3 -u prona-watch.py https://prona.space/board/ВАШ_ID`
    с timeout_ms 1800000; когда монитор истечёт — запускай его заново, пока я не скажу «хватит».
    Каждая строка вывода — мой стикер: прочитай его текст и то, что рядом на доске, сделай,
    о чём я прошу, и ответь примечанием (annotation) с attachTo = id стикера и position right.
    Длинный результат положи на доску документом рядом, а в чат пиши только коротко, что сделано.
    
  3. Проверьте. Поставьте на доске стикер «Привет, ты здесь?» и подождите 10–15 секунд. Рядом со стикером появится примечание с ответом агента.

Как пользоваться

  • Пишите стикер как сообщение в чат. «Сведи эти три стикера в таблицу», «Почему этот вывод так оценён?», «Добавь этап про риски» — агент увидит, рядом с чем стоит стикер, и поймёт, о чём речь.
  • Дописывайте спокойно. Пока вы правите стикер, агент ждёт; отвечает через 10 секунд после последней правки.
  • Исправили вопрос — получите новый ответ. Если изменить текст уже отвеченного стикера, агент ответит снова. Простое перемещение стикера ответа не вызывает.
  • Остановить — напишите агенту в чат «хватит следить за доской».

Ограничения

  • Агент отвечает, только пока открыта сессия Claude Code, в которой он следит за доской. Закрыли терминал — ответы прекратятся; при следующем запуске повторите шаг 2, пропущенные стикеры наблюдатель не потеряет.
  • Фоновое наблюдение живёт до 30 минут, после чего агент перезапускает его сам — это предусмотрено в задаче из шага 2.
  • Пока агент выполняет одну просьбу, следующие стикеры ждут в очереди и будут обработаны по порядку.

Если не заработало

Что видитеПричинаЧто сделать
«Не нашёл токен prona-board»Claude Code не подключён к доске или подключён под другим именемПодключите по инструкции или задайте токен в переменной PRONA_BOARD_TOKEN
python3: command not foundНе установлен Python 3Установите Python 3; на Windows запускайте через python
Агент говорит, что у него нет MonitorСтарая версия Claude CodeОбновите Claude Code и повторите шаг 2
Стикер стоит, ответа нетАгент занят предыдущей просьбой или сессия закрытаПосмотрите в терминал: если сессия закрыта, повторите шаг 2
«Токен доски не принят»Истёк срок токенаВыпустите новый токен в окне «Подключить агента», выполните команду подключения заново и повторите шаг 2
Событие error в чате агентаСбой сетиНаблюдатель повторит запросы сам; если ошибка не уходит — перезапустите шаг 2

Что дальше

Дайте агенту большую задачу на доске — как её поставить, рассказано в инструкции «Первая задача агенту». Стикерами удобно уточнять ход работы по этапам, не переключаясь в терминал.

Читайте также

Вопросы

Почему агент отвечает не сразу, а через 10 секунд?
Наблюдатель ждёт, пока вы допишете стикер. Если стикер правят, отсчёт начинается заново — иначе агент ответил бы на половину фразы.
Агент ответит на стикеры, которые ставит сам?
Нет. Наблюдатель отличает ваши правки от правок агента и будит его только на ваши стикеры и тексты.
Работает ли это в Codex или Cursor?
Пока нет. Нужен Claude Code с фоновым наблюдением (инструмент Monitor) — он получает строки наблюдателя прямо во время работы. В других клиентах доску можно вести обычными задачами в чате.
Куда уходит токен?
Никуда, кроме сервера доски. Скрипт берёт токен из настроек Claude Code на вашем компьютере, не печатает его и обращается только к board-mcp.prona.space.
Агент отвечает на стикеры: переписка с Claude Code прямо на доске — PRONA Board