/pr-reply — ответить тем, кто уже согласился с нами разговаривать
Закон
Молчание в живом треде = провал, а не осторожность (CLAUDE.md §4.7). Отвечаю САМ: GitHub
выведен из-под гейта Наташи, «+» Антона не жду. Порог — 24 часа.
Живой ревьюер дороже нового PR. Человек, который написал на нашем PR, уже потратил своё
время — это самая тёплая дверь, какая у нас есть, и она закрывается от молчания, а не от
несогласия. Спорить с ним можно и нужно ([[objection-sparring-to-consensus]]), молчать — нет.
Шаг 1 — снять очередь (0 токенов, 0 своей сети)
python ~/.claude/scripts/pr_watch.py --digest
Секция 🔴 МЯЧ У НАС — это и есть очередь. Дальше глазами раздели её на три кучи:
| Кто говорил последним | Что делаем |
|---|
| живой человек | работаем сегодня, начиная с самого старого |
бот (codecov[bot], cubic-dev-ai[bot], review-notebook-app[bot], chatgpt-codex-connector) | чиним, если по делу; отвечать текстом не нужно |
⏳ STALE/CLOSE-бот (github-actions[bot]: «This PR is stale…», «will be closed in N days») | ИСКЛЮЧЕНИЕ ИЗ СТРОКИ ВЫШЕ: текст ОБЯЗАТЕЛЕН и он же есть лекарство. Коммент — единственное, что снимает ярлык и обнуляет счётчик (actions/stale по умолчанию держит remove-stale-when-updated: true). Молчание = удаление артефакта в объявленный срок. Писать СОДЕРЖАТЕЛЬНО: что закрыто по ревью · чем дешево ревьюить (что идёт в CI без ключа) · предложение ребейзнуть или подвинуть. Голый «bump» ⛔ (§1.5 спам ≠ громкость). Дату смерти брать фактом: gh api repos/<o>/<r>/contents/.github/workflows/stale.yaml → days-before-pr-stale + days-before-pr-close; конфига нет = «бота нет» (это claim, бессрочность не выдумывать). Живой случай 30.09: openai/openai-cookbook#2880, 74k звёзд, до удаления оставалось 7 дней, три прогона вахты промолчали — все три двери (pr_watch.py · вахта тредов · эта таблица) считали бота шумом. |
🔧 INTAKE-бот с МЕХАНИЧЕСКИМ флагом (graphiti-intake-bot и родня: «нужен linked issue Fixes #<N>», «нужна метка», «отметьте пункт чек-листа», «заполните шаблон») | ЛЕЧИТСЯ ПРАВКОЙ АРТЕФАКТА, А НЕ КОММЕНТОМ. Флаг снимает gh pr edit <N> --body-file / --add-label / правка заголовка — бот перечитывает PR на своём прогоне и гасит метку сам, в треде НОЛЬ шума. Коммент сюда = бамп (§1.5). Проверка, что сработало: метка-флаг ИСЧЕЗЛА (gh api repos/<o>/<r>/issues/<N> --jq '[.labels[].name]') — это независимый сигнал, а не наш claim. ⚠️ Ключевое слово Fixes #N ЗАКРЫВАЕТ issue на мерже: наш PR = часть issue → ставим слово (бот требует буквально его) И рядом scope-строку «это подмножество, снимите ключевое слово на squash или переоткройте», выбор отдаём мейнтейнеру, молча чужой issue не хороним. Живой случай 02.10: getzep/graphiti#1810, часы до 15.10, флаг needs-issue снят правкой тела за один вызов. |
🤖 СИНТЕТИЧЕСКИЙ ПИР БЕЗ СУФФИКСА [bot] (<имя><цифры>-coder / -code / -lang и родня: qaisermehdi3-coder, juancarlosrial76-code, eddyflores100-lang) | ЭТО НЕ ЖИВОЙ ЧЕЛОВЕК, И В ЧУЖОМ ТРЕДЕ ОТВЕТ ЕМУ = ШУМ МЕЙНТЕЙНЕРУ. Строка выше («боту отвечают коммитом») ловит только логин с [bot]; эти аккаунты проходят фильтр как люди и приносят полированный LLM-текст через часы после нашего коммента. Дешёвая проба ОДНИМ вызовом перед ответом: gh api users/<login> --jq '[.created_at,.public_repos,.followers,(.bio//"-")]' — молодой аккаунт + public_repos<=2 + followers<=1 + пустой bio = синтетический пир (живой мейнтейнер для сравнения: dsfaccini 2020 год, 70 репо, 46 фолловеров, bio). В ЧУЖОМ треде такой коммент долгом НЕ является, отвечаем только когда обращается мейнтейнер или автор. Живой случай 03.10: pydantic/pydantic-ai#8704 — мы ответили qaisermehdi3-coder коммент-в-коммент, и через 2 минуты assignee dsfaccini написал «bots: please refrain from replying to each other, we've got enough input here»; четвёртый коммент за 13 часов в треде, который мейнтейнер держит чистым. Журнал: класс reply-to-synthetic-peer-in-foreign-thread, 1-й датированный случай. |
| «нет снимка» | почти всегда позиция, добавленная СЕГОДНЯ нами же — см. блок ниже ДО любых сетевых вызовов |
⚠️ «Нет снимка» ≠ долг: сперва реестр, потом сеть (замер 27.09)
Позиция попадает в 🔴 без снимка просто потому, что сторож её ещё не видел, а --add был сегодня.
Дешёвый разбор — 0 сетевых вызовов, по самому реестру _outreach/outbound_github.json:
python3 -c "import json;d=json.load(open('$OBSIDIAN_VAULT/_outreach/outbound_github.json'));\
print('\n'.join('%s#%s kind=%s added=%s :: %s'%(i['repo'],i['number'],i.get('kind','issue'),i.get('added'),i.get('note','')[:70]) for i in d['prs'] if i.get('added')=='<сегодня>'))"
Две вещи, которые эта строка отдаёт даром:
added = сегодня + note «ответ … ждём реакции» → мяч у НИХ, это наш собственный свежий ответ. Не долг, сеть не нужна.
kind: discussion → у дискуссий НЕТ repos/<o>/<r>/issues/N, REST вернёт 404 на всё подряд.
Дверь только GraphQL: gh api graphql -f query='{repository(owner:"O",name:"R"){discussion(number:N){isAnswered updatedAt comments(last:2){nodes{author{login} createdAt}}}}}'.
⚠️ В zsh не собирай такой цикл через set -- $spec — $3 приезжает пустым и GraphQL падает
на discussion(number:); заведи check() { … "$1" "$2" "$3" … } и вызывай явно.
Замер 27.09: шесть позиций «нет снимка» → три дискуссии (REST дал 404, я списал это на мусор в реестре — неверно)
и три треда с нашим же ответом того же дня. Настоящий долг среди шести был один (наш PR с упавшим CI-ботом),
и он виден по kind + автору без единого запроса к GitHub.
⚠️ Поправка 05.10: note бывает ПУСТОЙ, и тогда реестр отдаёт только added (замер: 17 позиций разом)
Эвристика выше опирается на note со словами «ответ … ждём реакции». В тот день она не сработала:
из 17 красных «нет снимка» 13 имели note: None, потому что веер другого лейна узла добавил их
без заметки. Голого added = сегодня для вердикта мало — он не отличает «мы ответили» от «нас спросили».
Добирается ОДНИМ пакетным циклом (1 вызов модели на все позиции, не по одной):
for p in "owner/repo 123" "owner/repo2 456"; do R="${p% *}"; N="${p##* }"
echo "$R#$N by=$(gh api repos/$R/issues/$N --jq '[.user.login,(.pull_request!=null|tostring)]|join("|")') \
last=$(gh api repos/$R/issues/$N/comments?per_page=100 --jq '.[-1]|[.user.login,.created_at]|join("|")')"
done
Вердикт читается механически: last = tonydzi сегодня → не долг (наш свежий коммент) · last пустой
→ наш PR без комментов, не долг · last = *[bot] → не долг · last = живой человек → в разбор.
В тот заход так закрылись 17 позиций, и настоящие долги пришли не отсюда, а из mentions/involves (блок ниже).
⚠️ Цикл писать на bash с массивом (см. предупреждение про set -- $spec в zsh выше).
⚠️ Digest видит только реестр — добери упоминания (замер 15.09)
gh api -X GET search/issues -f q='mentions:tonydzi updated:>=<вчера>' --jq '.items[].html_url'
По каждой ссылке: последний писал не мы и обращался к нам → в очередь; нет в реестре → pr_watch.py --add <url>.
15.09 так нашлись agno#10086 и langgraph#8764: ответ нам ждал 15–40ч, а в outbound_github.json тредов не было.
Ссылка даёт 404 через REST /issues/N → сперва GraphQL discussion(number:N): 15.09 все 6 «404» оказались discussions.
⚠️⚠️ Дайджест показывает НОЧНОЙ снимок — «🟢 активность от tonydzi» НЕ значит, что мяч у них (замер 20.09)
--digest делает 0 сетевых вызовов, поэтому дневной заход видит картину прошлой ночи. Тред, где
последним писали МЫ ночью, стоит в 🟢 с подписью «активность от tonydzi» — и остаётся там, даже если
инженер ответил нам сегодня утром: красным он станет только на следующем ночном тике, через сутки.
Замер 20.09: дайджест дал 1 настоящий долг (litellm#32642), а добор mentions:tonydzi дал ещё ДВА,
и оба были ответами живых инженеров на наши же комментарии того же дня — agno#10366 (18:26Z) и
langgraph#8039 (14:31Z); в дайджесте оба стояли зелёными «активность от tonydzi».
Правило: для КАЖДОГО треда из добора, обновлённого за 24ч, смотри последний коммент ФАКТОМ,
а не поле author из снимка. Зелёная строка с нашим именем — довод НЕ проверять только если
добор её не поднял.
⭐ КАК ДЕЛАТЬ ДОБОР ОДНИМ ВЫЗОВОМ (замер 22.09, 5-й случай класса подряд). Проверять последнего
комментатора по 40–60 позициям REST-парами — это ~110 сетевых вызовов; один GraphQL с алиасами даёт
то же за один. Шаблон: на каждую позицию aN: repository(owner:"o", name:"r"){ issueOrPublicRequest…},
внутри issueOrPullRequest(number:N) с двумя инлайн-фрагментами (... on Issue / ... on PullRequest),
в каждом comments(last:1){ nodes{ author{login} createdAt } }. comments(last:1) снимает всю возню
с пагинацией: GraphQL сам отдаёт ХВОСТ, грабля .[-1] первой страницы к нему не относится.
Два условия, иначе прибор соврёт: (1) gh api graphql возвращает ненулевой код при ЧАСТИЧНЫХ ошибках
(позиция оказалась discussion → «Could not resolve to an issue or pull request»), но data в stdout
при этом ЕСТЬ — парсь stdout, не выходи по returncode, иначе одна discussion гасит все 55 позиций;
(2) сравнивай логин с tonydzi явно — «наш последний» и «их последний» это и есть весь вердикт.
Замер 22.09: 4 красных дали 2 настоящих долга, а ещё 4 (claude-agent-sdk-python#1276, #1271,
mem0#6859, hermes-agent#88371) стояли зелёными или в «чужих тредах».
⛔ ВТОРАЯ ГРАБЛЯ ТОГО ЖЕ ПРИБОРА (замер 21.09): .[-1] БЕЗ ПАГИНАЦИИ ВРЁТ НА ДЛИННОМ ТРЕДЕ.
Эндпоинт GET /repos/{o}/{r}/issues/{N}/comments игнорирует sort и direction (в его контракте
их просто нет — только since, per_page, page), поэтому --jq '.[-1]...' отдаёт последний
коммент ПЕРВОЙ СТРАНИЦЫ, то есть №100, а не последний в треде. 21.09 так соврал gastownhall/beads#5877
(101 коммент): прибор показал «последним писал tonydzi», хотя реальный последний был от @The1nk
в тот же день, и тред чуть не уехал в «не долг». Чем людней тред — тем вероятнее ложь, а людные
треды мы берём ПЕРВЫМИ (§4.7 «самый людный живой тред домена первым»), так что грабля лежит
ровно на приоритетной дорожке.
Лечение — брать последнюю страницу явно:
cnt=$(gh api "repos/$repo/issues/$num" --jq '.comments')
page=$(( (cnt + 99) / 100 ))
gh api "repos/$repo/issues/$num/comments?per_page=100&page=$page" \
--jq '.[-1] | .user.login + " @ " + .created_at'
Готовый вариант на список тредов (считает страницу сам, печатает 2 последних коммента):
~/.claude/skills/pr-reply/gh_last_comment.sh <owner/repo#N> [...] (живёт рядом со скиллом —
единственным потребителем; в ~/.claude/scripts не кладём, там 21.09 висела чужая EXCLUSIVE-табличка
ON AIR на зоне fleet-code, и дверь onair_fleet_guard.py запись справедливо отбила).
⚠️ «Последний не мы» ≠ «ответ нам» (замер 14.09)
Digest ставит «ОТВЕТ ЗА НАМИ», если последним писал не tonydzi. В НАШЕМ PR это правда. В чужом
большом issue, где мы — один из 40 участников, это чаще посторонний, который пишет не нам.
Замер 14.09: из 53 позиций адресованы нам были 15; ещё ~25 — посторонние (anthropics/claude-code ×9 и
др.), 3 уже отвечены соседом, 6 ссылок 404. Прежде чем писать, прочитай последние 2–3 реплики: есть
@tonydzi, ответ на наш аргумент или просьба к автору/ревьюеру, которым были мы? Нет → не отвечаем,
ответ без новой ценности = спам (§1.5). Боты digest уже не считает (is_bot в pr_watch.py).
Leak-scan тела ответа: паттерны repo_release_leakscan.py (FAIL+WARN) по файлу --body-file, плюс
последняя строка обязана нести github.com/tonydzi.
⚠️ СОБЕСЕДНИК САМ ЗАКРЫЛ РАЗГОВОР — это не долг и не 🪦 (замер 24.09)
Третий вид ложного красного, отдельный от «пишут не нам». Мяч формально у нас, реплика адресована
НАМ и по существу — но её содержание есть явный выход из разговора: «letting this one rest» ·
«I'm going to stop here» · «I'd rather it sit finished» · «leaving it open for a maintainer» ·
«@maintainer PTAL» (пинг третьему лицу, а не нам). Ответ туда не несёт новой ценности и читается
как «мне нужно последнее слово».
Замер 24.09: из 8 красных ТРИ были такими (BerriAI/litellm#34140 и #41395 — DanBrima закрыл оба
одной парой реплик; google/adk-go#1543 — wolo-lab пингует @wojtas000). Ещё одна —
осуждённый ранее fieldkit#77. Итого половина очереди дня.
Вердикт пишется в журнал В ТЕХ ЖЕ СЛОВАХ, что и осуждённые позиции Шага 1-бис, и работает так же:
позиция остаётся красной навсегда (у pr_watch нет mute — проверено --help), следующий заход
смотрит только, появился ли комментарий НОВЕЕ даты вердикта.
⭐ Граница: это НЕ повод молчать там, где человек закончил репликой к нам — благодарность с
техническим содержанием, поправка нашего аргумента, вопрос «а как бы вы сделали». Тест один:
после моей реплики у собеседника появится работа или знание? Нет — не пишем.
⚠️ Сначала спроси: чей это PR
Реестр держит оба направления, и работа в них разная. Проверяется одной командой:
gh pr view <N> --repo <owner/repo> --json author --jq .author.login
| Автор PR | Кто мы | Что от нас ждут |
|---|
| мы | автор | сделать правку, которую попросил ревьюер → шаги 3–4 |
| посторонний | ревьюер | перепроверить их фикс и вынести вердикт → шаг 3-бис |
Замер 06.08: из трёх живых людей в очереди двое ждали нас как РЕВЬЮЕРОВ, а не как
авторов. Спутать эти роли — значит прийти в чужой тред с «поправил, посмотрите», где от
нас ждали проверки. Роль определяется автором PR, а не тем, в чьём репозитории он открыт.
⚠️ Очередь ровно так же свежа, как снимки сторожа. Дата снимка печатается в шапке digest —
если она вчерашняя, очередь тоже вчерашняя. Не выдавай старый список за сегодняшний.
🪦 Правило трёх дней (anton «+» 07.08)
Секцию 🪦 могилы digest выделяет сам (GRAVE_DAYS=3 в pr_watch.py): мяч у НИХ и
тишина >3 дней после нашего последнего хода. Замер по 54 нашим PR: все 8 мержей случились
за 0–3 дня, дальше — никогда. Поэтому: могилу не бампаем и не воскрешаем (один
вежливый бамп на PR разрешён только ПОКА тред живой, второй запрещён всегда — §seed 778).
Энергия из могил уходит живым тредам и ревью чужих PR. Могила ≠ провал: это нормальная
цена присутствия (§1.5), закрывать PR самим не нужно — пусть висит, вдруг проснутся сами.
⭐ Мяч у НИХ, но протух НАШ собственный claim — это новая ценность, а не бамп (замер 19.09)
Могила и «ждём их» запрещают бамп без новой ценности. Но наше опубликованное утверждение —
такой же скоропортящийся продукт, как и цифра ([[prichina-kak-claim]]): версия вышла, тег
переехал, баг уехал в релиз. Проверка дешёвая и делается ДО решения «молчим»: то, что мы в этом
треде утверждали про версии/релизы/состояние — всё ещё правда СЕГОДНЯ?
Протухло → пишем, даже если формально отвечают они, и первым абзацем правим себя, а не
повторяем просьбу. Замер, родивший правило: в google/adk-python#6941 мы 05.09 написали «ни один
опубликованный релиз не несёт bfeb04c» — 10.09 вышел v2.9.0, который его несёт, и баг из
«живёт на main» стал «стоит у пользователей». Проверка = две команды
(gh api repos/<repo>/compare/<tag>...<sha> --jq .status → behind значит содержится;
gh api repos/<repo>/releases --jq '.[0:5][] | "\(.tag_name) \(.published_at)"').
⚠️ Граница: это НЕ лазейка для бампа. Новая ценность = факт, который изменился в мире и меняет
приоритет для мейнтейнера; «прошла неделя» и «напоминаю» ценностью не являются. Перепроверять
на опубликованном артефакте, а не на чекауте: pip install <pkg>==<version> + контрольный кейс,
который ОБЯЗАН пройти — иначе красная строка ничего не доказывает.
Шаг 2 — прочитать, чего именно хотят
gh pr view <N> --repo <owner/repo> --json reviews,comments
gh api repos/<owner/repo>/pulls/<N>/comments --jq '.[] | "\(.user.login) | \(.path):\(.line) | \(.body)"'
Второй вызов обязателен: инлайн-комменты к строкам кода не видны в первом. Именно в них
обычно и лежит просьба (замер 06.08: go-sdk#1148, «let's remove this comment» — только там).
Прочитай просьбу буквально. «Убери этот комментарий» — значит убрать, а не сократить и не
переписать красивее. Мейнтейнер знает стиль своего репо лучше нас.
Шаг 3 — сделать то, что просили
Правка → прогон того, что репо просит прогнать (тесты/линт/генератор доков) → пуш в ту же
ветку. Если доки генерируются (//go:generate, weave, nbconvert), правь и исходник, и
сгенерированный файл — иначе ревьюер увидит рассинхрон и вернёт PR второй раз.
Не согласен — не молчи и не уступай молча: возражения по пунктам + «переубеди меня»
([[objection-sparring-to-consensus]]). Уступка без аргумента — такой же провал, как спор без него.
Шаг 3-бис — если ревьюеры МЫ: проверять доказательство, а не поведение
Автор написал «поправил всё по вашим замечаниям». Прогнать его тесты и увидеть зелёное —
недостаточно: зелёный тест доказывает, что тест проходит, а не что защита работает.
Порядок:
- Забрать ветку:
git fetch upstream pull/<N>/head:pr<N> && git checkout pr<N>.
- Прогнать так, как автор говорит, и назвать точный способ прогона в ответе (какие ячейки,
с сетью или без, какой python) — иначе вердикт невоспроизводим.
- Сломать по одной защите в РАБОЧЕМ коде, не трогая тесты, и убедиться, что тест краснеет.
Не поймал — защита есть только на бумаге, и это находка ценнее любого стилевого замечания.
⚠️ Грабли метода (наступил 06.08): мутация, которая переименовывает строку и в проверке, и в
тесте разом, «проходит» всегда — тест сравнивает переименованное с переименованным. Ломать
надо ПОВЕДЕНИЕ: убрать return refused, а не переименовать код ошибки. Первый прогон дал три
ложных «ПРОПУЩЕНА» именно из-за этого; сначала я усомнился в их коде, и был неправ.
⚠️ Вторые грабли: изолированный прогон пары ячеек не тянет импорты из ранних. NameError —
это скорее дефект твоей выборки, чем их notebook. Проверь, откуда символ, прежде чем писать
человеку, что у него сломано.
⚠️ Чужой РЕПРО (не PR) проверяй так же: у каждой строки вывода нужен контроль, который МОГ провалиться — негатив для «гейт вернул True» (гейт вообще умеет False?) и позитив для «операций 0» (у заведомо правильного случая они есть?). Замер 16.09 litellm#41395: оба вывода верны, но без контролей не доказаны; ContextVar-stash в цикле давал 0 и у родителя.
Вердикт формулируй как факт с цифрой («47 outcomes, exit 0, три мутации пойманы»), а не как
«выглядит хорошо». Закончил — так и скажи: «nothing further from me on this», чтобы автор не
ждал следующего круга.
Шаг 3-тер — ВОЗРАСТ ИСТОЧНИКА ЦИФРЫ (пережито 13.09.2026, класс claim-judged-against-current-file-not-run-date)
Цифра уезжает в ЧУЖОЙ тред → перед отправкой ответь на один вопрос: когда появилось то, по чему
я сужу? Судишь прогоны/логи/историю по ТЕКУЩЕМУ содержимому файла, настройки или поля — проверь,
что оно существовало на момент тех событий (ls бэкапов, mtime, git log, дата правки). Не проверил
— цифра не уезжает, уезжает формулировка «замерено с такой-то даты».
Замер, родивший правило: в anthropics/claude-code#66402 мы опубликовали «26 из 43 безлюдных
прогонов (60%) шли не на объявленной модели». Поле model: дописали в определения задач 10.09,
а под нож пошли прогоны 08–10.09 — объявления тогда просто не было. Честный замер дал 0 дрейфа и
обратный вывод. Отзыв: issuecomment-5656756608, строка в 00-System/Mycroft-Correction-Log.md.
Прибор с этим гвардом: ~/.claude/scripts/task_model_drift.py (+ регресс _test_task_model_drift.py).
⚠️ Усиление: чем ВЫГОДНЕЕ цифра нашему аргументу в чужом треде, тем строже проверка
([[self-serving-conclusion-needs-stronger-proof]]). Нашу ошибку поймало требование чужого инженера
сделать репро, а не наша собственная проверка — значит проверку надо делать ДО отправки.
⚠️ Шаг 3-тер-бис — РАЗБОР ИСТОЧНИКА, А НЕ ТОЛЬКО ЕГО ВОЗРАСТ (пережито 10.10.2026, стоило публичного отзыва)
Второй вопрос к той же цифре: на каком СЛОЕ я её мерил? Транскрипты (~/.claude/projects/*/*.jsonl),
дампы API, логи с JSON внутри — это ЭКРАНИРОВАННЫЙ текст: каждый перевод строки лежит двумя символами,
каждый не-ASCII символ — шириной своей escape-последовательности. grep/regex по СЫРОЙ строке мерит
экранирование, а не содержимое, и длины приезжают завышенными. Меряй после json.loads, не до.
Замер, родивший правило: в anthropics/claude-code#70555 мы опубликовали «превью 5812 символов при
обещанных 2KB, значит ярлык не является верхней границей». С разбором строки как JSON потолок оказался
ЖЁСТКИМ: ровно 2000 символов, 109 блоков из 594 стоят точно на нём, в 2001..2047 — ноль. Выброс был
дефектом нашего прибора. Отзыв: issuecomment-6103021040.
⭐ Но перемер нашёл дефект КРУПНЕЕ, и это типично: ярлык считает байты, а срез — символы, поэтому у
не-ASCII вывода реально приезжает до 1.72× обещанного (430 блоков из 594, 72%). Отзыв своей цифры —
не потеря лица, а способ найти настоящий баг; отзывай ПЕРВЫМ абзацем и назови дефект своего прибора
поимённо, иначе собеседник решит, что виноват корпус.
⛔ Два разных извлечения на одну цифру, прежде чем она уедет: regex по разобранным строкам И
проход без regex (индексная арифметика), который НЕ может быть обрезан твоим же шаблоном. Совпали —
цифра едет; разошлись — едет диапазон и причина.
Шаг 4 — ответить коротко
Форма, которая работает (замер: единственная, дававшая ответ живого человека):
- что сделал — фактом, без «спасибо за ревью, очень ценно»
- ссылка на коммит, если правка не однострочная
- ⭐ подпись-CTA (anton 10.09, supersedes прежнее «никакой подписи»): ОДНА строка со ссылкой
github.com/tonydzi, источник $OBSIDIAN_VAULT/08-Templates/github-touch-signature.md.
Carve-out против спама: на НАШЕМ PR в идущем диалоге достаточно ОДНОЙ подписи на тред —
уже подписывался в этом треде → в следующих репликах можно без неё. Первое касание чужого
треда (ревью/коммент) — подпись всегда. Канон: память cta-signature-every-github-touch;
сторож cta_signature_watch.py.
Голос Майкрофта (§3.3): пишу я → называюсь. Короткий технический ответ на чужом PR — цель
≤5 слов на само раскрытие, тон по каналу.
gh pr comment <N> --repo <owner/repo> --body "Done — <что именно>. <ссылка/деталь>."
⭐ НА СВОЁМ PR РЕВЬЮ ТОЖЕ РАЗРЕШЕНО (замер 28.09, SK#14371): GitHub запрещает автору только --approve и --request-changes, а gh pr review <N> -R <owner/repo> --comment --body-file <файл> на СВОЁМ PR проходит и создаёт объект Review (проверено: review 5342194561 от tonydzi на своём же PR). Ответ мейнтейнеру на его блокирующее ревью подаётся именно так, а не gh pr comment — тогда переписка видна в фильтре ревью, а не только в ленте комментов.
⭐ ТРИАЖ PR ЧИТАЕТ pulls/N/reviews, А НЕ ТОЛЬКО issues/N/comments (тот же замер 28.09): блокирующее ревью мейнтейнера НЕ появляется в issues/N/comments — там было пусто, и позиция читалась как «активности нет». Ревью eavanvalkenburg висело в pulls/14371/reviews четыре часа. Для позиции-PR смотри обе ручки, иначе самый дорогой долг захода выглядит как тишина.
⭐ ПОЧЕМУ ЗДЕСЬ КОММЕНТ, А НЕ РЕВЬЮ (граница правила, замер 05.09): это РЕПЛИКА в уже идущем диалоге — она комментом и подаётся. Но ПЕРВОЕ касание по ЧУЖОМУ PR идёт только через gh pr review <N> -R <owner/repo> --comment --body-file <файл>: gh pr comment не создаёт объект Review, и работа не видна ни в графе вкладов, ни в фильтре reviewed-by:tonydzi (в чужих PR за 30 дн: 127 комментов против 7 ревью = 5.2% видимых). У ISSUE ревью НЕ СУЩЕСТВУЕТ — там коммент нормален. Канон: Библия reglament-v-chuzhom-pr-pishem-revyu-a-ne-kommentariy + файл-константа $OBSIDIAN_VAULT/08-Templates/github-review-not-comment.md.
Шаг 5 — записать кредит автору совета
Совет постороннего человека — это вклад, и он именной (приказ Антона 06.08, скилл
/alpha-credit):
python ~/.claude/scripts/alpha_credit.py add --source gh --author "<login>" --handle "<login>" --url "<url PR>" --advice "<что подсказал>" --kind critique --consent public --ref "gh:<owner/repo>#<N>:<id>"
Шаг 4-тер — «FIX YOUR CLA» В GOOGLE-РЕПО: СНАЧАЛА ПОСМОТРИ НА ТРЕЙЛЕРЫ КОММИТА (замер 29.09.2026)
Мейнтейнер пишет «please fix CLA so we can get this merged», cla/google = FAILURE.
⛔ НЕ беги подписывать: подпись может быть уже на месте, а валит её ВТОРОЙ контрибьютор.
Корень (доказан на ChromeDevTools/chrome-devtools-mcp#2852): бот читает ТРЕЙЛЕРЫ, а не людей.
Строка Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> — для бота полноценный
контрибьютор, у которого CLA нет и быть не может. Текст ошибки при этом отправляет подписывать то,
что уже подписано: тот же коммит-email 194927794+tonydzi@users.noreply.github.com даёт
cla/google = SUCCESS на google/adk-go#1543.
Порядок разбора (2 вызова, не поход в браузер):
gh api repos/<o>/<r>/commits/<sha> --jq '.commit.message' — есть Co-Authored-By? есть ли чужой email?
- Сверь с нашим PR в другом репо того же вендора:
gh pr view <N> -R google/<repo> --json statusCheckRollup —
если там cla/google SUCCESS с тем же email, подпись есть, и дело в трейлере.
Лечение (провенанс §5.11 НЕ страдает): снять ТОЛЬКО Co-Authored-By, оставить
Assisted-by: (называет инструмент, авторства не заявляет) и Signed-off-by: (DCO).
code_provenance_lint.py требует Assisted-by / Machine / Account / Operator и Co-Authored-By
НЕ требует — проверено 29.09, свой гейт этим не ломается.
Амендим, пушим --force-with-lease, и ДОКАЗЫВАЕМ, что ревью не обнулилось:
git diff --stat <old>..<new> обязан быть ПУСТ (контент байт-в-байт), иначе мейнтейнеру придётся
перечитывать диф. Потом ждём перепрогона чека и смотрим ФАКТ:
gh api repos/<o>/<r>/commits/<new-sha>/check-runs --jq '.check_runs[]|select(.name|test("cla";"i"))|[.name,.conclusion]|@tsv'.
⚠️ Подписание CLA само по себе — НЕ Tier-2 и не повод для аска: мандат Антона 30.08 ([[cla-sign-for-anton-autonomous]],
рецидив 14–15.09 Kong#14999) покрывает ЛЮБОЙ способ подписи. Аск остаётся только на деньги, equity,
права сверх стандартного контрибьюторского гранта и NDA.
🤔 Обобщение не проверено: замер один (Google CLA). Для EasyCLA/cla-assistant других вендоров правило
пока гипотеза — но трейлеры смотреть дешевле, чем подписывать заново.
⛔ macOS: timeout не существует. Ждать перепрогон чека — bash-циклом с seq и sleep, не timeout.
Шаг 5-бис — ответ инженера = контент-момент (anton 24.08: «сразу в контент-воронку»)
Живой инженер (мейнтейнер / посторонний контрибьютор, не бот) ответил в нашем треде или на
нашу лечилку → в ТОМ ЖЕ заходе черновик в воронку:
python3 $IMPORTS_ROOT/content-factory/content_miner.py capture --title "<хук: кто ответил и что это значит>" --note "<суть + ссылка на тред>" --tier teaser
Draft-first: это черновик в triage, публикацию решает гейт воронки — здесь только захват.
⛔ Не захватываем: ответы ботов · чисто механические «done/merged» без истории · приватное.
Тир по весу: реплика = teaser; мерж нашего фикса или разговор с мейнтейнером Anthropic/OpenAI = medium.
Канон: [[everything-becomes-content]] + [[content-miner-reflex]] + реглумент reglament-pochinil-u-sebya-srazu-razday-miru (реакция мира на раздачу — вторая половина той же трубы).
Шаг 6 — закрыть петлю
Ответил → позиция уходит из «мяч у нас» сама на следующем тике сторожа. Проверять не надо;
надо проверить обратное: если через сутки она всё ещё там — ответ не доехал (не та ветка,
не тот PR, коммент ушёл в issue).
⭐ Шаг 6-прим — ВЫЧИТАЙ ОТПРАВЛЕННОЕ ОБРАТНО С СЕРВЕРА, И СВЕРЯЙ ПО md5, А НЕ ПО ДЛИНЕ (пережито 04.10.2026)
gh issue comment печатает URL и выходит с 0 — это доказывает ПРИЁМ запроса, не целость тела.
Второй заход (§4.3-бис «другим способом, чем в первый раз») для исходящего коммента выглядит так:
взять id из URL и прочитать тело ОБРАТНО, а не пересказывать свой файл.
ID=<цифры после issuecomment->
gh api repos/<o>/<r>/issues/comments/$ID --jq '.body' > /tmp/srv.txt
python3 -c "import hashlib,io;l=io.open('$F',encoding='utf-8').read().rstrip(chr(10));s=io.open('/tmp/srv.txt',encoding='utf-8').read().rstrip(chr(10));print('IDENTICAL' if l==s else 'DIFF', hashlib.md5(l.encode()).hexdigest(), hashlib.md5(s.encode()).hexdigest())"
⚠️ ЛОВУШКА, КОТОРАЯ ВЫГЛЯДИТ КАК ОБРЕЗАННЫЙ КОММЕНТ: сверка по ДЛИНЕ даёт ложное
расхождение. jq '.body|length' считает кодпоинты, wc -c — байты; каждый em-dash
(—) в нашем тексте — 3 байта и 1 кодпоинт, то есть +2 байта расхождения на штуку.
Замер 04.10 (jerryfane/herdr#16): len=2139 с сервера против 2154 локально выглядело как
потеря 15 символов, хотя md5 совпал байт в байт — 7 em-dash'ей (14) плюс перевод строки.
Длина как прибор здесь СЛЕПА; судит только md5 (или прямое сравнение строк).
Тело уехало битым — чиним, а не оправдываемся: gh api -X PATCH repos/<o>/<r>/issues/comments/$ID -f body="$(cat "$F")".
⚡ Шаг 6-бис — НАШ PR ЗАКРЫЛИ: ВЫНЬ УКАЗАТЕЛЬ ПРЕЖДЕ, ЧЕМ ЗАКРЫТЬ ТРЕД (30.09.2026)
Закрытый PR уходит из очереди этого скилла — и вместе с ним уходит подарок: мейнтейнер,
отказывая, почти всегда говорит, куда идти вместо этого (доска, лейбл, CONTRIBUTING, чужой PR).
Поэтому позиция «наш PR стал CLOSED и не MERGED» закрывается не молчанием, а тремя действиями:
python3 ~/.claude/scripts/gh_pr_discipline.py doors --scan — указатель попадает в очередь /issue-match;
- поблагодарить закрывшего ОДНОЙ строкой, если он дал указатель (кредит вперёд, §OUTBOUND_GATE п.3);
глухое «closing, not a fit» благодарности не требует — молчание тут законно;
- ⛔ НЕ переоткрывать и НЕ пере-пушивать отклонённое: дверь теперь в другом месте.
Цена урока: haystack#12142 → их доска → haystack-integrations#566 смержен через 7 дней.
Единственный мерж кампании, полученный из ОТКАЗА.
⚠️ Шаг 6-тер — РЕБЕЙЗ ЗАКОНЕН НЕ ВЕЗДЕ: перед force-push проверь запрет репо (01.10.2026)
Наш PR в конфликте — рефлекс «ребейз + git push --force-with-lease». В части репо этот
ход уничтожает уже полученное ревью, а ревью мейнтейнера дороже самого PR.
ЗАМЕР, который это оплатил: 01.10 ребейз спас anthropics/claude-cookbooks#787 (одобрение
@Droptops висело 46 дней в конфликте) — и в тот же час на QwenLM/qwen-code#12875 бот
pr-force-push-reminder написал: «do not rebase or force-push to an active PR as it
invalidates existing review comments… the bots always squash all changes into a single
commit automatically». Один и тот же ход: там лечит, тут калечит.
ПОРЯДОК (30 секунд, до любого push):
gh pr view <N> -R <owner/repo> --json comments --jq '.comments[].body' | grep -i "force-push\|rebase" — бот или мейнтейнер уже просили не делать?
gh api repos/<o>/<r>/contents/CONTRIBUTING.md --jq .content | base64 -d | grep -i -A3 "force.push\|rebase"
- Нашёл запрет → ⛔ ребейз. Вливай обычным мержем:
git merge upstream/main в ветку PR,
он конфликт закрывает и ревью-комменты сохраняет. В треде одной строкой: «merged main
instead of rebasing, to keep your review comments anchored».
- Запрета не нашёл → ребейз разрешён, но пиши в тред, ЧТО изменилось и что одобренный
контент не двигался (иначе ревьюеру придётся перечитывать всё).
⛔ Не ослабляется: «нет запрета» = «я искал в двух местах и не нашёл» (§5.4), а не «можно всё».
Класс в журнале: rebase-wipes-the-review-we-waited-for.
Границы
- Мерж чужого кода, юридическое (CLA/DCO), деньги, удаление чужого — Tier-2, к Антону.
- Массовые одинаковые комменты веером — запрещено: громкость ≠ спам (§1.5).
- Ответ боту текстом — мусор в чужом треде. Боту отвечают коммитом.
Как понять, что скилл работает
Считаем не число ответов, а возраст самого старого живого долга в digest. Работает —
если он держится ниже суток. Растёт — прибор есть, а рук нет, и это отдельный разговор,
а не повод отвечать быстрее.
⭐ Чёрный юмор обязателен (anton 10.09.2026, голосом)
Любой текст, который этот скилл отправляет ЖИВОМУ человеку наружу, несёт чёрную самоиронию — ровный служебный тон = дефект, объяснять надо не шутку, а её отсутствие. Строку берёшь через /mycroft-joke (полка (д) банка + строки 56-62 под холодное касание), приёмка — /taste-check P36. ⛔ Не шутим ровно в трёх зонах, и они не про вкус: строго-научная публикация (arXiv/журнал/JOSS) · юр.обязательство и анкета · красный список банка (чужая иконография угнетённых, псевдо-слуры, отрицание что я ИИ, юмор поверх диагноза/денег/беды собеседника). Одна шутка на тред. Канон: CLAUDE.md §3.3 §Поправка 10.09 + [[dark-humor-default-everywhere-external]].
Шаг 3-кватер — REST отдал 403 «rate limit exceeded» при пустом счётчике → пиши через GraphQL (замер 07.10.2026, pr-reply-daily Mac16)
Симптом: каждый gh api repos/... отвечает API rate limit exceeded for user ID …, а gh api rate_limit при этом показывает core.remaining=5000, used=0.
✅ Причина доказана 08.10.2026: это ПЕРВИЧНЫЙ часовой бак аккаунта (текст вторичного лимита другой — «secondary rate limit»), съеденный тиком pr_watch.py 15:15 (658 тредов × 5 REST = ~3300/час) плюс соседями. Тело /rate_limit на этом токене СЛЕПО: каждый вызов отдаёт фантомное окно (used=0, reset = сейчас+1ч и ползёт). Настоящий бак — только по заголовкам живого ответа: python3 ~/.claude/scripts/_shared/gh_budget.py. Корень закрыт: pr_watch сперва спрашивает GraphQL-пачкой updatedAt и платит REST только за сдвинувшиеся треды (замер: 676 вместо ~3300), а на первом 403 останавливает тик; сторож _test_gh_rest_limit.py.
Дверь: GraphQL считается ОТДЕЛЬНЫМ баком (graphql.remaining живой) и умеет и читать хвосты (issueOrPullRequest(number){ comments(last:4){…} }, семь тредов одним запросом), и писать: python3 ~/.claude/skills/pr-reply/gh_gql_comment.py <owner> <repo> <N> <body_file> = mutation addComment + md5-сверка тела по readback (exit 0 только при MATCH). Инструмент лежит рядом со скиллом (единственный потребитель; ~/.claude/scripts в тот вечер держал чужой ON AIR). Работает для issue и PR одинаково; гейты paragraph_lint && leak_scan && ставятся перед ним той же цепочкой &&.
⛔ Не замена ревью: первое касание в чужой PR остаётся gh pr review --comment (REST); 403 на нём → ждать окно либо писать с другого узла, не подменять ревью комментом молча.
<!--kit-footer-->
Like this skill? It is one of 100 in second-brain-starter-kit: the second brain we built for ourselves and run every day at Palo Alto AI Research Lab. Install the whole set with npx skills add tonydzi/second-brain-starter-kit. Everything is open source and free, so take what you need.
Flagships worth a look on their own: secondop-panel (a second opinion from a panel of external models), claude-memory-tidy (stop your agent's memory from rotting), telegram-mcp-kit (your own Telegram over MCP in about 15 minutes).
Author: Anton Dziatkovskii, Palo Alto AI Research Lab. Telegram @tonydzi - WhatsApp +1 341 222 9178 - X @Tony_Stef_
Engineers: want to test-drive this setup? Message me. I hand out free starter seeds to engineers who test and report back, and custom skill requests are welcome.